使用加密验证
注册后在发送给用户的邮件中添加验证链接,最简单链接就是http://www.xxxx.com/users/comfirm/<id>
这种形式的URL,其中id为数据库给注册的用户分配的id。用户点击访问这个链接后,对应的视图函数会确认收到用户的id,然后用户的状态更新为已验证。但这种方法并不安全,只要得知链接格式,就能对任意id的用户进行验证。解决的办法时将URL中id换成与之对应的令牌
。
这里使用itsdangerous
包来生成身份验证令牌。Flask就使用了这个包来生成加密签名来保护用户会话cookie。
itsdangerous加密令牌
from itsdangerous import URLSafeTimedSerializer as usts
import base64
from django_ulysses import settings as django_settings
class Token:
def __init__(self, security_key):
self.security_key = security_key
self.salt = base64.encodebytes(security_key.encode())
def generate_validate_token(self, username):
serializer = usts(self.security_key)
return serializer.dumps(username, self.salt)
def confirm_validate_token(self, token, expiration=3600):
serializer = usts(self.security_key)
return serializer.loads(token, salt=self.salt, max_age=expiration)
def remove_validate_token(self, token):
"""验证不通过删除用户"""
serializer = usts(self.security_key)
print(serializer.loads(token, salt=self.salt))
return serializer.loads(token, salt=self.salt)
token_confirm =Token(django_settings.SECRET_KEY)
使用了itsdangerous
包下的URLSafeTimedSerializer
类产生和验证令牌,它会产生一个具有过期时间的签名,而且能安全地在URL中使用。指定django项目的SECRET_KEY
作为秘钥,并将其经过编码为字节码bytes后base64加密转换后作为salt。generate_validate_token
函数会在注册时,根据用户名生成一段令牌,令牌生成后会将带有token的验证链接发送到用户注册用的邮箱。confirm_validate_token
函数中,只有没有到令牌过期时间max_age
,它会返回token解密后的username
。而在remove_validate_token
,不考虑过期时间,只是将获取用户名,用于删除对应的用户。
验证用的视图函数和URL
验证用户视图函数confirm
def confirm(request, token):
try:
username = token_confirm.confirm_validate_token(token)
except:
username = token_confirm.remove_validate_token(token)
users = User.objects.filter(username=username).all()
for user in users:
user.delete()
messages.add_message(request, DANGER, '验证码错误或已过期,请重新注册')
return HttpResponseRedirect(reverse('learning_logs:index'))
try:
user =User.objects.get(username=username)
except User.DoesNotExist:
messages.add_message(request, DANGER, '没有此用户,请重新注册')
return HttpResponseRedirect(reverse('learning_logs:index'))
user.is_active = True
user.save()
login(request, user)
messages.add_message(request, messages.SUCCESS, "验证成功,已登录")
return HttpResponseRedirect(reverse('learning_logs:index'))
验证页面不管是否通过都会重定向到主页。这里,验证不通过的用户会直接被删除,通过的用户会将is_active
属性设置为True,然后直接登录此用户。
>>> from users.confirm import token_confirm
>>>token=token_confirm.generate_validate_token('ulysses')
>>> token
'InVseXNzZXMi.DrmMjA.BqhabEN9p_tkOcOIHjZMUgaGcF0'
>>> token_1=token_confirm.generate_validate_token('Hanabi')
>>> token_1
'IkhhbmFiaSI.DrmNOw.Wj_-I3drUh6yWoT-13NksPapfqg'
可见,生成的token由3部分组成,每部分有可能由字母、数字或者下划线连接符_-组成。因此URL格式:
re_path(r"^confirm/(?P<token>\w+.[-_\w]*\w+.[-_\w]*\w+)/$", confirm, name='confirm')
验证邮件
修改注册函数,在注册时使用用户名生成信息令牌,在邮件正文中添加这段验证地址
register:
if form.is_valid():
new_user = form.save(commit=False)
# 默认创建时is_active 是True
new_user.is_active = False
new_user.save()
token = token_confirm.generate_validate_token(new_user.username)
send_register_email.delay(new_user, token)
messages.add_message(request, messages.INFO, '验证邮件已发送,请查收')
获取完整的URL地址
要在邮件正文中添加验证地址链接,需要获取其完整的绝对地址,可以使用Django的site
框架。Django带有一个可选的“站点”框架。 它是将对象和功能与特定网站相关联的钩子。
Django的sites框架基于django.contrib.sites.models.Site
类,它会保存网址的2个属性:
domain
域名:它是网站的完整域,如:www.example.com。name
:网站的详细名称,用于人为区分使用。
为了使用site框架,需要:
- 添加
django.contrib.sites
应用到INSTALLED_APPS
; - 在
setting
设置一个SITE_ID
,如SITE_ID = 1; - 运行数据迁移;
在数据库中可以看到django-site
这张表,
而上文所说要指定的SITE_ID
就是指使用的数据库中那一条数据。更多用法见site
framework
使用site对象的get_current()
方法获取当前的网站,再使用拼接的方式形成完整的URL地址。
from django.core.mail import send_mail,send_mass_mail, EmailMultiAlternatives
from django.template import loader, Template
from celery import task
from django.conf import settings
from django.contrib.sites.models import Site
from django.urls import reverse
import os
@task
def send_register_email(user, token):
subject, from_email, to = "[Django]验证用户", os.environ.get('EMAIL_HOST_USER', "2276777056@qq.com"), \
user.email
site = Site.objects.get_current()
url = '{protocol}://{domain}{path}'.format(
protocol=getattr(settings, 'ABSOLUTEURI_PROTOCOL', 'http'),
domain=site.domain,
path= reverse('users:confirm', args=[token])
)
text_content = f"{user.username},你好\
欢迎来到 Ulysses\
为了验证您的账户,请点击以下链接进行验证\
链接:{url}\
Ulysses\
请勿回复此邮件"
html_template = loader.get_template('email/confirm.html')
context = {'user':user, 'url':url}
html_context = html_template.render(context)
email = EmailMultiAlternatives(subject=subject, body=text_content, from_email=from_email, to=[to])
email.attach_alternative(html_context, 'text/html')
# email.attach_alternative()
# 添加附件
# email.attach_file('users/templates/email/confirm.html', 'text/plain')
# email.attach_file('users/templates/email/confirm.txt', 'text/html')
email.send()
可以使用已经定义好格式的html模板,导入模板,使用context = {'user':user, 'url':url}
来填充、渲染,并将其作为邮件的html正文。
confirm.html:
<p>{{ user.username }}, 你好</p>
<p>欢迎来到 <b>Django Ulysses</b>!</p>
<p>为了验证您的账户,请<a href="{{ url }}">点击</a>进行验证</p>
<p>或者,您可以在浏览器被输入以下内容:</p>
<p>链接:{{ url }}</p>
<p>Ulysses</p>
<p><small>请勿回复此邮件</small></p>
收到的验证邮件
点击进行验证: