Django REST framework JWT
djangorestframework-jwt自帶的認證視圖進行用戶登陸驗證源代碼學習
SECRET_KEY = '1)q(f8jrz^edwtr2#h8vj=$u)ip4fx7#h@c41gvxtgc!dj#wkc'python
按期動態生成SECRET_KEYgit
字符串導包 https://blog.csdn.net/chaoguo1234/article/details/81277590github
安裝配置
安裝django
pip install djangorestframework-jwt
配置後端
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的方法api
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)
從api_settigs下去找,在rest_framework_jwt.settings下面瀏覽器
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLERcookie
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) # 這三個參數分別對應settings文件下的參數
DEFAULTS 這個參數app
DEFAULTS = { ... 'JWT_PAYLOAD_HANDLER': 'rest_framework_jwt.utils.jwt_payload_handler', ... }
從源碼能夠看出對應的就是框架
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER 中的 JWT_PAYLOAD_HANDLER ,key對應的value就是 'rest_framework_jwt.utils.jwt_payload_handler'
而rest_framework_jwt.utils.jwt_payload_handler其實就是一個導包路徑
如今從這個路徑下去尋找到utils下的jwt_payload_handler函數
def jwt_payload_handler(user): username_field = get_username_field() username = get_username(user) warnings.warn( 'The following fields will be removed in the future: ' '`email` and `user_id`. ', DeprecationWarning ) payload = { 'user_id': user.pk, 'username': username, 'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA # JWT_EXPIRATION_DELTA對應的就是在咱們配置裏指定的過時時間 } if hasattr(user, 'email'): payload['email'] = user.email if isinstance(user.pk, uuid.UUID): payload['user_id'] = str(user.pk) payload[username_field] = username # Include original issued at time for a brand new token, # to allow token refresh if api_settings.JWT_ALLOW_REFRESH: payload['orig_iat'] = timegm( datetime.utcnow().utctimetuple() ) if api_settings.JWT_AUDIENCE is not None: payload['aud'] = api_settings.JWT_AUDIENCE if api_settings.JWT_ISSUER is not None: payload['iss'] = api_settings.JWT_ISSUER return payload
下面在 jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER 用一樣的方法找到JWT_ENCODE_HANDLER對應的value, 也就是導包路徑
DEFAULTS = { ... 'JWT_ENCODE_HANDLER': 'rest_framework_jwt.utils.jwt_encode_handler', ... }
一樣根據導包路徑尋找
def jwt_encode_handler(payload): key = api_settings.JWT_PRIVATE_KEY or jwt_get_secret_key(payload) return jwt.encode( payload, key, api_settings.JWT_ALGORITHM ).decode('utf-8')
生成token的過程
瀏覽器的保存策略
Django REST framework JWT提供了登陸簽發JWT的視圖,能夠直接使用
from rest_framework_jwt.views import obtain_jwt_token urlpatterns = [ url(r'^authorizations/$', obtain_jwt_token), ]
可是默認的返回值僅有token,咱們還需在返回值中增長username和user_id。
從 obtain_jwt_token 進去
路由: url(r'^authorizations/, obtain_jwt_token),
obtain_jwt_token來自$PYTHON_ENVTIONS_PATH/site-packages/rest_framework_jwt/views.py
的102行和74-80行,代碼以下
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 """ 中間省略部分不相關代碼 """ obtain_jwt_token = ObtainJSONWebToken.as_view()
很明顯:這個就是一個登陸的視圖集
查看下繼承的JSONWebTokenAPIView視圖
jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER
...
class JSONWebTokenAPIView(APIView): # 繼承至APIView ... 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) # jwt_response_payload_handler 響應對象 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)
在jwt_response_payload_handler 響應對象中找到
def jwt_response_payload_handler(token, user=None, request=None): """ Returns the response data for both the login and refresh views. Override to return a custom response such as including the serialized representation of the User. Example: def jwt_response_payload_handler(token, user=None, request=None): return { 'token': token, 'user': UserSerializer(user, context={'request': request}).data } """ return { 'token': token }
能夠看出,登陸後返回的響應對象僅僅有token一個key , 這對於大多數場景來講都是不合適的,因此須要來重寫該方法
def jwt_response_payload_handler(token, user=None, request=None): """ 自定義jwt認證成功返回數據 """ return { 'token': token, 'user_id': user.id, 'username': user.username }
由於咱們自定義的該方法,因此也須要修改它的導包路徑,以前也找到了它的導包路徑傳入的源碼,則在配置文件中進行以下配置:
# JWT JWT_AUTH = { 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), 'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler', }
這樣,就實現了修改response響應對象
如今看完了繼承的類視圖,下面來看下序列化器:
在剛剛的源碼中能看獲得指定的序列化器就是 serializer_class = JSONWebTokenSerializer
既然指定了serializer_class = JSONWebTokenSerializer
說明是使用了DRF框架作驗證, 那麼驗證用戶登陸時傳輸的參數的代碼就是在序列化器類的代碼中
序列化器類來自於$PYTHON_ENVTIONS_PATH/site-packages/rest_framework_jwt/serializers.py
22-69行, 代碼以下:
class JSONWebTokenSerializer(Serializer): """ 省略部分代碼 """ def validate(self, attrs): # 獲取參數: 用戶登陸名稱 + 密碼 credentials = { self.username_field: attrs.get(self.username_field), 'password': attrs.get('password') } if all(credentials.values()): # 用戶登陸時傳入的參數完整, 則驗證用戶並獲取用戶對象 # 獲取用戶對象的代碼在下面👇這行代碼中!!! user = authenticate(**credentials) if user: if not user.is_active: msg = _('User account is disabled.') raise serializers.ValidationError(msg) payload = jwt_payload_handler(user) return { 'token': jwt_encode_handler(payload), 'user': user } else: msg = _('Unable to log in with provided credentials.') raise serializers.ValidationError(msg) else: msg = _('Must include "{username_field}" and "password".') msg = msg.format(username_field=self.username_field) raise serializers.ValidationError(msg)
獲取用戶對象的關鍵代碼在第50行 user = authenticate(**credentials)
; 而authenticate
到包自$PYTHON_ENVTIONS_PATH/site-packages/django/contrib/auth/init.py`的64行至81行, 代碼以下:
def authenticate(request=None, **credentials): """ If the given credentials are valid, return a User object. """ # 獲取驗證後端的backend對象的關鍵代碼在下面👇這行!!! for backend, backend_path in _get_backends(return_tuples=True): try: user = _authenticate_with_backend(backend, backend_path, request, credentials) except PermissionDenied: # This backend says to stop in our tracks - this user should not be allowed in at all. break if user is None: continue # Annotate the user object with the path of the backend. user.backend = backend_path return user # The credentials supplied are invalid to all backends, fire signal user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request)
這段代碼的核心就是
user = _authenticate_with_backend(backend, backend_path, request, credentials) # 而這句代碼的核心就是_authenticate_with_backend 這個名義上的私有方法
往下找
就在上面這個方法的下面
def _authenticate_with_backend(backend, backend_path, request, credentials): credentials = credentials.copy() # Prevent a mutation from propagating. args = (request,) # Does the backend accept a request argument? try: inspect.getcallargs(backend.authenticate, request, **credentials) # 很明顯backend.authenticate 中的 authenticate 就是核心邏輯
except TypeError: args = () credentials.pop('request', None) # Does the backend accept a request keyword argument? try: inspect.getcallargs(backend.authenticate, request=request, **credentials) except TypeError: # Does the backend accept credentials without request? try: inspect.getcallargs(backend.authenticate, **credentials) except TypeError: # This backend doesn't accept these credentials as arguments. Try the next one. return None else: warnings.warn( "Update %s.authenticate() to accept a positional " "`request` argument." % backend_path, RemovedInDjango21Warning ) else: credentials['request'] = request warnings.warn( "In %s.authenticate(), move the `request` keyword argument " "to the first positional argument." % backend_path, RemovedInDjango21Warning ) return backend.authenticate(*args, **credentials)
點進去
class ModelBackend(object): """ Authenticates against settings.AUTH_USER_MODEL. """ def authenticate(self, request, username=None, password=None, **kwargs): if username is None: username = kwargs.get(UserModel.USERNAME_FIELD) try: user = UserModel._default_manager.get_by_natural_key(username) except UserModel.DoesNotExist: # Run the default password hasher once to reduce the timing # difference between an existing and a non-existing user (#20760). UserModel().set_password(password) else: if user.check_password(password) and self.user_can_authenticate(user): return user
而上面這段代碼中 user = UserModel._default_manager.get_by_natural_key(username) 這句是核心代碼 不往下追了
能夠理解爲 User.objects.get(username=username)
就是 user = UserModel._default_manager.get_by_natural_key(username) 這裏寫死了只用username 去查詢User模型內的user對象是否存在,實際上jwt用的也是django的登陸認證方法
而咱們要實現多帳號登陸,則要重寫ModelBackend這個類
def get_user_by_account(account): """多帳號登陸的實現(手機號&用戶名)""" try: if re.match(r'^1[3-9]\d{9}$', account): user = User.objects.get(mobile=account) else: user = User.objects.get(username=account) except User.DoesNotExist: return None else: return user class UsernameMobileAuthBackend(ModelBackend): """重寫自定義django認證後端""" def authenticate(self, request, username=None, password=None, **kwargs): """ 重寫認證方式,使用多帳號登陸 :param request: 本次登陸請求對象 :param username: 用戶名/手機號 :param password: 密碼 :return: 返回值user/None """ # 1.經過傳入的username 獲取到user對象(經過手機號或用戶名動態查詢user) user = get_user_by_account(username) # 2.判斷user的密碼 if user and user.check_password(password): return user else: return None
那麼方法就重寫完了,下面就是要讓inspect.getcallargs(backend.authenticate, request, **credentials) 中的authenticate方法 去找到咱們重寫的類方法
而咱們以前在配置文件中獲知的配置方法
# 修改Django的默認的認證後端類 AUTHENTICATION_BACKENDS = [ 'users.utils.UsernameMobileAuthBackend', # 修改django認證後端類 ]
能夠從前面的這個代碼中提取_get_backends 方法
獲取用戶對象的關鍵代碼在第50行 user = authenticate(**credentials)
; 而authenticate
到包自$PYTHON_ENVTIONS_PATH/site-packages/django/contrib/auth/init.py`的64行至81行, 代碼以下:
def authenticate(request=None, **credentials): """ If the given credentials are valid, return a User object. """ # 獲取驗證後端的backend對象的關鍵代碼在下面👇這行!!! for backend, backend_path in _get_backends(return_tuples=True): try: user = _authenticate_with_backend(backend, backend_path, request, credentials) except PermissionDenied: # This backend says to stop in our tracks - this user should not be allowed in at all. break if user is None: continue # Annotate the user object with the path of the backend. user.backend = backend_path return user # The credentials supplied are invalid to all backends, fire signal user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request)
_get_backends 方法:
獲取驗證後端的backend對象的關鍵代碼在第68行for backend, backend_path in _get_backends(return_tuples=True):
;而_get_backends
對象來當前代碼文件的26-36行,代碼以下:
def _get_backends(return_tuples=False): backends = [] # 關鍵代碼在下面👇這行!!!! for backend_path in settings.AUTHENTICATION_BACKENDS: backend = load_backend(backend_path) backends.append((backend, backend_path) if return_tuples else backend) if not backends: raise ImproperlyConfigured( 'No authentication backends have been defined. Does ' 'AUTHENTICATION_BACKENDS contain anything?' ) return backends
關鍵代碼在第28行: for backend_path in settings.AUTHENTICATION_BACKENDS
, 而settings
導包自from django.conf import settings
, 那麼這裏的settings等同於咱們項目啓動時使用的meiduo_mall.settings.dev
而咱們在dev.py中添加了配置代碼以下:
# 告知Django使用自定義的認證後端 AUTHENTICATION_BACKENDS = [ 'users.utils.UsernameMobileAuthBackend', ]