flask-jwt-extended文档学习

官方文档

  1. 基础的用法:如何使用token保护一个endpoint
  2. 可选的路由保护:对一个被保护的endpoint区分有token和没有token的用户
  3. 访问令牌中的存储数据:在令牌中存储额外的信息进行权限的管理
  4. 根据Python Object生成令牌:就是3中存储的信息在数据库存储着该怎么办
  5. 根据令牌获取Python Object:在endpoint函数中从current_user中获得用户信息
  6. 自定义装饰器:对用户的身份以及权限等进行更多的验证,处理不同级别用户访问不同的被保护的endpoint的权限

1. 基础的用法

1
2
3
4
最基础的用法不需要很多的调用,只需要使用三个函数:
1. create_access_token()用来创建令牌
2. get_jwt_identity()用来根据令牌取得之前的identity信息
3. jwt_required()这是一个装饰器,用来保护flask节点

官方的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import json
from flask import Flask, jsonify, request
from flask_jwt_extended import (
    JWTManager, jwt_required, create_access_token,
    get_jwt_identity
)

app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'super-secret'  
jwt = JWTManager(app)


@app.route('/login', methods=['POST'])
def login():
    if not request.is_json:
        return jsonify({"msg": "Missing JSON in request"}), 400
    # 这部分需要看下POST请求的格式
    data = request.get_data()
    data = json.loads(data)
    username = data.get('username', None)
    password = data.get('password', None)
    if not username:
        return jsonify({"msg": "Missing username parameter"}), 400
    if not password:
        return jsonify({"msg": "Missing password parameter"}), 400

    if username != 'test' or password != 'test':
        return jsonify({"msg": "Bad username or password"}), 401    
    access_token = create_access_token(identity=username)
    return jsonify(access_token=access_token), 200


@app.route('/protected', methods=['GET'])
@jwt_required
def protected():    
    current_user = get_jwt_identity()
    return jsonify(logged_in_as=current_user), 200


if __name__ == '__main__':
    app.run()

在这里插入图片描述

访问的结果
这里尝试了使用同一个账户信息再次请求login,发现新获取的令牌和旧的令牌均可以访问protected节点,感觉这个还是挺好用的,就是可能会导致多个令牌都有效还有就是用户退出登录会稍微麻烦些。

2. 可选的路由保护

1
2
3
4
5
6
7
8
9
10
11
12
13
# 对于一个路由节点,授权和未授权的均可以访问,但会使用不同的功能,
# 这个时候就要使用jwt_optional()装饰器,
# 至于判断是否是有token的用户,可以根据get_jwt_identity()函数的返回值判断
@app.route('/partially-protected', methods=['GET'])
@jwt_optional
def partially_protected():
    # If no JWT is sent in with the request, get_jwt_identity()
    # will return None
    current_user = get_jwt_identity()
    if current_user:
        return jsonify(logged_in_as=current_user), 200
    else:
        return jsonify(logged_in_as='anonymous user'), 200

3. 访问令牌中存储数据

1
2
3
4
除去存放基本的用户的标识identity外,在access_token中还可能存放其他的信息,
1. user_claims_loader()用于将信息存储到access_token中,例子中的注释提到
该函数在create_access_token()函数被调用后使用,参数是创建令牌的参数identity
2. get_jwt_claims()用于在被包含的节点内获取access_token的信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 该函数在creat_access_token()被调用后使用
@jwt.user_claims_loader
def add_claims_to_access_token(identity):
    return {
        'hello': identity,
        'foo': ['bar', 'baz']
    }

# In a protected view, get the claims you added to the jwt with the
# get_jwt_claims() method
@app.route('/protected', methods=['GET'])
@jwt_required
def protected():
    claims = get_jwt_claims()
    return jsonify({
        'hello_is': claims['hello'],
        'foo_is': claims['foo']
    }), 200

4. 根据Python对象生成令牌

  • (感觉就是说参数可以传任何对象,不限于str类型)一般来说,用户信息会存储在数据库中,如果现在想用username作为token的identity,并且添加额外的权限信息。如果用3中的user_claims_loader,直接传递username必定会导致在数据库再一次的查询操作。一次在login的路由节点,一次在user_claims_loader装饰的函数内部。
  • 扩展提供了向create_access_token()函数传递object对象的能力,然后根据3中的规则,之后会传递给user_claims_loader()装饰器。这样的话就只需要在login的路由节点查询一次参数就可以,但是需要让 扩展知道 identity的信息(不能是一个对象),需要使用user_identity_loader()函数,它可以接受create_access_token()函数获取的所有参数,并返回一个可以json序列化的identity(get_jwt_identity()会返回这个值)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from flask import Flask, jsonify, request
