django drf框架中的user驗證以及JWT拓展的介紹
登入註冊是幾乎所有網站都需要去做的介面,而說到登入,自然也就涉及到驗證以及使用者登入狀態儲存,最近用DRF在做的一個關於網上商城的專案中,引入了一個拓展DRF JWT,專門用於做驗證和使用者狀態儲存。這個拓展比傳統的CSRF更加安全。先來介紹一下JWT認證機制吧!
Json web token (JWT), 是為了在網路應用環境間傳遞宣告而執行的一種基於JSON的開放標準((RFC 7519).該token被設計為緊湊且安全的,特別適用於分散式站點的單點登入(SSO)場景。JWT的宣告一般被用來在身份提供者和服務提供者間傳遞被認證的使用者身份資訊,以便於從資源伺服器獲取資源,也可以增加一些額外的其它業務邏輯所必須的宣告資訊,該token也可直接被用於認證,也可被加密。
基於token的鑑權機制
基於token的鑑權機制類似於http協議也是無狀態的,它不需要在服務端去保留使用者的認證資訊或者會話資訊。這就意味著基於token認證機制的應用不需要去考慮使用者在哪一臺伺服器登入了,這就為應用的擴充套件提供了便利。
流程上是這樣的:
- 使用者使用使用者名稱密碼來請求伺服器
- 伺服器進行驗證使用者的資訊
- 伺服器通過驗證傳送給使用者一個token
- 客戶端儲存token,並在每次請求時附送上這個token值
- 服務端驗證token值,並返回資料
這個token必須要在每次請求時傳遞給服務端,它應該儲存在請求頭裡, 另外,服務端要支援CORS(跨來源資源共享)
策略,一般我們在服務端這麼做就可以了Access-Control-Allow-Origin: *
那麼我們現在回到JWT的主題上。
JWT的構成
第一部分我們稱它為頭部(header),第二部分我們稱其為載荷(payload, 類似於飛機上承載的物品),第三部分是簽證(signature).
jwt的頭部承載兩部分資訊:1,宣告型別,這裡是jwt,2宣告加密的演算法 通常直接使用 HMAC SHA256。完整的頭部就像下面這樣的JSON:
{ 'typ': 'JWT', 'alg': 'HS256' }
然後將頭部進行base64加密(該加密是可以對稱解密的),構成了第一部分。如eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9。
載荷就是存放有效資訊的地方。這個名字像是特指飛機上承載的貨品,這些有效資訊包含三個部分1標準中註冊的宣告,2公共的宣告,3私有的宣告。標準中註冊的宣告 (建議但不強制使用) :1 iss: jwt簽發者,2 sub: jwt所面向的使用者,3 aud: 接收jwt的一方,4 exp: jwt的過期時間,這個過期時間必須要大於簽發時間,5 nbf: 定義在什麼時間之前,該jwt都是不可用的,6 iat: jwt的簽發時間,7 jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。公共的宣告 : 公共的宣告可以新增任何的資訊,一般新增使用者的相關資訊或其他業務需要的必要資訊.但不建議新增敏感資訊,因為該部分在客戶端可解密.私有的宣告 : 私有宣告是提供者和消費者所共同定義的宣告,一般不建議存放敏感資訊,因為base64是對稱解密的,意味著該部分資訊可以歸類為明文資訊。定義一個payload:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
然後將其進行base64加密,得到JWT的第二部分。如eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9。
JWT的第三部分是一個簽證資訊,這個簽證資訊由三部分組成:1 header (base64後的),2 payload (base64後的),3 secret。這個部分需要base64加密後的header和base64加密後的payload使用.
連線組成的字串,然後通過header中宣告的加密方式進行加鹽secret
組合加密,然後就構成了jwt的第三部分。
注意:secret是儲存在伺服器端的,jwt的簽發生成也是在伺服器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味著客戶端是可以自我簽發jwt了。
首先需要安裝拓展 pip install djangorestframework-jwt,然後在django進行配置,
JWT_EXPIRATION_DELTA 指明token的有效期。
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', ), } JWT_AUTH = { 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), }
Django REST framework JWT 擴充套件的說明文件中提供了手動簽發JWT的方法
from rest_framework_jwt.settings import api_settings jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER payload = jwt_payload_handler(user) token = jwt_encode_handler(payload)
在註冊時,引入上述程式碼,簽發JWT即可。而對於登入,JWT拓展提供了內建的檢視,在urls中新增對於路由即可。
from rest_framework_jwt.views import obtain_jwt_token urlpatterns = [ url(r'^authorizations/$', obtain_jwt_token), ]
雖然寫起來很簡單,但是內部其實做了很多的事,今天就來詳細研究一下,原始碼內部做了哪些事情。
當用戶登入,會以post形式發請求到後端,會訪問obtain_jwt_token中的post方法,在原始碼中可以看到obtain_jwt_token = ObtainJSONWebToken.as_view(),跟我們寫的類檢視十分類似,這是一個內部已經寫好的類檢視。
class ObtainJSONWebToken(JSONWebTokenAPIView): """ API View that receives a POST with a user's username and password. Returns a JSON Web Token that can be used for authenticated requests. """ serializer_class = JSONWebTokenSerializer
該類並未定義任何方法,所以對應的post方法應該寫在父類,下面是父類中的post方法
def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) if serializer.is_valid(): user = serializer.object.get('user') or request.user token = serializer.object.get('token') response_data = jwt_response_payload_handler(token, user, request) response = Response(response_data) if api_settings.JWT_AUTH_COOKIE: expiration = (datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA) response.set_cookie(api_settings.JWT_AUTH_COOKIE, token, expires=expiration, httponly=True) return response return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
上述方法返回一個Response物件,經過一系列操作返回到前端,訪問結束。
而在DRF框架中,在呼叫檢視之前,就會進行相應的驗證操作。想要了解整個過程,需要我們從原始碼中一步步去探索。當前端發起一個請求到後端,會根據路由訪問物件的檢視類的as_view()方法,該方法會接著呼叫dispatch()方法,APIView是DRF中所有檢視類的父類,可以看一下他的dispatch方法。
def dispatch(self, request, *args, **kwargs): """ `.dispatch()` is pretty much the same as Django's regular dispatch, but with extra hooks for startup, finalize, and exception handling. """ self.args = args self.kwargs = kwargs request = self.initialize_request(request, *args, **kwargs) self.request = request self.headers = self.default_response_headers # deprecate? try: self.initial(request, *args, **kwargs) # Get the appropriate handler method if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed response = handler(request, *args, **kwargs) except Exception as exc: response = self.handle_exception(exc) self.response = self.finalize_response(request, response, *args, **kwargs) return self.response
可以看到,到請求進來,會呼叫self.initalize_request()方法對請求進行處理。
def initialize_request(self, request, *args, **kwargs): """ Returns the initial request object. """ parser_context = self.get_parser_context(request) return Request( request, parsers=self.get_parsers(), authenticators=self.get_authenticators(), negotiator=self.get_content_negotiator(), parser_context=parser_context )
我們需要關注的只有authenticators=self.get_authenticators()這一句,
def get_authenticators(self): """ Instantiates and returns the list of authenticators that this view can use. """ return [auth() for auth in self.authentication_classes]
接著往上照,可以看到類屬性permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES,這就是要什麼我們要在django配置中加入DEFAULT_PERMISSION_CLASSES配置,上述方法會遍歷我們在配置中寫到的列表,拿到裡面的驗證類,並進行例項化,並將生成的物件裝在一個新的列表中,儲存在新生成的Request物件中。然後我們接著看dispatch方法,在實際呼叫檢視類中的對應方法前,還呼叫了self.initial()方法進行一些初始化操作。
def initial(self, request, *args, **kwargs): """ Runs anything that needs to occur prior to calling the method handler. """ self.format_kwarg = self.get_format_suffix(**kwargs) # Perform content negotiation and store the accepted info on the request neg = self.perform_content_negotiation(request) request.accepted_renderer, request.accepted_media_type = neg # Determine the API version, if versioning is in use. version, scheme = self.determine_version(request, *args, **kwargs) request.version, request.versioning_scheme = version, scheme # Ensure that the incoming request is permitted self.perform_authentication(request) self.check_permissions(request) self.check_throttles(request)
我們需要關注的是最後三排,分別呼叫了三個方法,進行身份驗證,許可權驗證和分流操作,這裡我們只關注 身份驗證方法,self.perform_authentication(),該方法內只有一句程式碼request.user,看起來像是在呼叫request物件的user屬性,其實不然,我們可以到DRF框架的Request物件中找到以下方法。
@property def user(self): """ Returns the user associated with the current request, as authenticated by the authentication classes provided to the request. """ if not hasattr(self, '_user'): with wrap_attributeerrors(): self._authenticate() return self._user
user方法經過property裝飾器裝飾後,就可以像一個屬性一樣呼叫該方法,該方法在Request物件中存在對應的user時會直接返回,若使用者登陸時,Request物件中沒有對應的user,所以程式碼會走if判斷裡面,我們只需要關注方法self._authenticate()的呼叫即可。
def _authenticate(self): """ Attempt to authenticate the request using each authentication instance in turn. """ for authenticator in self.authenticators: try: user_auth_tuple = authenticator.authenticate(self) except exceptions.APIException: self._not_authenticated() raise if user_auth_tuple is not None: self._authenticator = authenticator self.user, self.auth = user_auth_tuple return self._not_authenticated()
可以看到,該方法會遍歷我們在之前處理Request物件時傳入的裝著驗證類物件的列表,並呼叫驗證類的authenticate()方法,若驗證成功生成對應的self.user和self.auth並直接return,往上則直接將生成的self.user進行返回,若驗證內部出錯,會呼叫self._not_authenticated(),並丟擲錯誤,往上看,在dispatch方法中,若初始化方法出錯,則進行捕獲,並呼叫self.handle_exception()方法生成一個Response物件進行返回,不會執行檢視類中對應的方法,則呼叫對於的是self._not_authenticated()。
def _not_authenticated(self): """ Set authenticator, user & authtoken representing an unauthenticated request. Defaults are None, AnonymousUser & None. """ self._authenticator = None if api_settings.UNAUTHENTICATED_USER: self.user = api_settings.UNAUTHENTICATED_USER() else: self.user = None if api_settings.UNAUTHENTICATED_TOKEN: self.auth = api_settings.UNAUTHENTICATED_TOKEN() else: self.auth = None
UNAUTHENTICATED_USER在django預設配置中為一個匿名使用者的類,UNAUTHENTICATED_TOKEN預設為None,若所有驗證都為通過或者某一驗證過程中出錯,則生成一個匿名使用者,並將self.auth設定為None。
綜上所述,在請求執行之前,DRF框架會根據我們在配置檔案中配置的驗證類對使用者進行身份驗證,若未通過驗證,則會生成一個匿名使用者,驗證通過,則生成對應的使用者。
我的QQ:595395786。想交流的可以加一下!!!
self._not_authenticate