Flask教程(14)--flask-jwt-extended

1,827 阅读5分钟

软硬件环境

  • windows 10 64bit
  • anaconda3 with python 3.7
  • pycharm 2020.1.2
  • flask 1.1.2
  • flask-jwt-extended 3.24.1

flask-jwt-extended

前言

web开发中,ClientServer的交互都是通过HTTP协议发送请求和接收响应,但是因为HTTP协议是无状态的(stateless),也就是说ClientServer都不会记得先前的状态,Client每次发送request都会被视为是独立的,Server无法确定Client是否已经发送过认证请求。

本文分享基于Token令牌的认证。在flask中,使用的扩展是flask-jwt-extended

什么是JWT

JWT的原名是JSON Web Token,它是一种协定,就是把JSON结构的信息进行加密后变成Token传递给Client,然后客户端通过这个Token来与服务器进行交互。简单来说就是:使用者在登录或是验证过身份后,后端会在返回请求中附上JWT Token,未来使用者发送Request时携带此Token,就表示通过验证,而沒有携带JWT Token的使用者就会被拒绝访问,需要重新登录或重新验证身份。

安装扩展

flask-jwt-extendedJWT的一个实现,有了它,使得我们在开发基于flask框架的web应用时能够更加方便地实现基于Token的认证过程。首先需要安装扩展

pip install flask-jwt-extended

完整代码示例

这次示例,我们会用上之前介绍flask-sqlalchemyflask-corsflask-restful等扩展,编写一个相对完整的前后端分离的web后端系统,它具备如下功能

  • 可以实现用户登录
  • 用户登录信息的数据库存储
  • 基于Token的前后端交互、RESTful API
  • 跨域访问

先来看看整个项目的文件目录结构

flask-jwt-extended

首先我们准备下数据库,使用的是开源数据库mysql,创建数据库flask

flask-jwt-extended

通过scripts目录下的dbInitialize.py脚本文件创建初始数据库表并插入一条数据,用户名是admin@gmail.com,密码是字符串123456经过sha256加密后的数据,默认用户是激活状态

user表的结构是这样的

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    username = db.Column(db.String(45), nullable=False, unique=True)
    password = db.Column(db.String(128), nullable=False)
    active = db.Column(db.Boolean, default=True, nullable=False)

    def __init__(self, username=None, password=None, active=True):
        self.username = username
        self.password = password
        self.active = True

flask-jwt-extended

重点来看看用户登录部分的后端实现,还是RESTful API,这里提供一个POST方法,接收客户端发送过来的JSON数据,解析后得到用户名及加密后的密码,如果用户名存在于我们的数据库中且密码相符,调用flask_jwt_extendedcreate_access_token方法生成对应的token,注意到create_access_token的参数部分,我们传递的是usernameflask_jwt_extended还提供了方法get_jwt_identity,可以从token中获取到username,这点在实际项目中非常有用。

class Login(Resource):
    def __init__(self, **kwargs):
        self.logger = kwargs.get('logger')

    def post(self):
        code = None
        message = None
        token = None
        userid = None

        args = reqparse.RequestParser() \
            .add_argument('username', type=str, location='json', required=True, help="用户名不能为空") \
            .add_argument("password", type=str, location='json', required=True, help="密码不能为空") \
            .parse_args()

        flag_user_exist, flag_password_correct, user = User.authenticate(args['username'], args['password'])
        if not flag_user_exist:
            code = 201
            message = "user not exist"
        elif not flag_password_correct:
            code = 202
            message = "wrong password"
        else:
            code = 200
            message = "success"
            token = create_access_token(identity=user.username)
            userid = user.id

        return jsonify({
            "code": code,
            "message": message,
            "token": token,
            "userid": userid
        })

我们通过postman来模拟客户端的行为

flask-jwt-extended

可以看到,postman拿到了服务器发送过来的token值,保存这个值,后面的所有接口都需要带上这个token。接下来看看获取所有用户信息的接口