from flask_jwt_extended import (
    JWTManager, jwt_required, create_access_token,
    get_jwt_identity, get_jwt_claims
)

app = Flask(__name__)

app.config['JWT_SECRET_KEY'] = 'super-secret'  # Change this!
jwt = JWTManager(app)


# Python对象,可以是一个SQLAlchemy的用来ORM的对象
class UserObject:
    def __init__(self, username, roles):
        self.username = username
        self.roles = roles


# Create a function that will be called whenever create_access_token
# 在create_access_token()之后调用, 并获取前者的参数,
# 决定get_jwt_claims()函数的返回值
@jwt.user_claims_loader
def add_claims_to_access_token(user):
    return {'roles': user.roles}


# 在create_access_token调用之后使用,获取相关的所有参数,返回的是每个token的标识符
@jwt.user_identity_loader
def user_identity_lookup(user):
    return user.username


@app.route('/login', methods=['POST'])
def login():
    username = request.json.get('username', None)
    password = request.json.get('password', None)
    if username != 'test' or password != 'test':
        return jsonify({"msg": "Bad username or password"}), 401

    # 可以是相关的Sqlalchemy的查询操作
    user = UserObject(username='test', roles=['foo', 'bar'])

    # 传递object之后,可以使用user_identity_loader获取token需要的identity
    # 在user_claims_loader存储其他信息
    access_token = create_access_token(identity=user)
    ret = {'access_token': access_token}
    return jsonify(ret), 200


@app.route('/protected', methods=['GET'])
@jwt_required
def protected():
    ret = {
        'current_identity': get_jwt_identity(),  # test
        'current_roles': get_jwt_claims()['roles']  # ['foo', 'bar']
    }
    return jsonify(ret), 200


if __name__ == '__main__':
    app.run()

5. 根据令牌获取python对象

  • 这里是根据Object获取token的一个反向的操作,主要是持有token访问被保护的节点的时候,自动通过token加载Python对象,可能是Sqlalchemy中的对象(通过user_loader_callback_loader()装饰器)。该Object可以通过get_current_user()或者是current_user这个局部代理直接在节点函数中获取。
  • 需要注意的是user_loader_callback_loader()如果访问数据库,可能会增加查找的开销,不管是否需要数据库中的信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 访问受保护的节点的时候会被调用,这个函数在token被验证后调用,可以使用get_jwt_claims()获取相关的信息,如果加载没有成功,规定需要返回None
@jwt.user_loader_callback_loader
def user_loader_callback(identity):
    if identity not in users_to_roles:
        return None

    return UserObject(
        username=identity,
        roles=users_to_roles[identity]
    )
 
#  上面函数返回None的错误处理,展示给客户端看的信息
@jwt.user_loader_error_loader
def custom_user_loader_error(identity):
    ret = {
        "msg": "User {} not found".format(identity)
    }
    return jsonify(ret), 404

# 如果user_loader_callback返回的是None,这个节点就不会执行函数,
# 此外就是可以通过current_user函数以及get_current_user()方法访问对象
@app.route('/admin-only', methods=['GET'])
@jwt_required
def protected():
    if 'admin' not in current_user.roles:
        return jsonify({"msg": "Forbidden"}), 403
    else:
        return jsonify({"msg": "don't forget to drink your ovaltine"})

6. 自定义装饰器

  • 这里主要是用自定义的装饰器来扩展jwt_extend的功能,官网的例子就是对用户身份以及权限都进行验证,此外就是提到的官方的一系列的Verify Tokens in Request的装饰器函数,用来进行默认的验证。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 这里注意下wraps的使用,涉及到闭包函数的使用,不用的话会导致fn这个函数的
# 属性都消失,具体的可以看下functools的wraps的用法
def admin_required(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        verify_jwt_in_request()
        claims = get_jwt_claims()
        if claims['roles'] != 'admin':
            return jsonify(msg='Admins only!'), 403
        else:
            return fn(*args, **kwargs)
    return wrapper


@jwt.user_claims_loader
def add_claims_to_access_token(identity):
    if identity == 'admin':
        return {'roles': 'admin'}
    else:
        return {'roles': 'peasant'}


@app.route('/login', methods=['POST'])
def login():
    username = request.json.get('username', None)
    access_token = create_access_token(username)
    return jsonify(access_token=access_token)


@app.route('/protected', methods=['GET'])
@admin_required
def protected():
    return jsonify(secret_message="go banana!")

if __name__ == '__main__':
    app.run()