Django REST framework API认证(包括JWT认证)

 

Django REST framework API认证(包含JWT认证) + 权限

Django REST framework API认证(包含JWT认证)

一. 背景

在我们学习Django Rest Framework(简称DRF)时,其非常友好地给我们提供了一个可浏览API的界面。很多测试工作都可以在可浏览API界面完成测试。要使用可浏览API界面很简单,只需要在urls.py文件中添加如下部分即可。

1
2
3
4
from django.conf.urls import include
urlpatterns += [
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]

 

其中,r'^api-auth/'部分实际上可以用任何你想使用URL替代,唯一的限制是所包含的URL必须使用'rest_framework'命名空间。在Django 1.9+中,REST framework将自动设置,所以你也无须关心。
配置完成后,如果再次打开浏览器API界面并刷新页面,你将在页面右上角看到一个”Log in”链接。这就是DRF提供的登录和登出入口,可以用来完成认证。
然后进入到’rest_framework.urls’源码,是可以看到提供了’login’和’logout’两个接口,分别用来登入和登录的。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if django.VERSION < (1, 11):
login = views.login
login_kwargs = {'template_name': 'rest_framework/login.html'}
logout = views.logout
else:
login = views.LoginView.as_view(template_name='rest_framework/login.html')
login_kwargs = {}
logout = views.LogoutView.as_view()


app_name = 'rest_framework'
urlpatterns = [
url(r'^login/$', login, login_kwargs, name='login'),
url(r'^logout/$', logout, name='logout'),
]

 

其中login接口调用LoginView视图,logout接口调用LogoutView视图。这两个视图都是django.contrib.auth应用提供的。在LogoutView视图中,有这么一个装饰器@method_decorator(csrf_protect),是用来做CSRF code验证的,就是做表单安全验证的,防止跨站攻击。而这个CSRF code是在返回HTML页面的时候Django会自动注册这么一个CSRF code方法,而在template中会自动调用这个方法生成code值。在前端页面元素form部分,可以查看到name=”csrfmiddlewaretoken”标识,且在Django返回的 HTTP 响应的 cookie 里,Django 会为你添加一个csrftoken 字段,其值为一个自动生成的token。这就是用来做表单安全验证的,具体关于CSRF原理见Django章节。

这里要说明一个问题就是这个LoginView我们是无法直接拿来用的,因为它需要做CSRF验证,而在前后端分离系统中不需要做CSRF验证,这里不存在站内站外的问题,本身就是跨站访问的。那么在我们前后端分离项目中,如何做API接口的验证呢?其实framework也已经提供了多种验证方式。

二. 身份验证

REST framework提供了许多开箱即用的身份验证方案,同时也允许你实施自定义方案。这里需要明确一下用户认证(Authentication)和用户授权(Authorization)是两个不同的概念,认证解决的是“有没有”的问题,而授权解决的是“能不能”的问题。

BasicAuthentication
该认证方案使用 HTTP Basic Authentication,并根据用户的用户名和密码进行签名。Basic Authentication 通常只适用于测试。

SessionAuthentication
此认证方案使用 Django 的默认 session 后端进行认证。Session 身份验证适用于与您的网站在同一会话环境中运行的 AJAX 客户端。

TokenAuthentication
此认证方案使用简单的基于令牌的 HTTP 认证方案。令牌身份验证适用于 client-server 架构,例如本机桌面和移动客户端。

RemoteUserAuthentication
这种身份验证方案允许您将身份验证委托给您的 Web 服务器,该服务器设置 REMOTE_USER 环境变量。

默认的认证方案可以使用DEFAULT_AUTHENTICATION_CLASSES全局设置,在settings.py文件配置。在默认情况下,DRF开启了 BasicAuthentication 与 SessionAuthentication 的认证。

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

 

