Flask使用itsdangerous生成令牌
itsdangerous示例
(venv)$python manage.py shell
>>> from manage import app
>>> from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
>>> s = Serializer(app.config['SECRET_KEY'], expires_in = 3600)
>>> token = s.dumps({ 'confirm': 23 })
>>> token
'eyJhbGciOiJIUzI1NiIsImV4cCI6MTM4MTcxODU1OCwiaWF0IjoxMzgxNzE0OTU4fQ.ey ...'
>>> data = s.loads(token)
>>> data
{u'confirm':23}
itsdangerous 提供了多种生成令牌的方法,其中
TimedJSONWebSignatureSerializer
类生成具有过期时间的 JSON Web 签名(JSON Web Signatures,JWS)。这个类的构造函数接收的参数是一个密钥,在 Flask 程序中可使用 SECRET_KEY 设置。dumps() 方法为指定的数据生成一个加密签名,然后再对数据和签名进行序列化,生成令牌字符串。expires_in 参数设置令牌的过期时间,单位为秒。
为了解码令牌,序列化对象提供了 loads() 方法,其唯一的参数是令牌字符串。这个方法
会检验签名和过期时间,如果通过,返回原始数据。如果提供给 loads() 方法的令牌不正
确或过期了,则抛出异常。
实际项目使用流程
本例场景为用户邮箱认证功能。
-
添加到用户模型类(
models.py
)from itsdangerous import TimedJSONWebSignatureSerializer as Serializer from flask import current_app from . import db class User(UserMixin, db.Model): # ... confirmed = db.Column(db.Boolean, default=False) # 判断用户是否认证,默认False # 生成token令牌 def generate_confirmation_token(self, expiration=3600): # 过期时间 s = Serializer(current_app.config['SECRET_KEY'], expiration) # 序列化秘钥 return s.dumps({'confirm': self.id}) # 验证token令牌 def confirm(self, token): s = Serializer(current_app.config['SECRET_KEY']) # 序列化秘钥 try: data = s.loads(token) # 验证失败或令牌超时,该方法抛出异常 except: return False if data.get('confirm') != self.id: # 验证用户id,以防令牌生成方法泄露 return False self.confirmed = True # 验证通过 db.session.add(self) # 保存该用户 return True
-
发送令牌认证邮件(
views.py
)from ..email import send_email # 邮箱验证方法 @auth.route('/register', methods = ['GET', 'POST']) def register(): form = RegistrationForm() if form.validate_on_submit(): # ... db.session.add(user) db.session.commit() token = user.generate_confirmation_token() # 获取认证令牌 send_email(user.email, 'Confirm Your Account', 'auth/email/confirm', user=user, token=token) # 发送邮箱,配置模板参数:user、token flash('A confirmation email has been sent to you by email.') return redirect(url_for('main.index')) return render_template('auth/register.html', form=form) # 防止邮件丢失,再次发送认证邮件,此时用户为current_user(已登录的用户,即目标用户)。 from flask_login import current_user @auth.route('/confirm') @login_required def resend_confirmation(): token = current_user.generate_confirmation_token() send_email(current_user.email, 'Confirm Your Account', 'auth/email/confirm', user=current_user, token=token) flash('A new confirmation email has been sent to you by email.') return redirect(url_for('main.index'))
-
用户认证后,验证令牌(
views.py
)from flask.ext.login import current_user @auth.route('/confirm/<token>') @login_required # falsk-login扩展方法,必须先登录,才能调用该方法。 def confirm(token): if current_user.confirmed: # 判断用户是否已经认证 return redirect(url_for('main.index')) if current_user.confirm(token): # 用户请求该路由,调用验证函数进行验证。 flash('You have confirmed your account. Thanks!') # 成功 else: flash('The confirmation link is invalid or has expired.') # 失败 return redirect(url_for('main.index'))
- Flask-Login 提供的
login_required
修饰器会保护这个路由,因此,用户点击确认邮件中的链接后,要先登录,然后才能执行这个视图函数。 - 这个函数先检查已登录的用户是否已经确认过,如果确认过,则重定向到首页,因为很显然此时不用做什么操作。这样处理可以避免用户不小心多次点击确认令牌带来额外工作。
- 由于令牌确认完全在 User 模型中完成,所以视图函数只需调用
confirm()
方法即可,然后再根据确认结果显示不同的 Flash 消息。确认成功后,User 模型中confirmed
属性的值会被修改并添加到会话中,请求处理完后,这两个操作被提交到数据库。
- Flask-Login 提供的
-
当允许未确认的用户登录,但只显示一个页面,这个页面要求用户在获取权限之前先确认账户时,可使用 Flask 提供的 before_request 钩子完成:(
before_request 钩子只能应用到属于该路由的请求上。若想使用针对程序全局请求的钩子,必须使用 before_app_request 装饰器
):@auth.before_app_request def before_request(): if current_user.is_authenticated() \ and not current_user.confirmed \ and request.endpoint[:5] != 'auth.' \ and request.endpoint != 'static': return redirect(url_for('auth.unconfirmed')) @auth.route('/unconfirmed') def unconfirmed(): if current_user.is_anonymous() or current_user.confirmed: return redirect(url_for('main.index')) return render_template('auth/unconfirmed.html')
同时满足以下 3 个条件时,
before_app_request
处理程序会拦截请求,并且会被重定向到 /auth/unconfirmed 路由,显示一个确认账户相关信息的页面,比如提示绑定手机等。- 用户已登录(current_user.is_authenticated() 必须返回 True)。
- 用户的账户还未确认。
- 请求的端点不在认证路由中。(使用 request.endpoint 获取,访问认证路由要获取权限,因为这些路由的作用是让用户确认账户或执行其他账户管理操作。)