Django Rest Framework 认证(Authentication)

537 阅读6分钟

概述

身份认证(Authentication)是将传入请求与一组标识凭据(例如请求来自的用户或其签名的令牌)相关联的机制。然后权限(Permission)和限制(Throttling)可以使用这些凭据来确定是否应允许该请求。

认证始终是视图(view)的第一步,在权限、限制检查和其他代码执行之前进行。

request.user属性通常被设置为是contrb.auth库中User类的一个实例。 request.auth属性用于任何额外的身份认证信息,例如用于请求签名的身份验证令牌(token)

注意,验证本身不会允许或拒绝传入的请求,它只是识别请求所用的凭据。

如何确定身份验证

认证方案总被定义为一个认证类的列表。REST Framework会尝试用列表中的每个认证类进行身份验证,并将第一个成功进行身份验证的认证类的返回值赋给request.userrequest.auth

如果没有认证成功的认证类,request.user会被设置为django.contrib.auth.models.AnonymousUser的一个实例,request.auth将被设置为None

未认证请求的request.userrequest.auth的值可以使用UNAUTHENTICATED_USERUNAUTHENTICATED_TOKEN设置进行修改。

设置认证方案(authentication scheme)

默认的认证方案可以用DEFAULT_AUTHENTICATION_CLASSES全局设置,例如

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ]
}

也可以在基于APIView视图类的视图中设置认证方案,例如:

from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

class ExampleView(APIView):
    # 认证类列表
    authentication_classes = (SessionAuthentication, BasicAuthentication) 
    permission_classes = (IsAuthenticated,)

    def get(self, request, format=None):
        content = {
            'user': unicode(request.user),  # `django.contrib.auth.User` 实例。
            'auth': unicode(request.auth),  # None
        }
        return Response(content)

未认证和被禁止的响应

当未经身份验证的请求被拒绝时,有下面两种不同的错误代码可使用。

  • HTTP 401 Unauthorized
  • HTTP 403 Permission Denied

HTTP 401 响应包含一个WWW-Authenticateheader,指示客户端如何进行身份验证。HTTP 403响应不包括WWW-Authenticate。 具体选择哪种响应取决于认证方案。

如果请求通过了验证但是被拒绝执行请求的权限,无论是什么认证方式,都使用403 Permission Denied响应。

认证类 源码(authentication.py)

import base64  
import binascii  
  
from django.contrib.auth import authenticate, get_user_model  
from django.middleware.csrf import CsrfViewMiddleware  
from django.utils.translation import gettext_lazy as _  
  
from rest_framework import HTTP_HEADER_ENCODING, exceptions

def get_authorization_header(request):
    """
    返回bytestring类型的请求的'Authorization:' header。
    """
    auth = request.META.get('HTTP_AUTHORIZATION', b'') 
    # request.META是一个包含所有可用的HTTP header文件的字典
    if isinstance(auth, str):
        # Work around django test client oddness
        auth = auth.encode(HTTP_HEADER_ENCODING)
    return auth

BaseAuthentication

class BaseAuthentication:
    """
    所有的认证类都应给在BaseAuthentication的基础上扩展。
    """

    def authenticate(self, request):
        """       
        对请求进行身份验证并返回一个二元组(user,token)。
        """
        raise NotImplementedError(".authenticate() must be overridden.")

    def authenticate_header(self, request):
        """
        对于HTTP 401响应,返回一个字符串作为响应中`WWW-Authenticate`header的值;
        对于HTTP 403响应,返回`None`。
        """
        pass

BasicAuthentication

