详解flask-jwt插件验证机制

前言

jwt(JSON Web Tokens)是目前最流行的跨域身份验证解决方案。相比session它是无状态的,因此它非常适合json格式的api。flask中就有这样一个插件专门做jwt验证。

1.源码结构

flask-jwt的源码不长,仅有一个模块,首先来看看它的配置项。

配置项

current_identity = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_identity', None))

_jwt = LocalProxy(lambda: current_app.extensions['jwt'])

CONFIG_DEFAULTS = {
    'JWT_DEFAULT_REALM': 'Login Required',
    'JWT_AUTH_URL_RULE': '/auth',
    'JWT_AUTH_ENDPOINT': 'jwt',
    'JWT_AUTH_USERNAME_KEY': 'username',
    'JWT_AUTH_PASSWORD_KEY': 'password',
    'JWT_ALGORITHM': 'HS256',
    'JWT_LEEWAY': timedelta(seconds=10),
    'JWT_AUTH_HEADER_PREFIX': 'JWT',
    'JWT_EXPIRATION_DELTA': timedelta(seconds=300),
    'JWT_NOT_BEFORE_DELTA': timedelta(seconds=0),
    'JWT_VERIFY_CLAIMS': ['signature', 'exp', 'nbf', 'iat'],
    'JWT_REQUIRED_CLAIMS': ['exp', 'iat', 'nbf']
}

首先来看看current_identity_jwt这两个对象,首先它并不是普通的对象,而是代理对象LocalProxy。什么是代理对象,如果了解过flask机制的同学应该很清楚这个东西,不过不了解的也没关系,可以把它简单的理解成为原始对象的一个复制,但并不完全相同。知道了这些之后再来看看LocalProxy的参数,它接收一个无参且返回一个对象的函数。通过代理以后,我们就能使用这个对象的所有功能了。其中_jwt时JWT插件的核心对象代理,而current_identity这个对象到底是什么,顾名思义,它是当前线程用户对象的代理,具体的对象,下面的内容将会解释。

核心对象

class JWT(object):

    def __init__(self, app=None, authentication_handler=None, identity_handler=None):
        self.authentication_callback = authentication_handler
        self.identity_callback = identity_handler

        self.auth_response_callback = _default_auth_response_handler
        self.auth_request_callback = _default_auth_request_handler
        self.jwt_encode_callback = _default_jwt_encode_handler
        self.jwt_decode_callback = _default_jwt_decode_handler
        self.jwt_headers_callback = _default_jwt_headers_handler
        self.jwt_payload_callback = _default_jwt_payload_handler
        self.jwt_error_callback = _default_jwt_error_handler
        self.request_callback = _default_request_handler

        if app is not None:
            self.init_app(app)
            
        ...
复制代码

从对象的构造函数可看出除了authentication_handleridentity_handler其它都有默认的实现。对于每个callback对象中都有对应的装饰器来实现这些函数的自定义。

核心验证器

def jwt_required(realm=None):
    """View decorator that requires a valid JWT token to be present in the request

    :param realm: an optional realm
    """
    def wrapper(fn):
        @wraps(fn)
        def decorator(*args, **kwargs):
            _jwt_required(realm or current_app.config['JWT_DEFAULT_REALM'])
            return fn(*args, **kwargs)
        return decorator
    return wrapper
复制代码

核心验证器其实是一个装饰器它用来装饰flask视图函数来起到拦截非登录用户的请求。

2.源码分析

在分析源码前首先得了解插件的运行流程。

登录

api身份验证

明白了流程,源码分析起来就轻松了。

登录源码分析

首先是登录,先来看登录时调用的核心函数_default_auth_request_handler

def _default_auth_request_handler():
    data = request.get_json()
    username = data.get(current_app.config.get('JWT_AUTH_USERNAME_KEY'), None)
    password = data.get(current_app.config.get('JWT_AUTH_PASSWORD_KEY'), None)
    criterion = [username, password, len(data) == 2]

    if not all(criterion):
        raise JWTError('Bad Request', 'Invalid credentials')

    identity = _jwt.authentication_callback(username, password)

    if identity:
        access_token = _jwt.jwt_encode_callback(identity)
        return _jwt.auth_response_callback(access_token, identity)
    else:
        raise JWTError('Bad Request', 'Invalid credentials')
复制代码

这里提一点,flask-jwt的登录接口不需要开发者自己写对应的试图函数,因为他在init_app的时候已经注册了值为JWT_AUTH_ENDPOINT(在配置中可以自定义,默认为'/auth')的路由,来作为验证接口。

