欢迎访问我的网站,希望内容对您有用,感兴趣的可以加入我们的社群。

Flask教程(十八)flask-jwt-extended

Flask Web 迷途小书童 4年前 (2020-09-01) 9642次浏览 2个评论

软硬件环境

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

视频看这里

此处是 youtube 的播放链接,需要科学上网。喜欢我的视频,请记得订阅我的频道,打开旁边的小铃铛,点赞并分享,感谢您的支持。

前言

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-extendedtoken 过期后,有自己默认的出错信息,如果不满意,可以自己定制出错信息,使用装饰器 @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

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

源码下载

https://github.com/xugaoxiang/FlaskTutorial

Flask系列教程

更多 Flask 教程,请移步

https://xugaoxiang.com/category/python/flask/

参考资料

喜欢 (1)

您必须 登录 才能发表评论!

(2)个小伙伴在吐槽
  1. 按教程postman运行http://localhost:5000/users,出错TypeError: Object of type function is not JSON serializable
    匿名2021-08-31 13:52
    • 迷途小书童
      你好,新版本的flask-jwt-extended,已经改了很多的API了,可以参考这个 https://flask-jwt-extended.readthedocs.io/en/stable/v4_upgrade_guide/#api-changes