在之前的 Django REST framework的token原理和验证登录 中,使用了drf自带的Token认证类rest_framework.authentication.TokenAuthentication
实现了Token认证,但是drf自带的Token认证有以下几个问题:
- drf自带的token验证方式,token值是存储在生成的
authtoken_token
表中,是放在一台服务器上的,若采用分布式的话,还需要做数据的同步操作。 - 相比于 django_session 的设置,没有expire_date属性(过期时间),将是永久有效的,这样的话,一旦泄露将造成很大的安全问题。
- 和django_session存储同样的问题,随着用户的增多,token值会占用服务器大量空间,同时也会加大数据库的查询压力,导致性能下降。
为了解决以上的问题,将使用第三方的开源库djangorestframework-jwt完成这一功能。
¶JWT原理
参考文章:前后端分离之JWT用户认证
¶摘要
- HTTP协议的无状态存储,需要提供用户登录的验证功能。
- 前后端分离系统的传统解决方式是:前端登录,后端根据用户信息生成一个
token
,并保存这个token
和对应的用户id到数据库或Session中,接着把token
传给用户,存入浏览器 cookie,之后浏览器请求带上这个cookie,后端根据这个cookie值来查询用户,验证是否过期。
JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:
- 简洁(Compact)
可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快 - 自包含(Self-contained)
负载中包含了所有用户所需要的信息,避免了多次查询数据库
¶组成
JWT由三部分组成
- HEAD 头部: 包含了两部分,token 类型和采用的加密算法
- Payload 负载: 我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,常用的由 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)。默认情况下负载中的内容使用的是 base64 编码方式,是可逆的,所以任何人都可以解读该部分的内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。
- Signature 签名: 保证内容没有被篡改。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥(存放在服务端),然后使用 header 中指定的签名算法(HS256)进行签名。生成的方式:
1 | # python实现 |
完整的JWT构成:base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + Signature 签名
¶验证过程
由前端和后端共同完成
- 前端进行登录,提供账号和密码在后端进行验证,后端对其验证通过后生成JWT,再发送给前端,前端保存在本地(一般是存储在本地的cookie中(推荐使用该种方式,只是使用cookie的存储机制),或者是保存在localStorage或sessionStorage上,退出登录时由前端删除保存的JWT即可)
- 前端在跳转到登录页或者请求API的时候,会发送JWT,到达后端时被过滤器拦截进行JWT的验证(在Django可以交给中间件完成),验证通过之后请求被传送给view,未通过,返回错误信息(可以交给Django中间件中的process_exception函数处理),跳转到登录页面。(前面的所有的Django中间件的功能都将由即将使用到的
rest_framework_jwt.authentication.JSONWebTokenAuthentication
来完成)
¶JWT的优势
- 完全使用算法进行验证,加密解密取得用户ID(相较于sessionid,后者还多了向服务器请求对应session_data的步骤),通过用户ID查询到用户信息,这意味着不需要存储token表,也不需要Django之中的session表,会减少服务器的存储压力
- 适用于单点登录
JWT详细解读:https://www.cnblogs.com/DeadBoy/p/11481146.html
¶Django REST framework实现JWT的验证登录功能
使用第三方库djangorestframework-jwt
完成验证,使用方法详见官方文档
¶安装后的配置过程
- 安装完成后,首先需要在REST_FRAMEWORK的
DEFAULT_AUTHENTICATION_CLASSES
中配置
1 | REST_FRAMEWORK = { |
和drf自带的token功能使用的rest_framework.authentication.TokenAuthentication
作用一致,是对用户post过来的token进行验证,验证完成后返回user。该设置只有在需要用户访问view,发送token进行验证的时候才需要设置,单纯的提供token值是不需要设置的(只需要完成步骤2)。
另外,这个值,最好不要在settings.py中进行设置,而只在需要使用的view中进行设置,因为有些页面属于公共资源,不需要验证token也可以进行访问。通过设置authentication_classes
参数进行配置:
1 | # views.py |
- 在urls.py中添加url
1 | from rest_framework_jwt.views import obtain_jwt_token |
此时访问http://xxx/jwt-auth
,使用post请求传递用户信息(用户名和密码),验证通过之后就会直接返回一个jwt,采用的返回方式是k/v形式的json对象:
1 | { |
当传递的用于验证用户的信息(username和password)错误的时候,返回以下JSON数据:
1 | { |
- 验证方式(注意和drf自带的token验证方式的不同书写)
- 在http Headers中通过
Authorization: JWT <your_token>
格式传递获得的JWT - 打断点,请求设置了
authentication_classes = (JSONWebTokenAuthentication,)
的view,查看request属性,可以发现用户存储在request中的user属性中,JWT值存储在auth属性中 - JWT过期之后,返回的JSON格式数据
1
2
3
4
5{
"non_field_errors": [
"无法使用提供的认证信息登录。"
]
} - 在http Headers中通过
¶和前端vue登录功能的集成
直接将url(r'^api-token-auth/', obtain_jwt_token)
的url地址改为url(r'^login/', obtain_jwt_token)
,变成登陆接口,前端向后端传递用户名和密码请求登陆,后端接受到请求后验证,验证通过返回JWT,前端保存,之后的每次请求,发送该JWT,验证用户,完成登录。
详细过程:
后端验证用户信息返回JWT:
url 请求的 view 是 rest_framework_jwt 中的 ObtainJSONWebToken,该 view 继承自 JSONWebTokenAPIView,使用的序列化类是 JSONWebTokenSerializer,该序列化类中通过重写 validate() 方法,进行用户的身份校验,验证通过后进行 token 的构建,并和用户名组合为字典存放到 _validated_data
参数中。
1 | # 源码位于 rest_framework_jwt/serializers.py |
JSONWebTokenAPIView 中获取到生成的 token,通过可选的方式(通过判断 api_settings.JWT_AUTH_COOKIE 的值决定是否通过 cookie 的方式进行传递 token 值,默认是 None,也就是不通过 cookie 进行传递值)
1 | # 源码位于 rest_framework_jwt/views.py |
vue前端获得返回的JWT:
1 | // 登录 |
本例中使用的是非cookie传递的方式,后端直接将JWT通过Response进行返回,然后在Vue中通过response.data读取属性的方式获取到JWT并存储在cookie中,这里的cookie只起到了数据存储的作用,vue中之后会使用 cookie.getCookie(‘token’) 的方式读取数据使用。
vue 传递 token 值给后端,用于身份验证:
根据 djangorestframework_jwt 文档要求,需要以 Authorization: JWT <your_token>
的形式进行传递,所以在前端设置 request拦截器,为 request 请求添加该信息
1 | // vue设置http request拦截器 |
通过这种方式向后端传递JWT,
后端通过设置 authentication_classes = (JSONWebTokenAuthentication,) 进行JWT认证
JSONWebTokenAuthentication 继承自 BaseJSONWebTokenAuthentication,该父类中提供了 authenticate()
方法用于验证用户,内部通过 get_jwt_value(request)
方法获得前端传递过来的token,内部通过调用 jwt_decode_handler(jwt_value)
反解 token 获得其中的 payload(该操作与之前的 jwt_encode_handler(payload)
相对),再通过 authenticate_credentials(payload)
获得user。
1 | # 位于 rest_framework/authentication.py |
¶可以定制的方法
jwt_response_payload_handler(token, user, request)
1 | def jwt_response_payload_handler(token, user=None, request=None): |
可以通过在settings.py中配置,用于定制返回的内容,默认只返回了token值。
1 | JWT_AUTH = { |
¶djangorestframework_jwt 配置
提供配置文件的方式,方便定制JWT的某些功能,除了上面的指定方法外,常用的配置还有以下几个:
1 | import datetime |
默认的 JWT_AUTH_HEADER_PREFIX
就是设置的 JWT
前缀,该设置在后端传递给前端token时并不其作用,而是在前端传递给后端的时候用到,用于在后端读取前端设置在 headers.Authorization 中的 Authorization: JWT 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
时,验证前缀是否为settings.py中设置的前缀,所以该参数的设置只在后台验证的时候才会进行。
¶token功能的项目代码内嵌
有这样一种功能需求,我们在系统的注册功能中,当我们注册之后应该直接完成登录,显示登录状态,可以发现这部分的前端处理逻辑和登录功能的是类似,在跳转页面的时候也需要向后端传递token以完成身份的验证,但是djangorestframework_jwt只提供了login的url,并没有注册的,此时就需要我们手动在注册完成之后,手动构造符合djangorestframework_jwt要求的token并返回。
通过以上代码的分析,token的构造主要依赖两个方法,jwt_payload_handler(user)
和jwt_encode_handler(payload)
, 所以我们可以重写注册过程中需要使用到的 mixins.CreateModelMixin 中的create()方法,实现如下:
1 | def create(self, request, *args, **kwargs): |
¶登录状态和身份验证的关系
注意这里要区分 登录状态 和 身份验证的关系:
身份验证是前端传递相关数据,如用户信息,或者通过session、token等认证机制判断当前用户是谁,以及获得用户的信息。
登录状态是前端的表现,如用户通过身份认证后会得到相应的信息作为标记,前端通过判断这个标记,显示是否为登录状态。
¶session和token的区别
- token 和 session 其实都是为了身份验证,session 一般翻译为会话,而 token 更多的时候是翻译为令牌。
- session 服务器会保存一份,可能保存到缓存,文件,数据库,token 通过算法生成以及验证,后端不进行存储。
- 其实 token 与 session 的问题是一种时间与空间的博弈问题,session 是空间换时间,而 token 是时间换空间。两者的选择要看具体情况而定。虽然确实都是“客户端记录,每次访问携带”,但 token 很容易设计为自包含的,也就是说,后端不需要记录什么东西,每次一个无状态请求,每次解密验证,每次当场得出合法/非法的结论。这一切判断依据,除了固化在 CS 两端的一些逻辑之外,整个信息是自包含的。这才是真正的无状态。而 sessionid ,一般都是一段随机字符串,需要到后端去检索 id 的有效性。万一服务器重启导致内存里的 session 没了呢?万一 redis 服务器挂了呢?