深入探讨JWT流程原理及对drf-jwt的流程分析和源码修改

背景

之前做Django项目的时候,一直都用的自带的session认证模式,Django的session模式的后端可以选择redis,也可以选择数据库进行存储。用redis如果需要经常清理内存数据库,而用db存储,则需要频繁写操作,效率也比较低,在用户多的时候,存储大量的session记录带来额外的开销。

一 什么是JWT?

JWT俗称 Json Web Token,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。

它的数据格式一般以JSON对象的形式传递,并伴有数字签名,加强安全性。


二 传统的认证模式Session

传统的session认证(其实是session+cookie),session离不开cookie。当某一个用户第一次登录了某个网站,那么为了满足一定时间内或条件内下一次进入该网站无需再登录,但是由于HTTP的无状态,无连接特性,并不会记住该用户的信息,那么就需要将这个登录状态以一种形式保存下来。通常以数据库为backends,建立表,然后保存成一次记录。

我就Django而言,把session的使用流程简单说明以下:

首先会建立名为django_session的表,分别设立了3个字段:session_id,session_data,session_date。此时,有一名用户,输入了用户名和密码准备登录到网站,它发送了POST请求到后台(不带任何cookie请求),服务器在中间件(SessionMiddleware)拦截到了发送的请求,判断是否带有session_id,如果没有,则“放他走”,取执行视图,执行完毕,一般回调用login方法,根据user对象和过期时间等一些额外信息来创建新的session记录,存放到数据表中,并在响应对象中设置cookie,返回给前端。如果请求body中存在session_id,则表明该请求前不久请求过一次,session尚未过期,因此就拿着这个session_id取数据库表中比对,找到对应的记录,然后根据session_data数据中的信息,创建User对象,存入request.user中,以便后面的视图能够使用request.user,同是将session_data数据转为字典形式,封装到request.session中,以便后续使用。

其实不同框架,不同技术实现session+cookie的原理基本都一样。


三 细想Session存在的一些问题

1.session需要经常对数据库写操作,磁盘I/O嘛,懂得,比较慢。同时如果不及时处理,越写越多,造成额外的开销

2.session通常搭配数据库,那么如果一个项目它的用户量很大的时候,一个数据库难以支持大规模的读写操作,此时就需要对数据库进行负载均衡了,集群处理了。因此问题就来了,存储在一个backend的session,如果做了集群,还要需存储session的固定数据库匹配session_id,是不是觉的蛮麻烦的。。。

3.其实说session很安全,也并不是百分百特别安全,毕竟和cookie有关,需要将session_id保存在cookie中,假如中途被人截了,做CSRF攻击咋办。不过Django针对CSRF攻击想得还是蛮周到,每次请求,都会在Cookie中添加额外的字段csrftoken,然后请求的时候,在请求头中带上这个csrftoken,用来防跨域伪造请求。不过在用DRF的APIView的时候,csrf也被禁止了,那咋办嘞?于是我想到了之前从别人口中听到的JWT认证,今天就来会会它!


四 JWT的原理

JWT的构成有三段,分别为head(头部),payload(有效载荷), signature(签证)。

结构如图所示:

{width=”100%”}

不同部分分别以’.’分隔。分别表示head,payload,和signature。


1.head :主要包括认证令牌的类型,加密算法,也可以是一些项目的信息:

1
2
3
4
5
{
"project":"天秀",
"algorithm":"HS256",
"type":"jwt"
}

然后对其进行base64编码,注意不是加密,可以对其反解码

2.payload:有效载荷存放的是有效信息的地方,体现的一般是关键信息。包括三类:标准中注册的声明,公共的声明,私有的声明。

  • 标准中注册的声明:包括jwt的签发者,设备号,接受jwt的一方,jwt的过期时间,jwt的签发时间等等。

  • 公共声明:包含业务的信息或者用户名信息等。

  • 私有声明:往往是提供者和客户共同定义的信息。

格式如:

1
2
3
4
5
6
{
"id": "125",
"name": "syz",
"admin": true,
"is_active":true,
}

注:这一部分的内容也是通过base64进行编码的,同时也是可以反解码的,需要在后台解码,提取相关用户信息,创建用户对象,因此也不应存放过于隐私的数据,确保传输安全性。

3.signature:用户签证作为其第三部分,它的组成有三部分:

  • base64编码后的head

  • base64编码后的payload

  • 服务器安全码(采用hash加密,如md5,不可逆)

格式如下:

1
2
3
4
5
{
"head": "头的加密字符串",
"payload": "体的加密字符串",
"secret_key": "安全码"
}

然后将这head和payload以及secret_key组合成一个字符串,再经过加密,就得到了jwt的第三部分—签证。

而最终的token是由’.’将这三部分连接起来。

{width=”100%”}


注: secret_key是服务端提供的密钥,不能被客户端用户知晓,否则客户端就可以自己签发token了。


五 使用jwt的好处

jwt的出现是为了解决session存在的一些问题,token不需要存储下来,直接通过base64编码+加密以及base64解码的方式,来生成所需要的用户对象。

优点也很明显:

1.不需要存储在服务器端,也就不需要大量的写操作,高效,不需要占用额外的磁盘空间,省空间。

2.存在利用hash不可逆加密的签证,只要服务器端的安全码不被外泄,则还是蛮安全的。

