详解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_handler
和 identity_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_KEY
和JWT_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;定时更新密钥也是有效防范的措施。