背景 之前做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 :有效载荷存放的是有效信息的地方,体现的一般是关键信息。包括三类:标准中注册的声明,公共的声明,私有的声明。
格式如:
1 2 3 4 5 6 { "id" : "125" , "name" : "syz" , "admin" : true , "is_active" :true , }
注: 这一部分的内容也是通过base64进行编码的,同时也是可以反解码的,需要在后台解码,提取相关用户信息,创建用户对象,因此也不应存放过于隐私的数据,确保传输安全性。
3.signature :用户签证作为其第三部分,它的组成有三部分:
格式如下:
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 = { 'DEFAULT_AUTHENTICATION_CLASSES' : [ 'rest_framework_jwt.authentication.JSONWebTokenAuthentication' , ], } JWT_AUTH = { '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 = JSONWebTokenSerializerclass 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' ) 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()) 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 = 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) return { 'token' : jwt_encode_handler(payload), '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