详解flask 您所在的位置:网站首页 flask视图拦截器 详解flask

详解flask

2023-06-10 17:25| 来源: 网络整理| 查看: 265

前言

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;定时更新密钥也是有效防范的措施。

作者:jetto 张氏定理

qq:1045569270

声明:本文未经作者允许,禁止转载



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有