概述
身份认证(Authentication)是将传入请求与一组标识凭据(例如请求来自的用户或其签名的令牌)相关联的机制。然后权限(Permission)和限制(Throttling)可以使用这些凭据来确定是否应允许该请求。
认证始终是视图(view)的第一步,在权限、限制检查和其他代码执行之前进行。
request.user属性通常被设置为是contrb.auth库中User类的一个实例。
request.auth属性用于任何额外的身份认证信息,例如用于请求签名的身份验证令牌(token)
注意,验证本身不会允许或拒绝传入的请求,它只是识别请求所用的凭据。
如何确定身份验证
认证方案总被定义为一个认证类的列表。REST Framework会尝试用列表中的每个认证类进行身份验证,并将第一个成功进行身份验证的认证类的返回值赋给request.user和request.auth。
如果没有认证成功的认证类,request.user会被设置为django.contrib.auth.models.AnonymousUser的一个实例,request.auth将被设置为None。
未认证请求的request.user和request.auth的值可以使用UNAUTHENTICATED_USER和UNAUTHENTICATED_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