Django定制用户电子邮件账户验证

39

我希望在Django中添加电子邮件帐户验证。我尝试使用django-registration应用程序来实现,但似乎它还没有更新为完全兼容自定义用户模型,这会导致太多问题。是否有其他可靠且文档完整的应用程序可以让我在Django中的用户注册时发送验证电子邮件?


你如何处理新用户的注册?是使用自定义表单吗?如果是,你可以重写保存方法来发送电子邮件(将用户创建为非活动状态,生成一个链接并在电子邮件中发送,在访问此链接时激活用户)。 - Raphael Laurent
是的,我正在使用自定义的UserCreationForm来创建用户。因此,在该保存方法中,在user = super(UserCreationForm, self).save(commit=False)之后,我应该设置user.is_active = False,然后发送带有确认代码的电子邮件,而且没有任何插件,这可以简单地完成吗? - sdotslezek
2个回答

65

我如何个人处理电子邮件注册:

首先,我的个人资料扩展Django用户 (models.py):

class Profile(models.Model):
    user = models.OneToOneField(User, related_name='profile') #1 to 1 link with Django User
    activation_key = models.CharField(max_length=40)
    key_expires = models.DateTimeField()

forms.py文件中,Registration类:
class RegistrationForm(forms.Form):
    username = forms.CharField(label="",widget=forms.TextInput(attrs={'placeholder': 'Nom d\'utilisateur','class':'form-control input-perso'}),max_length=30,min_length=3,validators=[isValidUsername, validators.validate_slug])
    email = forms.EmailField(label="",widget=forms.EmailInput(attrs={'placeholder': 'Email','class':'form-control input-perso'}),max_length=100,error_messages={'invalid': ("Email invalide.")},validators=[isValidEmail])
    password1 = forms.CharField(label="",max_length=50,min_length=6,
                                widget=forms.PasswordInput(attrs={'placeholder': 'Mot de passe','class':'form-control input-perso'}))
    password2 = forms.CharField(label="",max_length=50,min_length=6,
                                widget=forms.PasswordInput(attrs={'placeholder': 'Confirmer mot de passe','class':'form-control input-perso'}))

    #recaptcha = ReCaptchaField()

    #Override clean method to check password match
    def clean(self):
        password1 = self.cleaned_data.get('password1')
        password2 = self.cleaned_data.get('password2')

        if password1 and password1 != password2:
            self._errors['password2'] = ErrorList([u"Le mot de passe ne correspond pas."])

        return self.cleaned_data

    #Override of save method for saving both User and Profile objects
    def save(self, datas):
        u = User.objects.create_user(datas['username'],
                                     datas['email'],
                                     datas['password1'])
        u.is_active = False
        u.save()
        profile=Profile()
        profile.user=u
        profile.activation_key=datas['activation_key']
        profile.key_expires=datetime.datetime.strftime(datetime.datetime.now() + datetime.timedelta(days=2), "%Y-%m-%d %H:%M:%S")
        profile.save()
        return u

    #Sending activation email ------>>>!! Warning : Domain name is hardcoded below !!<<<------
    #The email is written in a text file (it contains templatetags which are populated by the method below)
    def sendEmail(self, datas):
        link="http://yourdomain.com/activate/"+datas['activation_key']
        c=Context({'activation_link':link,'username':datas['username']})
        f = open(MEDIA_ROOT+datas['email_path'], 'r')
        t = Template(f.read())
        f.close()
        message=t.render(c)
        #print unicode(message).encode('utf8')
        send_mail(datas['email_subject'], message, 'yourdomain <no-reply@yourdomain.com>', [datas['email']], fail_silently=False)

现在,在views.py中,我们需要处理所有这些,让我们开始吧:
注册视图:
def register(request):
    if request.user.is_authenticated():
        return redirect(home)
    registration_form = RegistrationForm()
    if request.method == 'POST':
        form = RegistrationForm(request.POST)
        if form.is_valid():
            datas={}
            datas['username']=form.cleaned_data['username']
            datas['email']=form.cleaned_data['email']
            datas['password1']=form.cleaned_data['password1']

            #We generate a random activation key
            salt = hashlib.sha1(str(random.random())).hexdigest()[:5]
            usernamesalt = datas['username']
            if isinstance(usernamesalt, unicode):
                usernamesalt = usernamesalt.encode('utf8')
            datas['activation_key']= hashlib.sha1(salt+usernamesalt).hexdigest()

            datas['email_path']="/ActivationEmail.txt"
            datas['email_subject']="Activation de votre compte yourdomain"

            form.sendEmail(datas)
            form.save(datas) #Save the user and his profile

            request.session['registered']=True #For display purposes
            return redirect(home)
        else:
            registration_form = form #Display form with error messages (incorrect fields, etc)
    return render(request, 'siteApp/register.html', locals())