如果不太明白如何防止csrf的原理,我觉得这篇介绍了防止csrf,为什么要将cookie放到请求头中发送才行的解释,讲的还是蛮不错的。

我觉得最主要的一点就是csrf只是利用了受害者网站中的cookie,而不是拿其cookie,这样就促使服务器是从request的header中拿token,而不是直接从cookie中拿token,从而防止了CSRF攻击!

3.对于服务器或者数据库的水平扩展来说,比较方便,因为jwt也不需要设计到数据库,直接计算token返回给客户端,然后拿到token,进行解码校验,生成用户对象就ok了。


DRF中jwt使用的流程

签发(第一次请求后端):

1.从请求体中拿到json格式的数据,将head采用base64进行编码得到第一部分字符串

2.将payload采用base64编码得到第二部分字符串

3.将第一部分+第二部分+服务器安全码组合起来,使用hash加密得到签名字符串。

4.组合第一部分+第二部分+第三部分成为最终的token

校验(登录后再一次请求)

1.将token进行切片,分为三段,第一段为head,第二段为payload,第三段为signature。

2.将第二段进行解码,获取其中用户的信息,用于创建用户对象,并将其赋给request.user。

3.第一段+第二段+服务器安全码再进行一次安全加密去与切出来的第三段进行匹配,如果匹配成功,则通过认证,否则返回异常信息。


七 DRF中如何使用jwt

1.安装依赖包

pip install djangorestframework-jwt


2.setting.py中配置jwt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
REST_FRAMEWORK = {
# 全局添加jwt认证方式,所有视图请求都会调用该验证方法,对token进行认证,反解出生成user对象,赋给request.user
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
],
}

JWT_AUTH = {
# 配置jwt的过期时间,24小时存活周期
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
# 是否可以刷新
'JWT_ALLOW_REFRESH': True,
# 刷新的过期时间
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=1),
}

3.配置url路由

1
2
path('login-chsc-api/', LoginAPIView.as_view(), name='login-chsc-api'),

4.编写视图

drf-jwt默认为我们创建好了视图,但是只是基本的用户密码认证,如果我们要想自定义jwt视图和序列化器,就需要改写以下。

下面是我修改后的视图

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

class LoginAPIView(ObtainJSONWebToken):
""" 使用JWT登录"""
pass

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):
"""
Base API View that various JWT interactions inherit from.
"""
permission_classes = ()
authentication_classes = ()

def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'view': self,
}

def get_serializer_class(self):
"""
Return the class to use for the serializer.
Defaults to using `self.serializer_class`.
You may want to override this if you need to provide different
serializations depending on the incoming request.
(Eg. admins get full serialization, others get basic serialization)
"""
assert self.serializer_class is not None, (
"'%s' should either include a `serializer_class` attribute, "
"or override the `get_serializer_class()` method."
% self.__class__.__name__)
return self.serializer_class

def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)

def remember_username(self, response, is_remember, login_id):
"""设置cookie,本地暂存用户名1周"""
if is_remember:
response.set_cookie('login_id', login_id, max_age=7 * 24 * 3600)
else:
response.delete_cookie('login_id', login_id)

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 = serializer.object.get('token')
is_remember = serializer.object.get('is_remember')
previous_page = serializer.object.get('previous_page')
# 生成响应对象,如果配置中支持刷新,则更新token,将user调用中间件赋给request.user
response_data = jwt_response_payload_handler(token, user, request)
response_data.update({'previous_page':previous_page})
response = Response(response_data)
self.remember_username(response, is_remember, user.get_username()) # 记住用户名
# 将token存到response的cookie中,设置有效的日期
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)

修改对应的序列化器:

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
class JSONWebTokenSerializer(Serializer):
"""
Serializer class used to validate a username and password.

'username' is identified by the custom UserModel.USERNAME_FIELD.

Returns a JSON Web Token that can be used to authenticate later calls.
"""
def __init__(self, *args, **kwargs):
"""
Dynamically add the USERNAME_FIELD to self.fields.
"""
super(JSONWebTokenSerializer, self).__init__(*args, **kwargs)

# 创建额外的四个字段
self.fields[self.username_field] = serializers.CharField()
self.fields['password'] = PasswordField(write_only=True)
self.fields['previous_page'] = serializers.SlugField() # 前一页
self.fields['is_remember'] = serializers.BooleanField() # 是否记住用户名

@property
def username_field(self):
return get_username_field()

# 对前端传入的属性字段进行验证
def validate(self, attrs):

# 获取字段
credentials = {
self.username_field: attrs.get(self.username_field),
'password': attrs.get('password'),
}

if all(credentials.values()):
# 登录方式,邮箱或用户名登录
# 创建user对象
user = email_or_username.authenticate(**credentials)

if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)

payload = jwt_payload_handler(user) # 对其进行base64编码

return {
'token': jwt_encode_handler(payload), # 对其进行hash加密生成最终的token
'user': user,
'previous_page': attrs.get('previous_page'),
'is_remember':attrs.get('is_remember'),
}
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
else:
msg = _('Must include "{username_field}" and "password".')
msg = msg.format(username_field=self.username_field)
raise serializers.ValidationError(msg)

七 postman中测试编写好的jwt接口

{width=”100%”}

以上就是我对jwt的解读和drf-jwt的视图和序列化的自定义修改,其实jwt理解起来也不难。


我觉得以下几篇博客也值得参考:

DRF详解

一文读懂JWT