我们回到这个函数本身,请求上面说的验证接口需要在body中传一个包含账号密码json对象,其中账号密码的键名可以在配置文件中通过JWT_AUTH_USERNAME_KEYJWT_AUTH_PASSWORD_KEY来指定,默认为username和password。从body中获取了账号密码之后,就需要我们自定义的authentication_callback来验证信息是否正确了,这个函数可以在JWT对象初始化的时候作为参数传入,也可以通过@authentication_handler装饰器来传入。它需要接受username, password两个参数,并返回一个用户对象。从代码中可以看出验证成功后会生成一个token传入到auth_response_callback函数中通过它来生成一个json对象返回给前端.注意到token是由一个encode函数生成的我们来看看它的实现。

def _default_jwt_encode_handler(identity):
    secret = current_app.config['JWT_SECRET_KEY']
    algorithm = current_app.config['JWT_ALGORITHM']
    required_claims = current_app.config['JWT_REQUIRED_CLAIMS']

    payload = _jwt.jwt_payload_callback(identity)
    missing_claims = list(set(required_claims) - set(payload.keys()))

    if missing_claims:
        raise RuntimeError('Payload is missing required claims: %s' % ', '.join(missing_claims))

    headers = _jwt.jwt_headers_callback(identity)

    return jwt.encode(payload, secret, algorithm=algorithm, headers=headers)
复制代码

它的内部调用了python自带的JWT编码算法,输出一个可解码的编码,这里所编码的信息简单来讲是一个带有签发时间、到期时间以及用户账号信息的字典。编码解码需要同一个密钥也就是secret,这个默认是配置文件中的SECRET_KEY。这里这个编码就是上一步输出给前端的token。

到这里为止整个登录流程就结束了。

验证源码分析

验证这一块就要请出刚刚提到的jwt_required了。其实它只是一个装饰器,真正的验证函数是_jwt_required,我们来看看它的内部。

def _jwt_required(realm):
    """Does the actual work of verifying the JWT data in the current request.
    This is done automatically for you by `jwt_required()` but you could call it manually.
    Doing so would be useful in the context of optional JWT access in your APIs.

    :param realm: an optional realm
    """
    token = _jwt.request_callback()

    if token is None:
        raise JWTError('Authorization Required', 'Request does not contain an access token',
                       headers={'WWW-Authenticate': 'JWT realm="%s"' % realm})

    try:
        payload = _jwt.jwt_decode_callback(token)
    except jwt.InvalidTokenError as e:
        raise JWTError('Invalid token', str(e))

    _request_ctx_stack.top.current_identity = identity = _jwt.identity_callback(payload)

    if identity is None:
        raise JWTError('Invalid JWT', 'User does not exist')
复制代码

首先这个token需要从headers获取,这个由request_callback帮我们完成,接着需要将token进行解码,获取到我们之前编码的信息。jwt_decode_callback这个函数不仅进行了解码,还进行了token时效性的验证,因此超过时限的token也是无法访问接口的。通过一系列验证之后就来到了我们的重头戏了,为了突出它的关键,我们单独把这行代码列出来。

_request_ctx_stack.top.current_identity = identity = _jwt.identity_callback(payload)
复制代码

这段代码干了什么呢,首先它从我们传入的identity_callback中获取了我们用户对象,并将其推入_request_ctx_stack这个栈中,熟悉flask的小伙伴都知道它是一个线程隔离的栈。用户每一个请求进来都会创建一个线程,而这个栈处于每一个独立的线程中,所以它是线程安全的。flask-jwt将用户对象推入这个栈,这样一来这个线程就携带用户身份信息。那我们如何从栈中获取这个用户对象呢。这时候就要请到我们开头所说的current_identity对象了。它代理的对象就是这里推入的用户对象。所以我们可以在flask视图函数中通过调用current_identity来获取当前发出请求的用户信息了。

到此为止,整个验证过程分析完了。

3.总结

jwt机制通过无状态的编码来实现了身份验证,为前后端分离提供了便利。不过其中隐含了一定的安全问题,比如如果密钥泄露的话,通过泄露的密钥和用户id就可以自己签发token绕过验证系统。因此在实际开发过程中,有必要自定义包含信息的字典(源码中的payload)使得攻击者无法得知加密信息的格式,来避免攻击者自行签发token;定时更新密钥也是有效防范的措施。

posted on 2020-03-11 19:40  神笔马良  阅读(1772)  评论(0编辑  收藏  举报

导航