詳解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;定時更新密鑰也是有效防范的措施。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM