Django REST framework API认证(包含JWT认证) + 权限
一. 背景
在我们学习Django Rest Framework(简称DRF)时,其非常友好地给我们提供了一个可浏览API的界面。很多测试工作都可以在可浏览API界面完成测试。要使用可浏览API界面很简单,只需要在urls.py文件中添加如下部分即可。
1 | from django.conf.urls import include |
其中,r’^api-auth/‘部分实际上可以用任何你想使用URL替代。唯一的限制是所包含的URL必须使用’rest_framework’命名空间。在Django 1.9+中,REST framework将自动设置,所以你也无须关心。
配置完成后,如果再次打开浏览器API界面并刷新页面,你将在页面右上角看到一个“Log in”链接。这就是DRF提供的登录和登出入口,可以用来完成认证。
然后进入到’rest_framework.urls’源码,是可以看到提供了’login’和’logout’两个接口,分别用来登入和登录的。代码如下:
1 | if django.VERSION < (1, 11): |
其中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 | REST_FRAMEWORK = { |
关于DRF,几乎所有的配置都定义在MREST_FRAMEWORK变量中。另外,关于认证方式DRF默认会检测配置在DEFAULT_AUTHENTICATION_CLASSES变量中的所有认证方式,只要有一个认证方式通过即可登录成功。这里的DEFAULT_AUTHENTICATION_CLASSES与Django中的MIDDLEWARE类似,在将request通过url映射到views之前,Django和DRF都会调用定义在MREST_FRAMEWORK变量中的类的一些方法。
另外,你还可以使用基于APIView类的视图,在每个视图或每个视图集的基础上设置身份验证方案。
1 | from rest_framework.authentication import SessionAuthentication, BasicAuthentication |
需要明白的一点是,DRF的认证是在定义有权限类(permission_classes)的视图下才有作用,且权限类(permission_classes)必须要求认证用户才能访问此视图。如果没有定义权限类(permission_classes),那么也就意味着允许匿名用户的访问,自然牵涉不到认证相关的限制了。所以,一般在项目中的使用方式是在全局配置DEFAULT_AUTHENTICATION_CLASSES认证,然后会定义多个base views,根据不同的访问需求来继承不同的base views即可。
1 | from rest_framework.permissions import ( |
另外,在前后端分离项目中一般不会使用 BasicAuthentication 与 SessionAuthentication 的认证方式。所以,我们只需要关心 TokenAuthentication 认证方式即可。
三.TokenAuthentication
要使用TokenAuthentication
方案,你需要将认证类配置为包含TokenAuthentication
。
1 | REST_FRAMEWORK = { |
并在INSTALLED_APPS设置中另外包含 rest_framework.authtoken:
1 | INSTALLED_APPS = ( |
注意: rest_framework.authtoken应用一定要放到INSTALLED_APPS,并且确保在更改设置后运行
python manage.py migrate
。 rest_framework.authtoken应用需要创建一张表用来存储用户与Token的对应关系。
数据库迁移完成后,可以看到多了一个authtoken_token表,表结构如下:
1 | mysql> show create table authtoken_token\G |
其中“user_id”字段关联到了用户表。
- 配置URLconf
使用TokenAuthentication
时,你可能希望为客户提供一种机制,以获取给定用户名和密码的令牌。 REST framework 提供了一个内置的视图来支持这种行为。要使用它,请将obtain_auth_token
视图添加到您的 URLconf 中:其中,1
2
3
4from rest_framework.authtoken import views
urlpatterns += [
url(r'^api-token-auth/', views.obtain_auth_token)
]r'^api-token-auth/'
部分实际上可以用任何你想使用URL替代。 - 创建Token
你还需要为用户创建令牌,用户令牌与用户是一一对应的。如果你已经创建了一些用户,则可以为所有现有用户生成令牌,例如你也可以为某个已经存在的用户创建Token:1
2
3
4
5from 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表中生成对应的Token信息。1
2for user in User.objects.filter(username='admin'):
Token.objects.get_or_create(user=user)
如果你希望每个用户都拥有一个自动生成的令牌,则只需捕捉用户的post_save
信号即可。
1 | from django.conf import settings |
请注意,你需要确保将此代码片段放置在已安装的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 | class ObtainAuthToken(APIView): |
默认情况下,没有权限或限制应用于obtain_auth_token
视图。 如果您希望应用throttling
,则需要重写视图类,并使用throttle_classes
属性包含它们。
如果你需要自定义obtain_auth_token
视图,你可以通过继承ObtainAuthToken
视图类来实现,并在你的urls.py中使用它。例如,你可能会返回超出token值的其他用户信息:
1 | from rest_framework.authtoken.views import ObtainAuthToken |
还有urls.py:
1 | urlpatterns += [ |
- 认证Token
当我们获取到Token后,就可以拿着这个Token来认证其他API了。对于客户端进行身份验证,令牌密钥应包含在Authorization
HTTP header 中。关键字应以字符串文字 “Token” 为前缀,用空格分隔两个字符串。例如:注意: 如果你想在 header 中使用不同的关键字(例如Bearer),只需子类化1
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
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 | from rest_framework.authtoken import views |
这里虽然直接调用 views.obtain_auth_token 方法,但进入到 views.obtain_auth_token 方法后还是 DRF 模式,源码如下:
1 | obtain_auth_token = ObtainAuthToken.as_view() |
ObtainAuthToken 方法是继承 DRF 中的 APIView 的 View 类:
1 | class ObtainAuthToken(APIView): |
如果你是用 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 | class APIView(View): |
但是用户认证是在执行请求 View 之前做的,所以其实就是在 dispatch 函数之中做的,具体见源码 rest-framework/views.py 中 APIView 类中的 dispatch 函数:
1 | class APIView(View): |
这里的 self.initialize_request 也可以关注一下,因为这里的 request 对象,后面也会有调用的地方。
1 | class APIView(View): |
其中 self.get_authenticators() 方法就是用来取 self.authentication_classes 变量。
1 | class APIView(View): |
关于 authentication_classes 变量,上面已经给出了,就在 APIView 里面 authentication_classes 字段。
然后就到了认证,重点在于 self.initial(request, *args, **kwargs) 函数,对于这个函数:
1 | class APIView(View): |
这里关注 self.perform_authentication(request) 验证某个用户,其实可以看到权限检查及限流也是在这里做的。
1 | class APIView(View): |
这里 request.user 其实是一个 @property 的函数,加 @property 表示调用 user 方法的时候不需要加括号“user()”,可以直接调用 request.user 。而这里的 request 对象就是上面 initialize_request 方法返回的,其中还返回了 DRF 定义的 request 对象,在 request 对象中有被 @property 装饰的 user 方法。
1 | class Request(object): |
重点来了,到了真正认证的方法了,关注 self._authenticate()
函数即可。此方法会循环尝试每个 DRF 认证方式。
1 | class Request(object): |
那么 self.authenticators 从哪儿来的呢?就是上面展示的,在 APIVIEW 类中的 authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES 得到的。我们上面在介绍 DRF 身份验证时也说了,可以把认证类定义在全局 settings 文件中,你还可以使用基于 APIView 类的视图,在每个视图或每个视图集的基础上设置身份验证方案。如下方式:
1 | class ExampleView(APIView): |
当基于 APIView 类的视图定义验证或权限类时,相当于覆盖了原生 APIVIEW 中的相关变量,自然就使用覆盖后的变量了。authentication_classes 里面放的就是可以用来验证一个用户的类,他是一个元组,验证用户时,按照这个元组顺序,直到验证通过或者遍历整个元组还没有通过。同理 self.check_permissions(request) 是验证该用户是否具有API的使用权限。关于对view控制的其他类都在rest-framework/views.py的APIView类中定义了。
由于我们这里只是拿 TokenAuthentication 认证说明,所以忽略 BasicAuthentication 和 SessionAuthentication 这两种认证,其原理与TokenAuthentication 一样。这样,就进入到了 TokenAuthentication 认证,其源码部分如下:
1 | // 获取header部分 Authorization 标识的信息 |
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同样,你还可以使用基于APIView类的视图,在每个视图或每个视图集的基础上设置身份验证方案。与上面演示的 Token 认证一样,这里就不贴代码了,尽可能使用基于APIView类的视图认证方式。1
2
3
4
5
6
7
8
9
10REST_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类的视图认证方式时,不要忘记导入类。在你的urls.py文件中添加以下URL路由,以便通过POST包含用户名和密码的令牌获取。1
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
如果你使用用户名admin和密码admin123456创建了用户,则可以通过在终端中执行以下操作来测试JWT是否正常工作。1
2
3
4from rest_framework_jwt.views import obtain_jwt_token
urlpatterns += [
url(r'^api-token-auth/', obtain_jwt_token)
]或者,你可以使用Django REST framework支持的所有内容类型来获取身份验证令牌。例如:1
$ curl -X POST -d "username=admin&password=admin123456" http://127.0.0.1:8000/api-token-auth/
现在访问需要认证的API时,就必须要包含1
$ curl -X POST -H "Content-Type: application/json" -d '{"username":"admin","password":"admin123456"}' http://127.0.0.1:8000/api-token-auth/
Authorization: JWT <your_token>
头信息了:1
$ curl -H "Authorization: JWT <your_token>" http://127.0.0.1:8000/virtual/
- 刷新Token
如果JWT_ALLOW_REFRESH为True,可以“刷新”未过期的令牌以获得具有更新到期时间的全新令牌。像如下这样添加一个URL模式:使用方式就是将现有令牌传递到刷新API,如下所示:1
2
3
4from rest_framework_jwt.views import refresh_jwt_token
urlpatterns += [
url(r'^api-token-refresh/', refresh_jwt_token)
]{"token": EXISTING_TOKEN}
。请注意,只有非过期的令牌才有效。另外,响应JSON看起来与正常获取令牌端点{"token": NEW_TOKEN}
相同。可以重复使用令牌刷新(token1 -> token2 -> token3),但此令牌链存储原始令牌(使用用户名/密码凭据获取)的时间。作为orig_iat,你只能将刷新令牌保留至JWT_REFRESH_EXPIRATION_DELTA。1
$ curl -X POST -H "Content-Type: application/json" -d '{"token":"<EXISTING_TOKEN>"}' http://localhost:8000/api-token-refresh/
刷新token以获得新的token的作用在于,持续保持活跃用户登录状态。比如通过用户密码获得的token有效时间为1小时,那么也就意味着1小时后此token失效,用户必须得重新登录,这对于活跃用户来说其实是多余的。如果这个用户在这1小时内都在浏览网站,我们不应该让用户重新登录,就是在token没有失效之前调用刷新接口为用户获得新的token。 - 认证Token
在一些微服务架构中,身份验证由单个服务处理。此服务负责其他服务委派确认用户已登录此身份验证服务的责任。这通常意味着其他服务将从用户接收JWT传递给身份验证服务,并在将受保护资源返回给用户之前等待JWT有效的确认。添加以下URL模式:将Token传递给验证API,如果令牌有效,则返回令牌,返回状态码为200。否则,它将返回400 Bad Request以及识别令牌无效的错误。1
2
3
4from rest_framework_jwt.views import verify_jwt_token
urlpatterns += [
url(r'^api-token-verify/', verify_jwt_token)
]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
7from 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
39JWT_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,
}
JWT唯一的一个不算缺点的缺点就是Token太长了,180位。
refs
权限相关参考
Django REST framework的各种技巧【目录索引】 - 后端开发那点事儿 - SegmentFault 思否
Django REST framework的各种技巧——3.权限 - 后端开发那点事儿 - SegmentFault 思否
django权限管理(Permission)-内置权限实现
django viewset