前言
最近又要用到django去搭建网站,之前用的东西需要花一些时间去恢复记忆,同时还需要学习一些新的知识,进一步提升自己使用django的能力,因此写这篇博客持续更新使用过程中积累的一些技巧,知识和用法等。
目前我自己写了一个认证系统,包括登入登出,注册激活,密码找回等功能,放在了github仓库中,有使用django-registration-redux不适合的,或者需要个性化定制认证功能的,可以考虑使用和优化这个认证系统。以下的代码大部分来自于该项目。
发送邮件设置
要使用django发送邮件,事先需要做一些设置,这里我以腾讯企业邮箱为例。
- 打开邮箱的smtp发送邮件服务,一般在设置中的客户端设置中
- 然后在yourapp/settings中设置如下内容
EMAIL_HOST = 'smtp.exmail.qq.com'
EMAIL_PORT = '465'
EMAIL_HOST_USER = 'xxx@xxx.com'
EMAIL_HOST_PASSWORD = 'xxxx'
DEFAULT_FROM_EMAIL = 'xxx@xxx.com'
EMAIL_USE_SSL = True
- 之后使用django.core.mail中的send_mail发送邮件即可
from django.core.mail import send_mail
send_mail(subject,message,from_email,recipient_list)
#subject:发送主题
#message:发送内容
#from_email:对应settings中的DEFAULT_FROM_EMAIL
#recipient_list 发送人列表,例如[a@xx.com, b@xx.com]
保存用户密码
实际上,无论是django.forms.Form,还是django.forms.ModelForm,都仅仅是发挥表单提交和表单验证的功能,用户的密码在存到数据库的时候是不会加密的,因此,需要手动加密,一般的方法,这里引用django基础教程中的方法,先保存form模型到数据库,然后再根据用户名提取到user对象,之后再使用user.set_password()的方法给密码加密,例子如下:
if request.method == 'POST':
form = Registration_Form(request.POST)
if form.is_valid():
userform = form.save(commit=False)
userform.is_active = False #初始用户未激活
userform.save()
username = form.cleaned_data['username']
email = form.cleaned_data['email']
user = User.objects.get(username=username)
user.set_password(user.password) #form保存时不会自动加密,因此这里需要手动加密
user.userauth = UserAuth(user=user)
user.save()
user.userauth.send_activation_email(request,purpose='registration')
return render(request,'user_auth/registration_complete.html',{'username':username,
'email':email})
else:
print(form.errors)
return render(request,'user_auth/registration.html',{'form':form})
form = Registration_Form()
return render(request,'user_auth/registration.html',{'form':form})
自定义form表单验证
要对表单的一些字段或者内容进行自定义验证的话,可以通过在form 类中添加clean()和clean_yourfield()方法来增加验证内容,如下的例子,自定义方法验证邮箱的唯一性和两次输入的密码是否相同的form类,需要注意的是django默认的User中邮箱是可以重复的,因此我们需要自己去检查邮箱的唯一性。另外,除去User模型中有的字段,我们可以额外添加一些字段或者重写其中的字段,而Meta类中的field是有顺序的,如果你在模板中使用for循环来遍历字段的话,它将决定最终字段的显示的顺序。
class Registration_Form(forms.ModelForm):
username = forms.CharField(max_length=64,label='用户名',
widget=forms.TextInput(attrs={'placeholder':'用户名'}),
# error_messages={'required':'不能为空','max_length':'要求64个字符以内'}
)
confirm_password = forms.CharField(label='重新输入密码',max_length=64,widget=forms.PasswordInput(attrs={'class':'form-control',
'placeholder':'重复输入密码',
}))
password = forms.CharField(label='登陆密码',max_length=64,widget=forms.PasswordInput(attrs={'class':'form-control',
'placeholder':'登陆密码',
}))
email = forms.EmailField(label='用户邮箱',widget=forms.EmailInput(attrs={'placeholder':'用户邮箱'}))
class Meta:
model = User
fields = ['username','password','confirm_password','email'] #添加confirm_password以调整顺序
def clean(self):
if self.cleaned_data['password'] != self.cleaned_data['confirm_password']:
raise forms.ValidationError('两次密码输入不一致,请检查!',code='password error')
return self.cleaned_data
def clean_email(self): #函数名是严格限制的 clean+下划线+字段
"""
用于验证邮箱的唯一性
"""
if User.objects.filter(email__exact=self.cleaned_data['email']):
raise forms.ValidationError('该Email已经注册过了,请检查!',code='invalid email')
return self.cleaned_data['email']
可以看到,这里还使用了forms.ValidationError来自定义错误说明,它们最终将被添加到form.errors中。
这里特别说明的是在ModelForm中的字段,最终是先验证ModelForm中的字段,然后再验证自定义的字段,因此如果我这里想写一个clean_password(self)的方法来使用self.cleaned_data[‘confirm_password’]就会KeyError的的错误,因此最好的额办法是写一个clean_confirm_password(self)的方法中引用self.cleaned_data[‘password’],因为此时的password的key已经存在了!
模板中有多个参数的url写法
{% url 'user_auth:registration_resend_email' username email %}
这里的username和email都是即将传递给名为registration_resend_email的URL的参数,最终找到的URL如下:
'registration/resend_email/<username>/<email>/'
激活码的生成
通过查看django-registration-redux的源码,学习到了如何将用户激活的功能集成到User的关联模型中,我们只需要创建一个与User 有OneToOneField()的Userauth模型,然后在该模型下写认证功能,可以直接引用self.user和self.save()以及self.yourfield来构建激活码的生成及验证,以下是代码:
from datetime import datetime,timedelta
import hashlib
import string
import os
from django.db import models
from django.contrib.auth.models import User
from django.core.mail import send_mail
from django.utils.crypto import get_random_string
from django.conf import settings
class UserAuth(models.Model):
user = models.OneToOneField(User,on_delete=models.CASCADE)
activation_time = models.DateTimeField(default=datetime.now())
activation_key = models.CharField(max_length=64,blank=True)
activation_valid = models.BooleanField(default=False)
def __str__(self):
return self.user
def generate_activation_key(self,save=True):
"""
生成64位随机激活码
"""
random_string = get_random_string(length=32,allowed_chars=string.printable)
self.activation_key = hashlib.sha256(random_string.encode('utf-8')).hexdigest()
if save:
self.save()
return self.activation_key
def send_activation_email(self,request,purpose='reset_password'):
"""
用于给用户发送注册确认邮件或者密码重置激活邮件
purpose = [ reset_password,registration ]
这里特别需要注意的是activation_url的构建
purpose的设置是以urls.py中设置的路径关联的
因为在urls.py中设置了激活URL为:
resent_password/activation/(P<activation_key>[\w-]+)
registration/activation/(P<activation_key>[\w-]+)
因此,这里我拼了一个这样的激活路径,如果后续修改了url,那么
这里的activation_url也必须要修改以适应新的激活URL
"""
activation_key = self.generate_activation_key(save=True)
activation_url = 'http://' + "/".join([request.META['HTTP_HOST'],
'accounts',purpose,'activation',activation_key])
if purpose == 'registration':
subject = getattr(settings,'REGISTRATION_SUBJECT')
message = getattr(settings,'REGISTRATION_MESSAGE')
elif purpose == 'reset_password':
subject = getattr(settings,'RESET_PASSWORD_SUBJECT')
message = getattr(settings,'RESET_PASSWORD_MESSAGE')
if subject and message:
message = message.format(
username=self.user.username,
activation_url=activation_url,
sender=getattr(settings,'EMAIL_HOST_USER'),
time=datetime.now())
from_email = getattr(settings,'DEFAULT_FROM_EMAIL')
recipient_list = [ self.user.email ]
send_mail(subject,message,from_email,recipient_list)
#保存发送激活码时间
self.activation_time = datetime.now()
self.activation_valid = True
self.save()
return True
def confirm_activation_key(self):
"""
确认激活码是否有效且验证时间未过期
这里特别需要说明的是activation_valid值
它主要是用来保证用户激活及密码重置以后
原来的激活或者密码重置界面将无法使用
默认情况下是false
"""
expired_days = getattr(settings,'EXPIRED_DAYS') or 1
is_not_expired = self.activation_time + timedelta(expired_days) > datetime.now()
is_valid = self.activation_valid
return is_valid and is_not_expired