Flask使用itsdangerous生成令牌

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}
  1. itsdangerous 提供了多种生成令牌的方法,其中TimedJSONWebSignatureSerializer 类生成具有过期时间的 JSON Web 签名(JSON Web Signatures,JWS)。这个类的构造函数接收的参数是一个密钥,在 Flask 程序中可使用 SECRET_KEY 设置。

  2. dumps() 方法为指定的数据生成一个加密签名,然后再对数据和签名进行序列化,生成令牌字符串。expires_in 参数设置令牌的过期时间,单位为秒。

  3. 为了解码令牌,序列化对象提供了 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'))
    
    1. Flask-Login 提供的 login_required 修饰器会保护这个路由,因此,用户点击确认邮件中的链接后,要先登录,然后才能执行这个视图函数。
    2. 这个函数先检查已登录的用户是否已经确认过,如果确认过,则重定向到首页,因为很显然此时不用做什么操作。这样处理可以避免用户不小心多次点击确认令牌带来额外工作。
    3. 由于令牌确认完全在 User 模型中完成,所以视图函数只需调用 confirm() 方法即可,然后再根据确认结果显示不同的 Flash 消息。确认成功后,User 模型中confirmed属性的值会被修改并添加到会话中,请求处理完后,这两个操作被提交到数据库
  • 当允许未确认的用户登录,但只显示一个页面,这个页面要求用户在获取权限之前先确认账户时,可使用 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 路由,显示一个确认账户相关信息的页面,比如提示绑定手机等。

    1. 用户已登录(current_user.is_authenticated() 必须返回 True)。
    2. 用户的账户还未确认。
    3. 请求的端点不在认证路由中。(使用 request.endpoint 获取,访问认证路由要获取权限,因为这些路由的作用是让用户确认账户或执行其他账户管理操作。)
上一篇:python的项目框架搭建


下一篇:Windows 平台部署 Flask