Django REST framework API认证(包含JWT认证)
一. 背景
在咱们学习Django Rest Framework(简称DRF)时,其很是友好地给咱们提供了一个可浏览API的界面。不少测试工做均可以在可浏览API界面完成测试。要使用可浏览API界面很简单,只须要在urls.py文件中添加以下部分便可。html
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章节。python
这里要说明一个问题就是这个LoginView咱们是没法直接拿来用的,由于它须要作CSRF验证,而在先后端分离系统中不须要作CSRF验证,这里不存在站内站外的问题,自己就是跨站访问的。那么在咱们先后端分离项目中,如何作API接口的验证呢?其实framework也已经提供了多种验证方式。mysql
二. 身份验证
REST framework提供了许多开箱即用的身份验证方案,同时也容许你实施自定义方案。这里须要明确一下用户认证(Authentication)和用户受权(Authorization)是两个不一样的概念,认证解决的是“有没有”的问题,而受权解决的是“能不能”的问题。git
BasicAuthentication
该认证方案使用 HTTP Basic Authentication,并根据用户的用户名和密码进行签名。Basic Authentication 一般只适用于测试。github
SessionAuthentication
此认证方案使用 Django 的默认 session 后端进行认证。Session 身份验证适用于与您的网站在同一会话环境中运行的 AJAX 客户端。算法
TokenAuthentication
此认证方案使用简单的基于令牌的 HTTP 认证方案。令牌身份验证适用于 client-server 架构,例如本机桌面和移动客户端。sql
RemoteUserAuthentication
这种身份验证方案容许您将身份验证委托给您的 Web 服务器,该服务器设置 REMOTE_USER 环境变量。数据库
默认的认证方案可使用DEFAULT_AUTHENTICATION_CLASSES全局设置,在settings.py文件配置。在默认状况下,DRF开启了 BasicAuthentication 与 SessionAuthentication 的认证。django
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
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
4from 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
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:
1 |
for user in User.objects.filter(username='admin'): |
建立成功后,会在Token表中生成对应的Token信息。
若是你但愿每一个用户都拥有一个自动生成的令牌,则只需捕捉用户的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” 为前缀,用空格分隔两个字符串。例如: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 |
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_CLASSES1
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类的视图,在每一个视图或每一个视图集的基础上设置身份验证方案。与上面演示的 Token 认证同样,这里就不贴代码了,尽量使用基于APIView类的视图认证方式。
但使用基于APIView类的视图认证方式时,不要忘记导入类。
1 |
from rest_framework_jwt.authentication import JSONWebTokenAuthentication |
在你的urls.py文件中添加如下URL路由,以便经过POST包含用户名和密码的令牌获取。
1 |
from rest_framework_jwt.views import 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
4from 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
4from 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
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,
}