关于DRF,几乎所有的配置都定义在MREST_FRAMEWORK变量中。另外,关于认证方式DRF默认会检测配置在DEFAULT_AUTHENTICATION_CLASSES变量中的所有认证方式,只要有一个认证方式通过即可登录成功。这里的DEFAULT_AUTHENTICATION_CLASSES与Django中的MIDDLEWARE类似,在将request通过url映射到views之前,Django和DRF都会调用定义在MREST_FRAMEWORK变量中的类的一些方法。
另外,你还可以使用基于APIView类的视图,在每个视图或每个视图集的基础上设置身份验证方案。

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` instance.
'auth': unicode(request.auth), # None
}
return Response(content)

 

需要明白的一点是,DRF的认证是在定义有权限类(permission_classes)的视图下才有作用,且权限类(permission_classes)必须要求认证用户才能访问此视图。如果没有定义权限类(permission_classes),那么也就意味着允许匿名用户的访问,自然牵涉不到认证相关的限制了。所以,一般在项目中的使用方式是在全局配置DEFAULT_AUTHENTICATION_CLASSES认证,然后会定义多个base views,根据不同的访问需求来继承不同的base views即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from rest_framework.permissions import (
IsAuthenticated,
IsAdminUser,
IsAuthenticatedOrReadOnly
)

class BaseView(APIView):
'''普通用户'''
permission_classes = (
IsOwnerOrReadOnly,
IsAuthenticated
)


class SuperUserpermissions(APIView):
'''超级用户'''
permission_classes = (IsAdminUser,)


class NotLogin(APIView):
'''匿名用户'''
pass

 

另外,在前后端分离项目中一般不会使用 BasicAuthentication 与 SessionAuthentication 的认证方式。所以,我们只需要关心 TokenAuthentication 认证方式即可。

三.TokenAuthentication

要使用TokenAuthentication方案,你需要将认证类配置为包含TokenAuthentication

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

并在INSTALLED_APPS设置中另外包含 rest_framework.authtoken:

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

 

注意: rest_framework.authtoken应用一定要放到INSTALLED_APPS,并且确保在更改设置后运行python manage.py migrate。 rest_framework.authtoken应用需要创建一张表用来存储用户与Token的对应关系。
数据库迁移完成后,可以看到多了一个authtoken_token表,表结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
mysql> show create table authtoken_token\G
*************************** 1. row ***************************
Table: authtoken_token
Create Table: CREATE TABLE `authtoken_token` (
`key` varchar(40) NOT NULL,
`created` datetime(6) NOT NULL,
`user_id` int(11) NOT NULL,
PRIMARY KEY (`key`),
UNIQUE KEY `user_id` (`user_id`),
CONSTRAINT `authtoken_token_user_id_35299eff_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.01 sec)

 

其中“user_id”字段关联到了用户表。

  • 配置URLconf
    使用TokenAuthentication时,你可能希望为客户提供一种机制,以获取给定用户名和密码的令牌。 REST framework 提供了一个内置的视图来支持这种行为。要使用它,请将obtain_auth_token视图添加到您的 URLconf 中:
    1
    2
    3
    4
    from rest_framework.authtoken import views
    urlpatterns += [
    url(r'^api-token-auth/', views.obtain_auth_token)
    ]

其中,r'^api-token-auth/'部分实际上可以用任何你想使用URL替代。

  • 创建Token
    你还需要为用户创建令牌,用户令牌与用户是一一对应的。如果你已经创建了一些用户,则可以为所有现有用户生成令牌,例如
    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)

你也可以为某个已经存在的用户创建Token:

1
2
for user in User.objects.filter(username='admin'):
Token.objects.get_or_create(user=user)

 

创建成功后,会在Token表中生成对应的Token信息。

如果你希望每个用户都拥有一个自动生成的令牌,则只需捕捉用户的post_save信号即可。

1
2
3
4
5
6
7
8
9
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token

@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)

 

请注意,你需要确保将此代码片段放置在已安装的models.py模块或 Django 启动时将导入的其他某个位置。

  • 获取Token
    上面虽然介绍了多种创建Token的方式,其实我们最简单的就是只需要配置一下urls.py,然后就可以通过暴露的API来获取Token了。当使用表单数据或 JSON 将有效的username和password字段发布到视图时,obtain_auth_token视图将返回 JSON 响应:
    1
    2
    $ curl -d "username=admin&password=admin123456" http://127.0.0.1:8000/api-token-auth/
    {"token":"684b41712e8e38549504776613bd5612ba997616"}

请注意,缺省的obtain_auth_token视图显式使用 JSON 请求和响应,而不是使用你设置的默认的渲染器和解析器类。

当我们正常获取到Token后,obtain_auth_token视图会自动帮我们在Token表中创建对应的Token。源码如下:

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
31
32
33
34
35
36
37
38
39
40
41
class ObtainAuthToken(APIView):
throttle_classes = ()
permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,)
serializer_class = AuthTokenSerializer
if coreapi is not None and coreschema is not None:
schema = ManualSchema(
fields=[
coreapi.Field(
name="username",
required=True,
location='form',
schema=coreschema.String(
title="Username",
description="Valid username for authentication",
),
),
coreapi.Field(
name="password",
required=True,
location='form',
schema=coreschema.String(
title="Password",
description="Valid password for authentication",
),
),
],
encoding="application/json",
)

def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data,
context={'request': request})
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})


obtain_auth_token = ObtainAuthToken.as_view()

 

默认情况下,没有权限或限制应用于obtain_auth_token视图。 如果您希望应用throttling,则需要重写视图类,并使用throttle_classes属性包含它们。

如果你需要自定义obtain_auth_token视图,你可以通过继承ObtainAuthToken视图类来实现,并在你的urls.py中使用它。例如,你可能会返回超出token值的其他用户信息:

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

class CustomAuthToken(ObtainAuthToken):

def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data,
context={'request': request})
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,
'user_id': user.pk,
'email': user.email
})

 

还有urls.py:

1
2
3
urlpatterns += [
url(r'^api-token-auth/', CustomAuthToken.as_view())
]

 

  • 认证Token
    当我们获取到Token后,就可以拿着这个Token来认证其他API了。对于客户端进行身份验证,令牌密钥应包含在 Authorization HTTP header 中。关键字应以字符串文字 “Token” 为前缀,用空格分隔两个字符串。例如:
    1
    Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b

注意: 如果你想在 header 中使用不同的关键字(例如Bearer),只需子类化TokenAuthentication并设置keyword类变量。
如果成功通过身份验证,TokenAuthentication将提供以下凭据。

request.user是一个User实例,包含了用户名及相关信息。
request.auth是一个rest_framework.authtoken.models.Token实例。
未经身份验证的响应被拒绝将导致HTTP 401 Unauthorized的响应和相应的 WWW-Authenticate header。例如:

1
WWW-Authenticate: Token

 

测试令牌认证的API,例如:

1
$ curl -X GET -H 'Authorization: Token 684b41712e8e38549504776613bd5612ba997616' http://127.0.0.1:8000/virtual/

 

注意: 如果您在生产中使用TokenAuthentication,则必须确保您的 API 只能通过https访问。

四. 认证源码

使用 TokenAuthentication 认证方式,当认证成功后,在 request 中将提供了 request.user 和 request.auth 实例。其中 request.user 实例中有用户信息,比如用户名及用户ID,而 request.auth 实例中有Token信息。那么DRF是如何把 Token 转换为用户信息呢?通过下面的源码部分就可以看到它们是如何转换的。

基于 DRF 的请求处理,与常规的 url 配置不同,通常一个 Django 的 url 请求对应一个视图函数,在使用 DRF 时,我们要基于视图对象,然后调用视图对象的 as_view 函数,as_view 函数中会调用 rest_framework/views.py 中的 dispatch 函数,这个函数会根据 request 请求方法,去调用我们在 view 对象中定义的对应的方法,就像这样:

1
2
3
4
from rest_framework.authtoken import views
urlpatterns += [
url(r'^api-token-auth/', views.obtain_auth_token)
]

 

这里虽然直接调用 views.obtain_auth_token 方法,但进入到 views.obtain_auth_token 方法后还是 DRF 模式,源码如下:

1
obtain_auth_token = ObtainAuthToken.as_view()

 

ObtainAuthToken 方法是继承 DRF 中的 APIView 的 View 类:

