手摸手,带你用Django REST Framework撸接口系列六(认证篇)

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

REST framework 提供了一些开箱即用的身份验证方案,并且还允许你实现自定义方案。

说到认证还是需要提一下权限,认证只是让服务端知道了访问这是谁,来确定给你返回什么信息,而权限则是限制的访问内容和操作的,一定程度上我们需要在权限的基础上去做认证。

认证和权限都是针对用户的,比如我这个页面只允许登录的用户查看,其实这就是一种权限,这种权限就是需要我们通过了用户的认证才可以查看。

认证与权限的区别

认证(Authentication)与权限(Permission)不是一回事。认证是通过用户提供的用户ID/密码组合或者Token来验证用户的身份。权限(Permission)的校验发生验证用户身份以后,是由系统根据分配权限确定用户可以访问何种资源以及对这种资源进行何种操作,这个过程也被称为授权(Authorization)。

无论是Django还是DRF, 当用户成功通过身份验证以后,系统会把已通过验证的用户对象与request请求绑定,这样一来你就可以使用request.user获取这个用户对象的所有信息了。

什么是认证(Authentication)

身份验证是将传入的请求对象(request)与一组标识凭据(例如用户名+密码或者令牌token)相关联的机制。REST framework 提供了一些开箱即用的身份验证方案,并且还允许你实现自定义方案。

DRF的每个认证方案实际上是一个类。你可以在视图中使用一个或多个认证方案类。REST framework 将尝试使用列表中的每个类进行身份验证,并使用成功完成验证的第一个类的返回的元组设置 request.userrequest.auth

用户通过认证后request.user返回Django的User实例,否则返回AnonymousUser的实例。request.auth通常为None。如果使用token认证,request.auth可以包含认证过的token。

:认证一般发生在权限校验之前。

DRF自带认证方案

Django REST Framework提供了如下几种认证方案:

  • Session认证SessionAuthentication类:此认证方案使用Django的默认session后端进行身份验证。当客户端发送登录请求通过验证后,Django通过session将用户信息存储在服务器中保持用户的请求状态。Session身份验证适用于与你的网站在相同的Session环境中运行的AJAX客户端 (注:这也是Session认证的最大弊端)。
  • 基本认证BasicAuthentication类:此认证方案使用HTTP 基本认证,针对用户的用户名和密码进行认证。使用这种方式后浏览器会跳出登录框让用户输入用户名和密码认证。基本认证通常只适用于测试。
  • 远程认证RemoteUserAuthentication类:此认证方案为用户名不存在的用户自动创建用户实例。这个很少用,具体见文档。
  • Token认证TokenAuthentication类:该认证方案是DRF提供的使用简单的基于Token的HTTP认证方案。当客户端发送登录请求时,服务器便会生成一个Token并将此Token返回给客户端,作为客户端进行请求的一个标识以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。后面我们会详细介绍如何使用这种认证方案。

注意:如果你在生产环境下使用BasicAuthenticationTokenAuthentication认证,你必须确保你的API仅在https可用。

设置认证方案

可以使用 DEFAULT_AUTHENTICATION_CLASSES 设置全局的默认身份验证方案。比如:

1
2
3
4
5
6
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
)
}

你还可以使用基于APIView类视图的方式,在每个view或每个viewset基础上设置身份验证方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)

或者,如果你使用基于函数的视图,那就使用@api_view装饰器。

1
2
3
4
5
6
7
8
9
@api_view(['GET'])
@authentication_classes((SessionAuthentication, BasicAuthentication))
@permission_classes((IsAuthenticated,))
def example_view(request, format=None):
content = {
'user': unicode(request.user), # `django.contrib.auth.User` 实例。
'auth': unicode(request.auth), # None
}
return Response(content)

自定义认证

要实现自定义的认证方案,要继承BaseAuthentication类并且重写.authenticate(self, request) 方法。如果认证成功,该方法应返回(user, auth)的二元元组,否则返回None

在某些情况下,你可能不想返回None,而是希望从.authenticate()方法抛出AuthenticationFailed异常。

通常你应该采取的方法是:

  • 如果不尝试验证,返回None。还将检查任何其他正在使用的身份验证方案。
  • 如果尝试验证但失败,则抛出AuthenticationFailed异常。无论任何权限检查也不检查任何其他身份验证方案,立即返回错误响应。

你也可以重写.authenticate_header(self, request)方法。如果实现该方法,则应返回一个字符串,该字符串将用作HTTP 401 Unauthorized响应中的WWW-Authenticate头的值。

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

以下示例将以自定义请求标头中名称为’X_USERNAME’提供的用户名作为用户对任何传入请求进行身份验证,其它类似自定义认证需求比如支持用户同时按用户名或email进行验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)

前后端分离时为何推荐token认证

  • Token无需存储降低服务器成本,session是将用户信息存储在服务器中的,当用户量增大时服务器的压力也会随着增大。
  • 防御CSRF跨站伪造请求攻击,session是基于cookie进行用户识别的, cookie如果被截获,用户信息就容易泄露。
  • 扩展性强,session需要存储无法共享,当搭建了多个服务器时其他服务器无法获取到session中的验证数据用户无法验证成功。Token可以实现服务器间共享,这样不管哪里都可以访问到。
  • Token可以减轻服务器的压力,减少频繁的查询数据库。
  • 支持跨域访问, 适用于移动平台应用

使用TokenAuthentication

DRF自带的TokenAuthentication方案可以实现基本的token认证,整个流程如下:

首先,你需要将修改settings.py, 加入如下app。

1
2
3
4
INSTALLED_APPS = (
...
'rest_framework.authtoken'
)

记得迁移一下数据库

1
2
python manager.py makemigrations
python manager.py migrate

如果你已经创建了一些用户,则可以打开shell为所有现有用户生成令牌,如下所示:

1
2
3
4
5
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token

for user in User.objects.all():
Token.objects.get_or_create(user=user)

你还可以在admin.py中给用户创建token,如下所示:

1
2
from rest_framework.authtoken.admin import TokenAdmin
TokenAdmin.raw_id_fields = ['user']

从3.6.4起,你还可以使用如下命令为一个指定用户新建或重置token。

1
2
./manage.py drf_create_token <username> # 新建
./manage.py drf_create_token -r <username> # 重置

接下来,你需要暴露用户获取token的url地址(API端点).

1
2
from rest_framework.authtoken import views
path("get_token", views.obtain_auth_token),

这样每当用户使用form表单或JSON将有效的usernamepassword字段POST提交到以上视图时,obtain_auth_token视图将返回如下JSON响应:

1
{ 'token' : '58b9bf3131a6b569f81715afbecf4a7e9c6b7cb7' }

客户端拿到token后可以将其存储到本地cookie或localstorage里,下次发送请求时把token包含在Authorization HTTP头即可,如下所示:

1
Authorization: Token 58b9bf3131a6b569f81715afbecf4a7e9c6b7cb7

你还可以通过curl工具来进行简单测试。

1
curl -X GET http://127.0.0.1:8000/api/articles/ -H 'Authorization: Token 58b9bf3131a6b569f81715afbecf4a7e9c6b7cb7'

或者使用postman测试 ,首先你要确保要测试的视图权限(permission_classes)是IsAuthenticated,只有验证用户才可访问。