激活视图:
#View called from activation email. Activate user if link didn't expire (48h default), or offer to
#send a second link if the first expired.
def activation(request, key):
    activation_expired = False
    already_active = False
    profile = get_object_or_404(Profile, activation_key=key)
    if profile.user.is_active == False:
        if timezone.now() > profile.key_expires:
            activation_expired = True #Display: offer the user to send a new activation link
            id_user = profile.user.id
        else: #Activation successful
            profile.user.is_active = True
            profile.user.save()

    #If user is already active, simply display error message
    else:
        already_active = True #Display : error message
    return render(request, 'siteApp/activation.html', locals())

def new_activation_link(request, user_id):
    form = RegistrationForm()
    datas={}
    user = User.objects.get(id=user_id)
    if user is not None and not user.is_active:
        datas['username']=user.username
        datas['email']=user.email
        datas['email_path']="/ResendEmail.txt"
        datas['email_subject']="Nouveau lien d'activation yourdomain"

        salt = hashlib.sha1(str(random.random())).hexdigest()[:5]
        usernamesalt = datas['username']
        if isinstance(usernamesalt, unicode):
            usernamesalt = usernamesalt.encode('utf8')
        datas['activation_key']= hashlib.sha1(salt+usernamesalt).hexdigest()

        profile = Profile.objects.get(user=user)
        profile.activation_key = datas['activation_key']
        profile.key_expires = datetime.datetime.strftime(datetime.datetime.now() + datetime.timedelta(days=2), "%Y-%m-%d %H:%M:%S")
        profile.save()

        form.sendEmail(datas)
        request.session['new_link']=True #Display: new link sent

    return redirect(home)

最后,在urls.py文件中:
url(r'^register/$', 'register'),
url(r'^activate/(?P<key>.+)$', 'activation'),
url(r'^new-activation-link/(?P<user_id>\d+)/$', 'new_activation_link'),

有了这些,你就有了一些开端,在txt邮件和HTML中使用正确的模板标签,它应该能正常工作。

注意:此代码不完美,存在重复(例如,随机密钥的生成可以在一个函数中定义),但它能完成工作。此外:激活密钥并不是使用适当的加密函数生成的。一个替代方法是使用以下函数来生成密钥:

from django.utils.crypto import get_random_string

def generate_activation_key(username):
    chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)'
    secret_key = get_random_string(20, chars)
    return hashlib.sha256((secret_key + username).encode('utf-8')).hexdigest()

NB2: Django的send_mail没有提供任何工具来验证您的电子邮件。如果您想要验证您的电子邮件(DKIM,SPF),我建议您查看这个链接:https://djangosnippets.org/snippets/1995/ NB3: 视图new_activation_link存在一个安全问题:它应该检查请求重新发送的用户是否正确,并且也应该检查该用户是否已经通过身份验证。请您对此进行修正。

@Raphael Laurent,那么使用令牌应该如何进行“登录”?上面的答案是执行“save()”,而不是登录。 - LKM
3
警告:该答案使用的生成激活密钥的方式不安全,仅依赖于“random”模块。获取安全值的简单方法是使用django.utils.crypto中的get_random_string(在使用startproject时提供默认的“ SECRET_KEY”)或者如果使用Python3.6以上版本,则使用新的“secrets”模块。否则,请使用os.urandom获取真正的随机数据,而不是random.random - Bakuriu
@raphael-laurent 我可以使用uuid4()来生成随机密钥吗? - pavitran
如果您想避免在电子邮件中硬编码激活链接,您可以在views.py文件中执行reg_data['activation_url'] = request.build_absolute_uri('/activate/')。这个答案是在2014年写的/2017年编辑的,但对我设置电子邮件确认非常有帮助!感谢@Raphael和@Bakuriu。 - Tom

4
您可能也对简单但强大的django-verified-email-field感兴趣。
只需在您的表单中使用VerifiedEmailField
from django import forms
from verified_email_field.forms import VerifiedEmailField

class RegistrationForm(forms.ModelForm):
    email = VerifiedEmailField(label='email', required=True)

或者在您的模型中:

from django.db import models
from verified_email_field.models import VerifiedEmailField

class User(models.Model):
    email = VerifiedEmailField('e-mail')

它呈现了两个输入字段:电子邮件和验证码。如果给定的电子邮件没有有效的验证码,将使用AJAX或在字段的清除期间通过电子邮件地址发送验证码,因此即使没有JavaScript也可以工作。

1
你能简要解释一下吗? - user 98
1
我已经简要地解释过了。除非你有特定的问题,否则我不确定应该深入解释哪个部分。 - Jakub QB Dorňák
这是一个文档非常不完善的库。 - nassim
这不是一个库,只是一个单一的(虽然功能强大的)模型/表单字段。 (当你买锤子时,你期望什么样的文档呢?我想重量和木材种类就足够了。) 如果您觉得缺少重要信息,欢迎扩展文档。或者您可以提出一些合理的问题,我会回答并可能更新README。 - Jakub QB Dorňák

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接