1
2
3
4
5
6
7
class ObtainAuthToken(APIView):
throttle_classes = ()
permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,)
serializer_class = AuthTokenSerializer
....

 

如果你是用 POST 方法请求 ObtainAuthToken,那么 as_view() 函数会调用 dispatch 函数,dispatch 根据 request.METHOD,这里是 POST,去调用 ObtainAuthToken 类的 POST 方法,这就跟通常的 url->view 的流程一样了。

这里需要注意的一点就是,DRF 中的 APIVIEW 是继承 Django View 的,重写了部分 as_view 方法,而调用 dispatch 函数是在 Django View 的 as_view 方法中做的事情,源码部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class APIView(View):
....
@classmethod
def as_view(cls, **initkwargs):
"""
Store the original class on the view function.

This allows us to discover information about the view when we do URL
reverse lookups. Used for breadcrumb generation.
"""
if isinstance(getattr(cls, 'queryset', None), models.query.QuerySet):
def force_evaluation():
raise RuntimeError(
'Do not evaluate the `.queryset` attribute directly, '
'as the result will be cached and reused between requests. '
'Use `.all()` or call `.get_queryset()` instead.'
)
cls.queryset._fetch_all = force_evaluation

view = super(APIView, cls).as_view(**initkwargs)
view.cls = cls
view.initkwargs = initkwargs

 

但是用户认证是在执行请求 View 之前做的,所以其实就是在 dispatch 函数之中做的,具体见源码 rest-framework/views.py 中 APIView 类中的 dispatch 函数:

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
31
32
33
34
35
36
37
38
class APIView(View):
// 从settings文件中获取认证类、限流类、权限类
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
....

def dispatch(self, request, *args, **kwargs):
"""
`.dispatch()` is pretty much the same as Django's regular dispatch,
but with extra hooks for startup, finalize, and exception handling.
"""
self.args = args
self.kwargs = kwargs

// 封装request,对原始request对象增加一些功能,比如认证类,都是在initialize_request方法中完成
request = self.initialize_request(request, *args, **kwargs)
self.request = request
self.headers = self.default_response_headers # deprecate?

try:
// 调用self.initial进行用户认证
self.initial(request, *args, **kwargs)

# Get the appropriate handler method
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(),
self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed

response = handler(request, *args, **kwargs)

except Exception as exc:
response = self.handle_exception(exc)

self.response = self.finalize_response(request, response, *args, **kwargs)
return self.response

 

这里的 self.initialize_request 也可以关注一下,因为这里的 request 对象,后面也会有调用的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class APIView(View):
....
def initialize_request(self, request, *args, **kwargs):
"""
Returns the initial request object.
"""
parser_context = self.get_parser_context(request)

return Request(
request,
parsers=self.get_parsers(),
authenticators=self.get_authenticators(), // 这里把认证类封装进行了request里面
negotiator=self.get_content_negotiator(),
parser_context=parser_context
)

 

其中 self.get_authenticators() 方法就是用来取 self.authentication_classes 变量。

1
2
3
4
5
6
7
class APIView(View):
....
def get_authenticators(self):
"""
Instantiates and returns the list of authenticators that this view can use.
"""
return [auth() for auth in self.authentication_classes]

 

关于 authentication_classes 变量,上面已经给出了,就在 APIView 里面 authentication_classes 字段。

然后就到了认证,重点在于 self.initial(request, *args, **kwargs) 函数,对于这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class APIView(View):
....
def initial(self, request, *args, **kwargs):
"""
Runs anything that needs to occur prior to calling the method handler.
"""
self.format_kwarg = self.get_format_suffix(**kwargs)

# Perform content negotiation and store the accepted info on the request
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg

# Determine the API version, if versioning is in use.
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme

# Ensure that the incoming request is permitted
self.perform_authentication(request) // 用户认证
self.check_permissions(request) // 权限检查
self.check_throttles(request) // 限流检查

 

这里关注 self.perform_authentication(request) 验证某个用户,其实可以看到权限检查及限流也是在这里做的。