class BasicAuthentication(BaseAuthentication):
    """
    用户名/密码的HTTP基本身份验证
    """
    www_authenticate_realm = 'api'

    def authenticate(self, request):
        """
        如果提供了正确的用户名、密码,则返回一个`User`
        否则返回 `None`.
        """
        auth = get_authorization_header(request).split()
        """
        auth应给是一个有两个元素的列表,auth[0]=b'basic, auth[1]包含usename和password。'
        """
        if not auth or auth[0].lower() != b'basic':
            return None

        if len(auth) == 1:
            msg = _('Invalid basic header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _('Invalid basic header. Credentials string should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)

        try:
            try:
                auth_decoded = base64.b64decode(auth[1]).decode('utf-8')
            except UnicodeDecodeError:
                auth_decoded = base64.b64decode(auth[1]).decode('latin-1')
            auth_parts = auth_decoded.partition(':')
        except (TypeError, UnicodeDecodeError, binascii.Error):
            msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
            raise exceptions.AuthenticationFailed(msg)

        userid, password = auth_parts[0], auth_parts[2]
        return self.authenticate_credentials(userid, password, request)

    def authenticate_credentials(self, userid, password, request=None):
        """
        通过userid和password进行验证
        """
        credentials = {
            get_user_model().USERNAME_FIELD: userid,
            'password': password
        }
        user = authenticate(request=request, **credentials)

        if user is None:
            raise exceptions.AuthenticationFailed(_('Invalid username/password.'))

        if not user.is_active:
            raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

        return (user, None)

    def authenticate_header(self, request):
        return 'Basic realm="%s"' % self.www_authenticate_realm

SessionAuthentication

class SessionAuthentication(BaseAuthentication):
    """
    利用Django的session框架来验证
    """

    def authenticate(self, request):
        """
        Returns a `User` if the request session currently has a logged in user.
        Otherwise returns `None`.
        """

        # Get the session-based user from the underlying HttpRequest object
        user = getattr(request._request, 'user', None)

        # Unauthenticated, CSRF validation not required
        if not user or not user.is_active:
            return None

        self.enforce_csrf(request)

        # CSRF passed with authenticated user
        return (user, None)

    def enforce_csrf(self, request):
        """
        Enforce CSRF validation for session based authentication.
        """
        def dummy_get_response(request):  # pragma: no cover
            return None

        check = CSRFCheck(dummy_get_response)
        # populates request.META['CSRF_COOKIE'], which is used in process_view()
        check.process_request(request)
        reason = check.process_view(request, None, (), {})
        if reason:
            # CSRF failed, bail with explicit error message
            raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)

TokenAuthentication

class TokenAuthentication(BaseAuthentication):
    """
    简单的基于令牌的身份验证。

    客户端应该通过HTTP header中的"Authorization"传递令牌进行身份验证,令牌以字符串"Token"
    开头,例如:
    
        Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a
    """

    keyword = 'Token'
    model = None

    def get_model(self):
        if self.model is not None:
            return self.model
        from rest_framework.authtoken.models import Token
        return Token

    """
    A custom token model may be used, but must have the following properties.

    * key -- The string identifying the token
    * user -- The user to which the token belongs
    """

    def authenticate(self, request):
        auth = get_authorization_header(request).split()

        if not auth or auth[0].lower() != self.keyword.lower().encode():
            return None

        if len(auth) == 1:
            msg = _('Invalid token header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _('Invalid token header. Token string should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)

        try:
            token = auth[1].decode()
        except UnicodeError:
            msg = _('Invalid token header. Token string should not contain invalid characters.')
            raise exceptions.AuthenticationFailed(msg)

        return self.authenticate_credentials(token)

    def authenticate_credentials(self, key):
        model = self.get_model()
        try:
            token = model.objects.select_related('user').get(key=key)
        except model.DoesNotExist:
            raise exceptions.AuthenticationFailed(_('Invalid token.'))

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

        return (token.user, token)

    def authenticate_header(self, request):
        return self.keyword

自定义认证类

自定义的认证方法,要继承BaseAuthentication类并重写.authenticate(self, request)方法。

  • 认证成功,返回二元组(user, auth)
  • 认证失败,返回None

认证失败也可以不返回None而是抛出AuthenticationFailed

