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