1
2
3
4
5
6
7
8
9
10
11
class APIView(View):
....
def perform_authentication(self, request):
"""
Perform authentication on the incoming request.

Note that if you override this and simply 'pass', then authentication
will instead be performed lazily, the first time either
`request.user` or `request.auth` is accessed.
"""
request.user

 

这里 request.user 其实是一个 @property 的函数,加 @property 表示调用 user 方法的时候不需要加括号“user()”,可以直接调用 request.user 。而这里的 request 对象就是上面 initialize_request 方法返回的,其中还返回了 DRF 定义的 request 对象,在 request 对象中有被 @property 装饰的 user 方法。

1
2
3
4
5
6
7
8
9
10
11
12
class Request(object):
....
@property
def user(self):
"""
Returns the user associated with the current request, as authenticated
by the authentication classes provided to the request.
"""
if not hasattr(self, '_user'):
with wrap_attributeerrors():
self._authenticate()
return self._user

 

重点来了,到了真正认证的方法了,关注 self._authenticate()函数即可。此方法会循环尝试每个 DRF 认证方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Request(object):
....
def _authenticate(self):
"""
尝试使用每个身份验证实例验证请求
self.authenticators = [BasicAuthentication, SessionAuthentication, TokenAuthentication]
"""
for authenticator in self.authenticators:
try:
user_auth_tuple = authenticator.authenticate(self)
except exceptions.APIException:
// 如果authenticate方法抛出异常,则执行self._not_authenticated方法,相当于匿名用户,没有通过认证
self._not_authenticated()
raise

if user_auth_tuple is not None:
self._authenticator = authenticator
self.user, self.auth = user_auth_tuple
return

// 如果没有设置认证类的话,也相当于匿名用户,没有通过认证
self._not_authenticated()

 

那么 self.authenticators 从哪儿来的呢?就是上面展示的,在 APIVIEW 类中的 authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES 得到的。我们上面在介绍 DRF 身份验证时也说了,可以把认证类定义在全局 settings 文件中,你还可以使用基于 APIView 类的视图,在每个视图或每个视图集的基础上设置身份验证方案。如下方式:

1
2
3
class ExampleView(APIView):
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated,)

 

当基于 APIView 类的视图定义验证或权限类时,相当于覆盖了原生 APIVIEW 中的相关变量,自然就使用覆盖后的变量了。authentication_classes 里面放的就是可以用来验证一个用户的类,他是一个元组,验证用户时,按照这个元组顺序,直到验证通过或者遍历整个元组还没有通过。同理 self.check_permissions(request) 是验证该用户是否具有API的使用权限。关于对view控制的其他类都在rest-framework/views.py的APIView类中定义了。

由于我们这里只是拿 TokenAuthentication 认证说明,所以忽略 BasicAuthentication 和 SessionAuthentication 这两种认证,其原理与TokenAuthentication 一样。这样,就进入到了 TokenAuthentication 认证,其源码部分如下:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// 获取header部分 Authorization 标识的信息
def get_authorization_header(request):
"""
Return request's 'Authorization:' header, as a bytestring.

Hide some test client ickyness where the header can be unicode.
"""
auth = request.META.get('HTTP_AUTHORIZATION', b'')
if isinstance(auth, text_type):
# Work around django test client oddness
auth = auth.encode(HTTP_HEADER_ENCODING)
return auth