如果重写.authenticate_header(self, request),则应该返回一个字符串,该字符串将作用于HTTP 401 Unauthorized响应中WWW-Authenticateheader的值。

如果.authenticate_header(self, request)未被重写,则认证方案将在未验证的请求被拒绝的时候返回HTTP 403 Forbidden响应。

from django.contrib.auth.models import User
from rest_framework import authentication
from rest_framework import exceptions

class ExampleAuthentication(authentication.BaseAuthentication):
    def authenticate(self, request):
        username = request.META.get('X_USERNAME')
        if not username:
            return None

        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            raise exceptions.AuthenticationFailed('No such user')

        return (user, None)

JWTAuthentication

rest_framework_simplejwt.authentication.JWTAuthentication

class JWTAuthentication(authentication.BaseAuthentication):
    """
    一个身份验证插件,通过请求头中提供的JSON web token对请求进行身份验证。
    """

    www_authenticate_realm = "api"
    media_type = "application/json"

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.user_model = get_user_model()

    def authenticate(self, request: Request) -> Optional[Tuple[AuthUser, Token]]:
        header = self.get_header(request)
        if header is None:
            return None

        raw_token = self.get_raw_token(header)
        if raw_token is None:
            return None

        validated_token = self.get_validated_token(raw_token)

        return self.get_user(validated_token), validated_token

    def authenticate_header(self, request: Request) -> str:
        return '{} realm="{}"'.format(
            AUTH_HEADER_TYPES[0],
            self.www_authenticate_realm,
        )

    def get_header(self, request: Request) -> bytes:
        """
        Extracts the header containing the JSON web token from the given
        request.
        """
        header = request.META.get(api_settings.AUTH_HEADER_NAME)

        if isinstance(header, str):
            # Work around django test client oddness
            header = header.encode(HTTP_HEADER_ENCODING)

        return header

    def get_raw_token(self, header: bytes) -> Optional[bytes]:
        """
        Extracts an unvalidated JSON web token from the given "Authorization"
        header value.
        """
        parts = header.split()

        if len(parts) == 0:
            # Empty AUTHORIZATION header sent
            return None

        if parts[0] not in AUTH_HEADER_TYPE_BYTES:
            # Assume the header does not contain a JSON web token
            return None

        if len(parts) != 2:
            raise AuthenticationFailed(
                _("Authorization header must contain two space-delimited values"),
                code="bad_authorization_header",
            )

        return parts[1]

    def get_validated_token(self, raw_token: bytes) -> Token:
        """
        Validates an encoded JSON web token and returns a validated token
        wrapper object.
        """
        messages = []
        for AuthToken in api_settings.AUTH_TOKEN_CLASSES:
            try:
                return AuthToken(raw_token)
            except TokenError as e:
                messages.append(
                    {
                        "token_class": AuthToken.__name__,
                        "token_type": AuthToken.token_type,
                        "message": e.args[0],
                    }
                )

        raise InvalidToken(
            {
                "detail": _("Given token not valid for any token type"),
                "messages": messages,
            }
        )

    def get_user(self, validated_token: Token) -> AuthUser:
        """
        Attempts to find and return a user using the given validated token.
        """
        try:
            user_id = validated_token[api_settings.USER_ID_CLAIM]
        except KeyError:
            raise InvalidToken(_("Token contained no recognizable user identification"))

        try:
            user = self.user_model.objects.get(**{api_settings.USER_ID_FIELD: user_id})
        except self.user_model.DoesNotExist:
            raise AuthenticationFailed(_("User not found"), code="user_not_found")

        if not user.is_active:
            raise AuthenticationFailed(_("User is inactive"), code="user_inactive")

        if api_settings.CHECK_REVOKE_TOKEN:
            if validated_token.get(
                api_settings.REVOKE_TOKEN_CLAIM
            ) != get_md5_hash_password(user.password):
                raise AuthenticationFailed(
                    _("The user's password has been changed."), code="password_changed"
                )

        return user

参考