Django-rest-framework 的JWT登录和认证

DRF 中 JSONWebTokenAuthentication 的分析

基于django-rest-framework的登陆认证方式常用的大体可分为四种:

BasicAuthentication:账号密码登陆验证

SessionAuthentication:基于session机制会话验证

TokenAuthentication: 基于令牌的验证

JSONWebTokenAuthentication:基于Json-Web-Token的验证

其中最常用的是JWT,什么是JWT呢?官方是这样介绍的:

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

基于session的登录认证

基于session的登录认证.png

传统的用户登录认证中,因为http是无状态的,所以都是采用session的认证方式。用户登录成功,服务端就会保存一个session,也会给客户端一个sessionID,以后客户端每次请求,都会携带这个sessionID,服务端会会根据这个sessionID来区分不同的用户。

这种基于cookie+session的认证方式,随着服务从单服务到多服务,缺点就出来了,因为session是存储在服务端,这样服务器的开销就会大起来了。并且session标示丢失,就可能出现安全问题CSRF(跨站请求伪造)。

**扩展性**: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。

基于token的认证机制

JWT认证.png

JWT的认证机制不是这样的,它不需要服务端来保存用户的认证信息和会话信息,只需要每次客户端请求时签发一个token,token保存在客户端,这样就不需要考虑用户在哪台服务器登录了。

JWT的优点

  1. 签名的方式验证用户信息,安全性较之一般的认证高
  2. 加密后的字符串保存于客户端浏览器中,减少服务器存储压力
  3. 签名字符串中存储了用户部分的非私密信息,一定程度上能够减少服务器数据库查询用户信息的开销

JWT缺点

  1. 采用对称加密,一旦被恶意用户获取到加密方法,就可以不断破解入侵获取信息,不过基本加密方法很难被破解
  2. 加大了服务器的计算开销,–不过相对于磁盘开销,这都不算啥

总的来说,JWT没多少缺点,所以很多公司都用这个业务做用户认证,当然,涉及到金钱的,还是选择非对称加密方式比较好。

django-rest-framework_JWT的流程:

  • 用户使用用户名密码来请求服务器
  • 服务器进行验证用户的信息
  • 服务器通过验证发送给用户一个token
  • 客户端存储token,并在每次请求时附送上这个token值
  • 服务端验证token值,并返回数据

JWT的构成

JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature)。

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256

完整的头部就像下面这样的JSON:

1
2
3
4
{
'typ': 'JWT',
'alg': 'HS256'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

标准中注册的声明 (建议但不强制使用) :

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload

1
2
3
4
{
'name':'zhangsan',
'age':18
}

然后将其进行base64加密,得到Jwt的第二部分。

1
eyJuYW1lIjoiend6IiwiYWdlIjoiMTgifQ
signature

JWT的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行 jwt的签发和 jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

Django REST framework JWT 的使用

服务端在检验完用户身份(用户名和密码)之后,需要向客户端签发JWT,在需要用到身份验证的时候,需要核验用户的JWT。

使用详情参考django-rest-framework_jwt.

1.安装扩展

1
pip install djangorestframework-jwt

2.配置

1
2
3
4
5
6
7
8
9
10
11
12
REST_FRAMEWORK = {
# 配置默认的认证方式 base:账号密码验证 session:session_id认证
'DEFAULT_AUTHENTICATION_CLASSES': (
# drf的这一阶段主要是做验证,middleware的auth主要是设置session和user到request对象
# 默认的验证是按照验证列表从上到下的验证
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
"rest_framework_jwt.authentication.JSONWebTokenAuthentication",
)}
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), # 指明token的有效期
}

3.签发token

检验完用户的信息之后,需要手动签发token,返回给客户端

1
2
3
4
5
6
7
8
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),根据user的信息生成一个payload
payload = jwt_payload_handler(user)
# 根据payload和secret_key,采用HS256,生成token.
token = jwt_encode_handler(payload)