1
2
3
4
5
6
7
8
9
10
11
class ArticleView(viewsets.ModelViewSet):
"""文章列表视图类"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 只有登录用户才能访问该视图
permission_classes = (IsAuthenticated, )
authentication_classes = (TokenAuthentication, )

def perform_create(self, serializer):
# 将request.user与author绑定。调用create方法时执行如下函数。
serializer.save(author=self.request.user)

测试不携带token访问,直接提示身份认证信息未提供。

然后测试携带token访问,数据正常显示了。

自定义Token返回信息

默认的obtain_auth_token视图返回的json响应数据是非常简单的,只有token一项。如果你希望返回更多信息,比如用户id或email,就就要通过继承ObtainAuthToken类量身定制这个视图,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.response import Response


class TokenView(ObtainAuthToken):
"""自定义Token视图类"""

def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user)
return Response({
'token': token.key,
"email": user.email,
"id": user.id
})

然后修改urls.py:

1
2
3
from api.authenticate import TokenView

path("get_token", TokenView.as_view()),

TokenAuthentication小结

通过演示流程可以看出来,Django REST framework自身携带的token认证一样还是要请求数据库验证token,而且也不可以设置token有效期,所以后面我们着重认识一下JSON Web Token。

JSON Web Token

JSON Web Token(JWT)是一种开放标准,它定义了一种紧凑且自包含的方式,用于各方之间安全地将信息以JSON对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。JWT用于为应用程序创建访问token,通常适用于API身份验证和服务器到服务器的授权。那么如何理解紧凑和自包含这两个词的含义呢?

  • 紧凑:就是说这个数据量比较少,可以通过url参数,http请求提交的数据以及http header多种方式来传递。
  • 自包含:这个字符串可以包含很多信息,比如用户id,用户名,订单号id等,如果其他人拿到该信息,就可以拿到关键业务信息。

那么JWT认证是如何工作的呢? 首先客户端提交用户登录信息验证身份通过后,服务器生成一个用于证明用户身份的令牌(token),也就是一个加密后的长字符串,并将其发送给客户端。在后续请求中,客户端以各种方式(比如通过url参数或者请求头)将这个令牌发送回服务器,服务器就知道请求来自哪个特定身份的用户了。

JSON Web Token由三部分组成,这些部分由点(.)分隔,分别是header(头部),payload(有效负载)和signature(签名)。

  • header(头部): 识别以何种算法来生成签名;
  • pyload(有效负载): 用来存放实际需要传递的数据;
  • signature(签名): 安全验证token有效性,防止数据被篡改。

通过http传输的数据实际上是加密后的JWT,它是由两个点分割的base64-URL长字符串组成,解密后我们可以得到header, payload和signature三部分。我们可以简单的使用 https://jwt.io/ 官网来生成或解析一个JWT,如下所示:

后面我们会用django-rest-framework-simplejwt这个第三方软件包进行JWT身份验证。

JSON Web Token是一种更新的使用token进行身份认证的标准。与内置的TokenAuthentication方案不同,JWT身份验证不需要使用数据库来验证令牌, 而且可以轻松设置token失效期。Django中可以通过djangorestframework-simplejwt 这个第三方包轻松实现JWT认证,当然第三方包有很多,大家可以根据自己喜欢和需求去选择。

Django OAuth Toolkit

Django OAuth Toolkit 包提供了OAuth 2.0 认证支持,并且兼容Python 2.7和Python 3.3+。这个包使用优秀的OAuthLib,由Evonove维护。该软件包有很完善的文档,并得到很好的支持,目前是我们推荐使用的OAuth 2.0支持软件包

安装和配置

使用pip安装。

1
pip install django-oauth-toolkit

把这个包添加到你的INSTALLED_APPS中,并且修改你的REST framework设置。

1
2
3
4
5
6
7
8
9
10
INSTALLED_APPS = (
...
'oauth2_provider',
)

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'oauth2_provider.ext.rest_framework.OAuth2Authentication',
)
}

Django REST framework OAuth

Django REST framework OAuth包提供OAuth1和OAuth2支持。

这个软件包以前直接包含在REST framework中,但现在已被作为第三方软件包支持和维护。

安装和配置

使用pip进行安装。

1
pip install djangorestframework-oauth

Digest Authentication

HTTP摘要认证是一种广泛实现的方案,旨在替代HTTP基本认证,并提供简单的加密认证机制。Juan Riaza维护着djangorestframework-digestauth为REST framework提供了HTTP摘要认证支持。

Django OAuth2 Consumer

Rediker Software的Django OAuth2 Consumer是另一个为REST框架提供OAuth 2.0 support for REST framework的软件包。该包包含tokens范围限制权限,允许对你的API进行更细粒度的访问。

JSON Web Token Authentication

JSON Web Token是一种相当新的标准,可用于基于token的身份验证。与内置的TokenAuthentication方案不同,JWT身份验证不需要使用数据库来验证令牌。Blimp维护djangorestframework-jwt软件包,它提供了一个JWT Authentication类以及一个机制,客户端获得一个给定用户名和密码的JWT。

Hawk HTTP Authentication

HawkREST库基于Mohawk库,让你可以在API中使用Hawk签名的请求和响应。Hawk让双方使用共享密钥签名的消息彼此安全地进行通信。它基于HTTP MAC access authentication访问认证(它基于OAuth 1.0的部分)。

HTTP Signature Authentication

HTTP签名(目前为IETF草案)提供了一种实现HTTP消息的源认证和消息完整性的方法。与Amazon的HTTP签名方案类似,许多服务使用它,它允许无状态的每个请求的身份验证。Elvio Toccalino维护了djangorestframework-httpsignature包,提供了一个易于使用的HTTP签名身份验证机制。

Djoser

Djoser库提供一组视图来处理基本操作,如注册,登录,注销,密码重置和帐户激活。该包使用自定义用户模型,它使用基于token的身份验证。这是一个可以使用REST实现的Django认证系统。

django-rest-auth

Django-rest-auth库提供了一组REST API端点,用于注册,身份验证(包括社交媒体身份验证),密码重置,检索和更新用户详细信息等。有了这些API端点之后,你的客户端应用程序(如AngularJS,iOS,Android和其他)可以通过REST API独立通信到Django后端站点,以进行用户管理。

django-rest-framework-social-oauth2

Django-rest-framework-social-oauth2库提供了一种将社交插件(facebook,twitter,google等)集成到你的身份验证系统和简单的oauth2设置的简单方法。使用这个库,你将能够根据外部token(例如,Facebook访问token)对用户进行身份验证,将这些令牌转换为“内部”oauth2 tokens,并使用和生成oauth2 tokens来验证用户。

django-rest-knox

Django-rest-knox库提供了模型和视图,以比内置的TokenAuthentication方案更安全和可扩展的方式来处理基于token的身份验证 - 使用单页面应用程序和移动客户端能够一起。它为每个客户端提供tokens,以及在提供一些其他身份验证(通常是基本身份验证)时生成tokens,删除token(提供服务器强制注销)和删除所有tokens(注销用户登录的所有客户端)的视图。

djangorestframework-simplejwt

安装

django-rest-framework-simplejwt为Django REST Framework提供了JSON Web Token认证后端。它提供了一组保守的默认功能来涵盖了JWT的最常见用例,而且非常容易扩展。

首先,我们要使用pip安装它。

1
pip install djangorestframework-simplejwt

其次,我们需要告诉DRF我们使用jwt认证作为后台认证方案。修改apiproject/settings.py

1
2
3
4
5
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}

最后,我们需要提供用户可以获取和刷新token的urls地址,这两个urls分别对应TokenObtainPairViewTokenRefreshView两个视图。

1
2
3
4
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

path("jwt_token", TokenObtainPairView.as_view(), name="jwt_token"),
path("jwt_token/refresh", TokenRefreshView.as_view(), name="jwt_token_refresh"),

测试

现在我们可以开始使用postman测试了。通过POST方法发送登录请求到/token/, 请求数据包括username和password。如果登录成功,你将得到两个长字符串,一个是access token(访问令牌),还有一个是refresh token(刷新令牌),如下所示:

假如你有一个受保护的视图(比如这里的/articles/),权限(permission_classes)是IsAuthenticated,只有验证用户才可访问。访问这个保护视图时你只需要在请求头的Authorization选项里输入你刚才获取的access token即可,如下所示:

不过这个access token默认只有5分钟有效。5分钟过后,当你再次访问保护视图时,你将得到如下token已失效或过期的提示:

去获取新的access token,你需要使用之前获得的refresh token。你将这个refresh token放到请求的正文(body)里,发送POST请求到/jwt_token/refresh/即可获得刷新后的access token(访问令牌), 如下所示:

Simple JWT中的access token默认有效期是5分钟,那么refresh token默认有效期是多长呢? 答案是24小时。

Simple JWT的默认设置

Simple JWT的默认设置如下所示,各属性含义参考文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
DEFAULTS = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': True,


'ALGORITHM': 'HS256',
'SIGNING_KEY': settings.SECRET_KEY,
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,


'AUTH_HEADER_TYPES': ('Bearer',),
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',


'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',


'JTI_CLAIM': 'jti',


'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}

如果要覆盖Simple JWT的默认设置,可以修改settings.py, 如下所示。下例将refresh token的有效期改为了15天。

1
2
3
4
5
6
7
8
from datetime import timedelta


SIMPLE_JWT = {
'REFRESH_TOKEN_LIFETIME': timedelta(days=15),
# 将ROTATE_REFRESH_TOKENS设为True标识刷新token时不但返回访问token还返回了刷新token
'ROTATE_REFRESH_TOKENS': True,
}

如下:

自定义令牌(token)

如果你对Simple JWT返回的access token进行解码,你会发现这个token的payload数据部分包括token类型,token失效时间,jti(一个类似随机字符串)和user_id。如果你希望在payload部分提供更多信息,比如用户的username,email等,这时你就要自定义令牌(token)了。

创建自定义令牌序列化类

该序列化器继承了TokenObtainPairSerializer类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer


class SimpleJwtTokenSerializer(TokenObtainPairSerializer):
"""自定义令牌"""

@classmethod
def get_token(cls, user):
token = super().get_token(user)
# 添加其他信息
token["username"] = user.username
token["email"] = user.email

return token

创建自定义令牌视图类

其次,不使用Simple JWT提供的默认视图,使用自定义视图

1
2
3
4
5
6
7
8
9
from rest_framework_simplejwt.views import TokenObtainPairView

from api.serializers.simplejwt import SimpleJwtTokenSerializer


class SimpleJwtTokenView(TokenObtainPairView):
"""自定义令牌视图类"""
# 修改原有的序列化类
serializer_class = SimpleJwtTokenSerializer

修改自定义令牌路由

最后,将获取刷新令牌的路由指向新的自定义的视图。

1
2
3
4
5
6
7
8
9
from rest_framework_simplejwt.views import TokenRefreshView

from api.views.simplejwt import SimpleJwtTokenView

urlpatterns = [
path("jwt_token", SimpleJwtTokenView.as_view(), name="jwt_token"),
path("jwt_token/refresh", TokenRefreshView.as_view(), name="jwt_token_refresh"),
]

postman测试

对重新获取的access token进行解码,你将看到payload部分多了username,email的内容,是不是很酷? 在实际API开发过程中,通过Json Web Token传递更多数据非常有用。

自定义认证后台(Backend)

上面的演示案例是通过用户名和密码登录的,如果我们希望后台同时支持邮箱/密码,手机/密码组合登录怎么办? 这时我们还需要自定义认证后台(Backend)。

自定义认证后台

1
2
3
4
5
6
7
8
9
10
11
12
13
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
from django.contrib.auth.models import User

class MyCustomBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = User.objects.get(Q(username=username) | Q(email=username) )
if user.check_password(password):
return user
except Exception as e:
return None

其次,修改settings.py, 把你自定义的验证方式添加到AUTHENTICATION_BACKENDS里去。

1
2
3
AUTHENTICATION_BACKENDS = (
'自定义认证类路径.MyCustomBackend',
)

修改好后,你使用postman发送邮箱/密码组合到/token/,将同样可以获得access token和refresh token。