登錄注冊是幾乎所有網站都需要去做的接口,而說到登錄,自然也就涉及到驗證以及用戶登錄狀態保存,最近用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_authenticated(),