HS256中用到的key说明:默认使用的是django项目中的SECRET_KEY,如果需要用其他的,需要设置JWT_GET_USER_SECRET_KEY 或 JWT_PRIVATE_KEY,来看下源码的查找顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def jwt_encode_handler(payload):
# 先查找配置中的JWT_PRIVATE_KEY,没有再调用jwt_get_secret_key()方法
key = api_settings.JWT_PRIVATE_KEY or jwt_get_secret_key(payload)
return jwt.encode(
payload,
key,
api_settings.JWT_ALGORITHM
).decode('utf-8')

def jwt_get_secret_key(payload=None):
if api_settings.JWT_GET_USER_SECRET_KEY:
User = get_user_model() # noqa: N806
user = User.objects.get(pk=payload.get('user_id'))
# 使用配置中的JWT_GET_USER_SECRET_KEY,有的话,直接返回这个key
key = str(api_settings.JWT_GET_USER_SECRET_KEY(user))
return key
# 都没有值,调用django项目中的SECRET_KEY
return api_settings.JWT_SECRET_KEY

4.修改序列化器

1
2
3
4
5
6
class CreateUserSerializer(serializers.ModelSerializer):
"""
创建用户序列化器
"""
...
token = serializers.CharField(label='登录状态token', read_only=True) # 增加token字段

5.前端保存token

浏览器的本地存储提供了sessionStorage 和 localStorage 两种:

  • sessionStorage 浏览器关闭即失效
  • localStorage 长期有效
1
2
3
4
5
6
7
sessionStorage.变量名 = 变量值   // 保存数据
sessionStorage.变量名 // 读取数据
sessionStorage.clear() // 清除所有sessionStorage保存的数据

localStorage.变量名 = 变量值 // 保存数据
localStorage.变量名 // 读取数据
localStorage.clear() // 清除所有localStorage保存的数据

6.url配置(只针对做用户认证的)

1
2
3
4
5
6
7
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
# ...

url(r'^api-token-auth/', obtain_jwt_token),
]

obtain_jwt_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
42
43
44
45
# obtain_jwt_token 调用了ObtainJSONWebToken.as_view()
obtain_jwt_token = ObtainJSONWebToken.as_view()

class ObtainJSONWebToken(JSONWebTokenAPIView):
"""
API View that receives a POST with a user's username and password.

Returns a JSON Web Token that can be used for authenticated requests.
"""
serializer_class = JSONWebTokenSerializer

class JSONWebTokenAPIView(APIView):
...
# post请求过来调用的方法
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)

# 会进行校验操作
if serializer.is_valid():
# 拿到用户信息
user = serializer.object.get('user') or request.user
# 拿到token
token = serializer.object.get('token')
# jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER,
# 调用了JWT_RESPONSE_PAYLOAD_HANDLER(),生成响应数据
response_data = jwt_response_payload_handler(token, user, request)
response = Response(response_data)
if api_settings.JWT_AUTH_COOKIE:
expiration = (datetime.utcnow() +
api_settings.JWT_EXPIRATION_DELTA)
response.set_cookie(api_settings.JWT_AUTH_COOKIE,
token,
expires=expiration,
httponly=True)
return response

return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
...

# post方法调用了这个方法来返回响应数据, 所以想要返回其他数据,可以重写这个方法
# 这个方法里的user在调用时,已经传入了
def jwt_response_payload_handler(token, user=None, request=None):
return {
'token': token
}

7.自定义响应数据

obtain_jwt_token默认返回的只有token,如果前端还需要返回其他字段,需要重写 jwt_response_payload_handler()

1
2
3
4
5
6
7
8
9
def jwt_response_payload_handler(token, user=None, request=None):
"""
自定义jwt认证成功返回数据
"""
return {
'token': token,
'user_id': user.id,
'username': user.username
}

重写后,需要在修改配置文件,告诉JWT:JWT_RESPONSE_PAYLOAD_HANDLER 调用的是重写后的函数

1
2
3
4
5
6
# JWT
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
# 修改为自定义的函数路径
'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler',
}

总结

JWT用户认证流程.png

  1. 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
  2. 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形同lll.zzz.xxx的字符串。
  3. 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
  4. 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和CSRF问题)
  5. 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
  6. 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
------ 本文结束------
坚持原创技术分享,您的支持将鼓励我继续创作!
0%