// 解析并认证 Token
class TokenAuthentication(BaseAuthentication):
"""
Simple token based authentication.

Clients should authenticate by passing the token key in the "Authorization"
HTTP header, prepended with the string "Token ". For example:

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):
// 通过上面的get_authorization_header方法得到Token信息
// auth = [b'Token', b'684b41712e8e38549504776613bd5612ba997616']
auth = get_authorization_header(request).split()

// 通过获取Token关键字,并与keyword变量比对,来判断是否是Token方式认证
if not auth or auth[0].lower() != self.keyword.lower().encode():
return None

// auth长度等于2时才是合法值,继续往下进行
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)

// 进行token解码,从bytes编码格式转为字符串
try:
token = auth[1].decode()
except UnicodeError:
msg = _('Invalid token header. Token string should not contain invalid characters.')
raise exceptions.AuthenticationFailed(msg)

// 把转换过的token传给认证凭证方法进行验证
return self.authenticate_credentials(token)

// 验证凭证方法进行接收token并进行验证
def authenticate_credentials(self, key):
// 获取Token模型实例
model = self.get_model()
try:
// 使用select_related方法获取相应外键对应的对象(就是两表Join),然后进行Token过滤查询
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.'))

// 返回user实例,及token实例
return (token.user, token)

 

PS:DRF自带的TokenAuthentication认证方式也非常简单,同时弊端也很大,真正项目中用的较少。由于需要存储在数据库表中,它在分布式系统中用起来较为麻烦,并且每次都需要查询数据库,增加数据库压力;同时它不支持Token的过期设置,这是一个很大的问题。在实际前后端分离项目中使用JWT(Json Web Token)标准的认证方式较多,每个语言都有各自实现JWT的方式,Python也不例外。

五. JWT认证

了解完DRF自带的TokenAuthentication认证方式的弊端之后,再来看JWT(Json Web Token)认证方式。它们两个的原理是一样的,就是认证用户Token,然后取出对应的用户。但JWT解决了两个较大的问题。

第一,是不需要把Token存储到数据库表中了,而是根据一定的算法来算出用户Token,然后每次用户来验证时再以同样的方式生成对应的Token进行校验。当然,实际JWT生成Token的方式还是较为复杂的,具体可以看JWT协议相关文章。

第二,JWT对于生成的Token可以设置过期时间,从而在一定程度提高了Token的安全性。

JWT的原理还是稍稍有点麻烦的,里面涉及了一些对称加密和非对称加密的算法。但是JWT使用起来确是非常简单,Python中有PyJWT库,而在DRF中也有对应的开源项目django-rest-framework-jwt

  • 安装
    直接使用pip安装即可,目前支持Python、Django、DRF主流版本:

    1
    $ pip install djangorestframework-jwt
  • 使用
    在settings.py文件中,将JSONWebTokenAuthentication 添加到REST framework框架的DEFAULT_AUTHENTICATION_CLASSES

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
    'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
    'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    'rest_framework.authentication.SessionAuthentication',
    'rest_framework.authentication.BasicAuthentication',
    ),
    }

同样,你还可以使用基于APIView类的视图,在每个视图或每个视图集的基础上设置身份验证方案。与上面演示的 Token 认证一样,这里就不贴代码了,尽可能使用基于APIView类的视图认证方式。
但使用基于APIView类的视图认证方式时,不要忘记导入类。

1
from rest_framework_jwt.authentication import JSONWebTokenAuthentication

 

在你的urls.py文件中添加以下URL路由,以便通过POST包含用户名和密码的令牌获取。

1
2
3
4
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns += [
url(r'^api-token-auth/', obtain_jwt_token)
]

 

如果你使用用户名admin和密码admin123456创建了用户,则可以通过在终端中执行以下操作来测试JWT是否正常工作。

1
$ curl -X POST -d "username=admin&password=admin123456" http://127.0.0.1:8000/api-token-auth/

 

或者,你可以使用Django REST framework支持的所有内容类型来获取身份验证令牌。例如:

1
$ curl -X POST -H "Content-Type: application/json" -d '{"username":"admin","password":"admin123456"}' http://127.0.0.1:8000/api-token-auth/

 

现在访问需要认证的API时,就必须要包含Authorization: JWT <your_token>头信息了:

1
$ curl -H "Authorization: JWT <your_token>" http://127.0.0.1:8000/virtual/

 

  • 刷新Token
    如果JWT_ALLOW_REFRESH为True,可以“刷新”未过期的令牌以获得具有更新到期时间的全新令牌。像如下这样添加一个URL模式:
    1
    2
    3
    4
    from rest_framework_jwt.views import refresh_jwt_token
    urlpatterns += [
    url(r'^api-token-refresh/', refresh_jwt_token)
    ]

使用方式就是将现有令牌传递到刷新API,如下所示: {"token": EXISTING_TOKEN}。请注意,只有非过期的令牌才有效。另外,响应JSON看起来与正常获取令牌端点{"token": NEW_TOKEN}相同。

1
$ curl -X POST -H "Content-Type: application/json" -d '{"token":"<EXISTING_TOKEN>"}' http://localhost:8000/api-token-refresh/

 

可以重复使用令牌刷新(token1 -> token2 -> token3),但此令牌链存储原始令牌(使用用户名/密码凭据获取)的时间。作为orig_iat,你只能将刷新令牌保留至JWT_REFRESH_EXPIRATION_DELTA。
刷新token以获得新的token的作用在于,持续保持活跃用户登录状态。比如通过用户密码获得的token有效时间为1小时,那么也就意味着1小时后此token失效,用户必须得重新登录,这对于活跃用户来说其实是多余的。如果这个用户在这1小时内都在浏览网站,我们不应该让用户重新登录,就是在token没有失效之前调用刷新接口为用户获得新的token。

  • 认证Token
    在一些微服务架构中,身份验证由单个服务处理。此服务负责其他服务委派确认用户已登录此身份验证服务的责任。这通常意味着其他服务将从用户接收JWT传递给身份验证服务,并在将受保护资源返回给用户之前等待JWT有效的确认。添加以下URL模式:
    1
    2
    3
    4
    from rest_framework_jwt.views import verify_jwt_token
    urlpatterns += [
    url(r'^api-token-verify/', verify_jwt_token)
    ]

将Token传递给验证API,如果令牌有效,则返回令牌,返回状态码为200。否则,它将返回400 Bad Request以及识别令牌无效的错误。

1
$ curl -X POST -H "Content-Type: application/json" -d '{"token":"<EXISTING_TOKEN>"}' http://localhost:8000/api-token-verify/

 

  • 手动创建Token
    有时候你可能希望手动生成令牌,例如在创建帐户后立即将令牌返回给用户。或者,你需要返回的信息不止是Token,可能还有用户权限相关值。你可以这样做:

    1
    2
    3
    4
    5
    6
    7
    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)
  • 其他设置
    你可以覆盖一些其他设置,比如变更Token过期时间,以下是所有可用设置的默认值。在settings.py文件中设置。

    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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    JWT_AUTH = {
    'JWT_ENCODE_HANDLER':
    'rest_framework_jwt.utils.jwt_encode_handler',

    'JWT_DECODE_HANDLER':
    'rest_framework_jwt.utils.jwt_decode_handler',

    'JWT_PAYLOAD_HANDLER':
    'rest_framework_jwt.utils.jwt_payload_handler',

    'JWT_PAYLOAD_GET_USER_ID_HANDLER':
    'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',

    'JWT_RESPONSE_PAYLOAD_HANDLER':
    'rest_framework_jwt.utils.jwt_response_payload_handler',

    // 这是用于签署JWT的密钥,确保这是安全的,不共享不公开的
    'JWT_SECRET_KEY': settings.SECRET_KEY,
    'JWT_GET_USER_SECRET_KEY': None,
    'JWT_PUBLIC_KEY': None,
    'JWT_PRIVATE_KEY': None,
    'JWT_ALGORITHM': 'HS256',
    // 如果秘钥是错误的,它会引发一个jwt.DecodeError
    'JWT_VERIFY': True,
    'JWT_VERIFY_EXPIRATION': True,
    'JWT_LEEWAY': 0,
    // Token过期时间设置
    'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
    'JWT_AUDIENCE': None,
    'JWT_ISSUER': None,

    // 是否开启允许Token刷新服务,及限制Token刷新间隔时间,从原始Token获取开始计算
    'JWT_ALLOW_REFRESH': False,
    'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),

    // 定义与令牌一起发送的Authorization标头值前缀
    'JWT_AUTH_HEADER_PREFIX': 'JWT',
    'JWT_AUTH_COOKIE': None,
    }
posted @ 2019-11-08 15:34  一米八大高个儿  阅读(891)  评论(0编辑  收藏  举报