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

  • 相关阅读:
    【Java】Caused by: com.ibatis.sqlmap.client.SqlMapException: There is no statement named *** in this SqlMap.
    【Mac】Mac 使用 zsh 后, mvn 命令无效
    【Java】Exception thrown by the agent : java.rmi.server.ExportException: Port already in use: 1099
    【Android】drawable VS mipmap
    【Android】java.lang.SecurityException: getDeviceId: Neither user 10065 nor current process has android.permission.READ_PHONE_STATE
    java sql解析
    java动态编译
    随想
    一致hash算法
    一致性哈希算法及其在分布式系统中的应用
  • 原文地址:https://www.cnblogs.com/java2018/p/12464900.html
Copyright © 2020-2023  润新知