djangorestframework-jwt自帶的認證視圖進行用戶登錄驗證源代碼學習


Django REST framework JWT

djangorestframework-jwt自帶的認證視圖進行用戶登錄驗證源代碼學習

 SECRET_KEY = '1)q(f8jrz^edwtr2#h8vj=$u)ip4fx7#h@c41gvxtgc!dj#wkc'

定期動態生成SECRET_KEY

字符串導包   https://blog.csdn.net/chaoguo1234/article/details/81277590

安裝配置

安裝

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的方法

 

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_HANDLER

api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)  # 這三個參數分別對應settings文件下的參數

 

DEFAULTS 這個參數

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.py22-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',
]

 

 

 

 

 

 

 

 

路由: url(r'^authorizations/, obtain_jwt_token),

obtain_jwt_token來自$PYTHON_ENVTIONS_PATH/site-packages/rest_framework_jwt/views.py的102行和74-80行,代碼如下


免責聲明!

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



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