restframework(4):JWT

状态保持的三种方式:

cookie

cookie从字面意思上来讲是小甜饼,其实也可以说是小甜饼的意义,相当于给浏览器的饼干。随着服务器端的响应发送给客户端浏览器。然后客户端浏览器会把Cookie保存起来,当下一次再访问服务器时把Cookie再发送给服务器。

源码如下:
下面是一些具体的参数:

'''
class HttpResponseBase:

        def set_cookie(self, key,                 键
                     value='',            值
                     max_age=None,        超长时间 
                              cookie需要延续的时间(以秒为单位)
                              如果参数是\ None`` ,这个cookie会延续到浏览器关闭为止。

                     expires=None,        超长时间
                                 expires默认None ,cookie失效的实际日期/时间。 
                                

                     path='/',           Cookie生效的路径,
                                                 浏览器只会把cookie回传给带有该路径的页面,这样可以避免将
                                                 cookie传给站点中的其他的应用。
                                                 / 表示根路径,特殊的:根路径的cookie可以被任何url的页面访问
                     
                             domain=None,         Cookie生效的域名
                                                
                                                  你可用这个参数来构造一个跨站cookie。
                                                  如, domain=".example.com"
                                                  所构造的cookie对下面这些站点都是可读的:
                                                  www.example.com 、 www2.example.com 
                                 和an.other.sub.domain.example.com 。
                                                  如果该参数设置为 None ,cookie只能由设置它的站点读取。

                     secure=False,        如果设置为 True ,浏览器将通过HTTPS来回传cookie。
                     httponly=False       只能http协议传输,无法被JavaScript获取
                                                 (不是绝对,底层抓包可以获取到也可以被覆盖)
                  ): pass

'''

这里最常用的其实只是前面三个,后面的不常见,因为本篇介绍的是jwt,所以我就顺带提一下,以后有机会再来细究里面的构造。具体的可以看看下面的流程图:

在这里插入图片描述

session

session的字面意思是"会话",顾名思义,当我们访问浏览器时,也就是向浏览器发起了会话,那么浏览器会根据这样的请求将我们的信息存入到服务器中保存,以至于下次依然还记得到底是哪个用户进行的会话。

在这里插入图片描述
由上图可以发现

Token

Token的字面意思是"令牌",它本身是由是用户身份的验证方式,可以看成是用户的唯一标识、time(当前时间的时间戳)、sign(签名),由token的前几位+盐以hash算法压缩成一定长的十六进制字符串,可以防止恶意第三方拼接token请求服务器。还可以把不变的参数也放进token,避免多次查库。而我们的JWT就是以token为媒介做的。

JWT简介

JWT的含义

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

JWT的优点:

  1. 一般不需要经过服务器,直接存储在客户端,设置了过期时长后,它会自动刷新。
  2. 采用对称加密方式,签名的方式验证用户信息,安全性较之一般的认证高
  3. 不用再担心csrf(跨站请求伪造),相比于通过发送请求给服务器生成token值,那么直接将数据进行sha256计算更加能提高性能,减少了服务器的负载均衡。
  4. 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。

JWT的缺点:

  1. 采用对称加密,一旦被恶意用户获取到加密方法,就可以不断破解入侵获取信息,不过基本加密方法很难被破解
  2. 加大了服务器的计算开销,–不过相对于磁盘开销,这都不算啥
    总的来说,JWT没多少去缺点,所以很多公司都用这个业务做用户认证,当然,涉及到金钱的,还是选择非对称加密方式比较好。

JWT解析

JWT的组成

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature):

在这里插入图片描述

Header头部:

头部包含了两部分信息,token 类型和采用的加密算法

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

payload载荷:
载荷就是存放有效信息的地方。这个名字开始让我想到了电力系统中的负荷,因为本身学的是电气,相当于承载的用电设备量,这里也差不多,这些有效信息包含三个部分:

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

另外里面的参数是:

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

signature:
字面意思为签名JWT的第三部分是一个签证信息,这个签证信息由三部分组成:

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

前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。

restframework中的JWT

基于django-rest-framework的登陆认证方式常用的大体可分为四种,从上往下,关系越来越强:

  1. BasicAuthentication:账号密码登陆验证
  2. SessionAuthentication:基于session机制会话验证
  3. TokenAuthentication: 基于令牌的验证
  4. JSONWebTokenAuthentication:基于Json-Web-Token的验证

JWT的配置

settings.py

REST_FRAMEWORK = {
     #身份认证的方式:jwt,session两种
    'DEFAULT_AUTHENTICATION_CLASSES': (
        #前后端分离使用jwt验证
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        #访问admin后台时使用
        'rest_framework.authentication.SessionAuthentication',
    ),
}

JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
    # JWT_EXPIRATION_DELTA 指明token的有效期
}

在上面的注释已经很清楚了,如果要配置jwt,这两个参数是不能改变的,另外就是为什么还有一个session,因为我们有在url中设置admin路由,一般是习惯加上这个,不过在这里并没有太大用处,可以不写。

url.py:

from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [   
       ...     
       url(r'^authorizations/$',obtain_jwt_token), 
]

路由中后面的视图部分是固定的,是jwt视图部分已经封装好的用于登录验证返回token的实例化对象,它对应的类是ObtainJSONWebToken,后面我们会介绍一下。

JWT的使用

这是我认为对jwt描述得比较好的流程图还有步骤:
在这里插入图片描述

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和XSRF问题)
5.后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

基于以上经验,在我们定义的serializers.py文件中,可以完成token的生成和传递。这里,模型类可以继承自AbstractUser,也就是django本身已经写好的认证表auth,我们只需要再往里加其它字段即可,而序列化器继承自Serializer更好,因为需要生成token并且传回去,所以一般需要重写create方法,ModelSerializer虽然也可以,但一般不这么用,而且较难理解,改写一般建议用Serializer,具体的可以看我序列化器的博文。重写的create方法如下:

    # 保存
    def create(self, validated_data):
        user = User()
        user.username = validated_data.get('username')
        user.set_password(validated_data.get('password'))
        user.save()

        #需要生成token
        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)#header.payload.signature

        #将token输出到客户端
        user.token=token

        return user

上面这段代码前面部分就不再解释了,我记得之前我写的博文里应该提过,那么重点放在后面四句话,从生成token这里开始,首先通过接收api设置里面的payload和encode处理器,这个是rest_framework_jwt里的默认设置。

payload处理器内部帮我们封装好了对user模型类数据的处理,也就是base64,也就是说我们前面解释的载荷里面的信息。

而我们的请求头一般都是不会变化的,当我们应用浏览器的时候,请求头基本上就已经固定,另一个秘钥的话,django中一般都是生成在settings里,也就是服务器内部,如果我们想要将其存于它处,也行,只要安全性有保证。

然后我们就能输出token到客户端进行验证,基于restframework的jwt验证到这里,后端的功能就已经全部实现。后面都将是前端的事情。