class Users(Resource):
    def __init__(self, **kwargs):
        self.logger = kwargs.get('logger')

    @jwt_required
    def get(self):
        users_list = []
        users = User.get_users()

        for user in users:
            users_list.append({"userid": user.id, "username": user.username})

        return jsonify({
            "code": 200,
            "message": "success",
            "users": users_list
        })

注意到上面的get方法有个装饰器@jwt_required,意思就是说这个接口是需要验证token的,所以,客户端在调用这个接口的时候就需要带上token,否则会报Missing Authorization Header的错误

flask-jwt-extended

正确的做法是这样的,在Headers添加字段

"Authorization: Bearer $ACCESS_TOKEN"

这里面的Bearertoken的一种类型,还有另一个类型是Mac Token,这是固定写法

flask-jwt-extended

定制Token过期的返回信息

flask-jwt-extended`在`token`过期后,有自己默认的出错信息,如果不满意,可以自己定制出错信息,使用装饰器`@jwt.expired_token_loader
@jwt.expired_token_loader
def expired_token_callback():
    return jsonify({
        'code': 201,
        'message': "token expired"
    })

Signature has expired处理

在程序运行时,无意中出现了Signature has expired的异常

File "/usr/local/lib/python3.5/site-packages/flask/app.py", line 1639, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/local/lib/python3.5/site-packages/flask/app.py", line 1625, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/usr/local/lib/python3.5/site-packages/flask_restful/__init__.py", line 477, in wrapper
resp = resource(*args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/flask/views.py", line 84, in view
return self.dispatch_request(*args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/flask_restful/__init__.py", line 587, in dispatch_request
resp = meth(*args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/flask_jwt_extended/utils.py", line 222, in wrapper
jwt_data = _decode_jwt_from_request(type='access')
File "/usr/local/lib/python3.5/site-packages/flask_jwt_extended/utils.py", line 204, in _decode_jwt_from_request
return _decode_jwt_from_headers()
File "/usr/local/lib/python3.5/site-packages/flask_jwt_extended/utils.py", line 176, in _decode_jwt_from_headers
return _decode_jwt(token, secret, algorithm)
File "/usr/local/lib/python3.5/site-packages/flask_jwt_extended/utils.py", line 136, in _decode_jwt
data = jwt.decode(token, secret, algorithm=algorithm)
File "/usr/local/lib/python3.5/site-packages/jwt/api_jwt.py", line 75, in decode
self._validate_claims(payload, merged_options, **kwargs)
File "/usr/local/lib/python3.5/site-packages/jwt/api_jwt.py", line 104, in _validate_claims
self._validate_exp(payload, now, leeway)
File "/usr/local/lib/python3.5/site-packages/jwt/api_jwt.py", line 149, in _validate_exp
raise ExpiredSignatureError('Signature has expired')
jwt.exceptions.ExpiredSignatureError: Signature has expired

google一番,看到官方的issue里有讨论这个问题,结论是在flask-jwt-extended配置中添加PROPAGATE_EXCEPTIONS = True,有兴趣的话,请查看参考资料里的链接。关于工程的所有配置信息,便于统一管理,我们集中在app/config.py中书写

import os


class Config:
    # flask
    DEBUG = os.environ.get('FLASK_DEBUG') or True

    # database
    SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI') or 'mysql+pymysql://root:toor@localhost/test'
    SQLALCHEMY_TRACK_MODIFICATIONS = True

    # jwt
    JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') or 'jwt-key'
    JWT_COOKIE_CSRF_PROTECT = True
    JWT_CSRF_CHECK_FORM = True
    JWT_ACCESS_TOKEN_EXPIRES = os.environ.get('JWT_ACCESS_TOKEN_EXPIRES') or 3600
    PROPAGATE_EXCEPTIONS = True

在生产环境中,可以通过对应的环境变量来获取配置,能够比较方便地区分调试和生产环境