那么我们可以看看前端是怎么接收的,下面是部分源码:

            if (this.error_username == false && this.error_pwd == false) {
                axios.post(this.host + '/authorizations/', {
                    username: this.username,
                    password: this.password
                }, {
                    responseType: 'json',
                    withCredentials: true
                })
                    .then(response => {
                        // 使用浏览器本地存储保存token
                        if (this.remember) {
                            // 记住登录
                            sessionStorage.clear();
                            localStorage.token = response.data.token;
                            localStorage.user_id = response.data.user_id;
                            localStorage.username = response.data.username;
                        } else {
                            // 未记住登录
                            localStorage.clear();
                            sessionStorage.token = response.data.token;
                            sessionStorage.user_id = response.data.user_id;
                            sessionStorage.username = response.data.username;
                        }

这里的前端验证没啥太大好说的,如果想要完整代码可以直接call我,这里我们注意到我们的jwttoken是保存到了两个仓库里,这在前面的jwt简介里也提过了。jwt的保存总共有三个地方,localstorage、sessionstorage和cookie。但具体要保存在哪,还是要看实际情况。

首先restframework中是有响应的处理器的,我们需要拿到对应的处理器,然后,相当于一个变量,我们可以看看处理器的源码。

restframework中JWT源码解析

rest_framework_jwt的源码相对来讲结构还是很一目了然的,这里我就直接上代码,基本上是一一对应。下面我们从类视图中入手,来看看它具体有哪几种验证方式:

class JSONWebTokenAPIView(APIView):
...

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 VerifyJSONWebToken(JSONWebTokenAPIView):
...

class RefreshJSONWebToken(JSONWebTokenAPIView):
...

序列化器为:

class JSONWebTokenSerializer(Serializer):
...

class VerificationBaseSerializer(Serializer):
...

class VerifyJSONWebTokenSerializer(VerificationBaseSerializer):
...

class RefreshJSONWebTokenSerializer(VerificationBaseSerializer):
...

在这里插入图片描述

这里因为我们本文是建立在获取token值并做身份验证的基础上,用的类是ObtainJsonWebToken,它里面的注释也说明了这个方法的作用:
接收带有用户用户名和密码的帖子的API视图。
返回一个JSON Web令牌,可用于验证请求。

那么我们可以看看JSONWebTokenAPIView中的方法:

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

因为这部分代码太多,所以挑选我认为比较重要的看看(其实我也根本不知道哪里重要。。)这里我们可以获取到两个重要的信息:

  1. 其调用了序列化器,让序列化器先对数据进行了校验,然后得到了处理后的token和当前user对象数据。而这个序列化器就是上面看到的序列化器类 – JSONWebTokenSerializer。
  2. 随后调用了一个函数 jwt_response_payload_handler(token, user, request),生成响应对象。然后再通过cookie将数据创建并保存了起来。httponly表示协议传递方式,expires表示超长时间,具体的可以看上面的状态保持一中的cookie参数。

而后我们可以看看序列化器JSONWebTokenSerializer中的代码:

class JSONWebTokenSerializer(Serializer):

    def validate(self, attrs):
        credentials = {
            self.username_field: attrs.get(self.username_field),
            'password': attrs.get('password')
        }

        if all(credentials.values()):
            user = 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
                }
            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)

序列化器里其实和上面视图对接最大的那么就是验证部分了。

  1. 通过其Django认证系统内部的一个 user = authenticate(**credentials) 实现对用户名和密码的校验和生成user对象 。关于authenticate日后应该会再开一篇博客再深究,这里单纯提一下有这个功能。
  2. 通过用户对象生成jwt的payload部分,并且签发token。
  3. 返回用户对象和 token。(返回值会传入validated_data中,可以在视图中通过serializer.validated_data获取到.)这里需要注意一点,就是我们一般从序列化器中取数据都是.validated_data,表示经过验证后的数据,我好像这个在之前的序列化器篇没有提,这里提一下,以后有时间汇总一下。

重写JWT登录方式

通过上面的分析,rest_framework_jwt提供的obtain_jwt_token视图,实际从rest_framework_jwt.views.ObtainJSONWebToken类视图而来,我们可以重写此类视图里的post方法来添加其余的逻辑,比如说是购物车的合并,我们可以这样写:

from rest_framework_jwt.views import ObtainJSONWebToken
from carts.utils import cart_cookie

class UserAuthorizeView(ObtainJSONWebToken):
    """
    用户认证
    """
    def post(self, request, *args, **kwargs):
        # 调用父类的方法,获取drf jwt扩展默认的认证用户处理结果
        response = super().post(request, *args, **kwargs)

        # 仿照drf jwt扩展对于用户登录的认证方式,判断用户是否认证登录成功
        # 如果用户登录认证成功,则合并购物车
        serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            user = serializer.validated_data.get('user')
            response = cart_cookie(request, user, response)
		
        return response

那么我们就能修改路由为:

urlpatterns = [
    ...
    # url(r'^authorizations/$', obtain_jwt_token), 
    url(r'^authorizations/$', views.UserAuthorizeView.as_view()),
    ...
]

总结

本来计划是第四篇接着写restframework中的认证组件,但按心情来讲就换成了这。。。这篇博文其实是及几天前就存在于我的草稿箱里,但当时我却很难下笔,原因就是接触不多吧,如果不是正好做了一个用户注册登录的模块,可能会在以后接触这种方式,而这种方式理解起来是有些费劲的,我认为是状态保持里最难理解的一个吧,因为session和cookie在学习的时候做过大量的铺垫,所以理解起来并不是很难,cookie即使是一个门外汉,大概都听说过浏览器记录,而session是因为django中通过数据库迁移会自动生成这么一张表,一般存数据都是基于此,而token还仅限于跨站请求部分,但真正梳理一遍后,其实jwttoken万变不离其宗,都是token的一种表达形式而已。所以,当学习到一个未知领域的时候,最好将自己所学的进行迁移联想,通过现有的方式去触类旁通,这种方式学习起来应该是最快的。

另外,因为最近虚拟机有些东西没有设置好,所以没有演示具体的token以及一些登录信息具体在哪,我就以csdn为例,通过右键检查,可以找到application中的数据。

在这里插入图片描述

参考文献:

  1. https://www.jianshu.com/p/180a870a308a
  2. http://blog.rainy.im/2015/06/10/react-jwt-pretty-good-practice/
  3. https://ruiming.me/authentication-of-frontend-backend-separate-application/
  4. https://blog.csdn.net/tobetheender/article/details/52485948
  5. https://www.cnblogs.com/yuanchenqi/articles/9036467.html
  6. http://www.cnblogs.com/littlefivebolg/p/9844727.html
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

submarineas

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值