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的字面意思是"会话",顾名思义,当我们访问浏览器时,也就是向浏览器发起了会话,那么浏览器会根据这样的请求将我们的信息存入到服务器中保存,以至于下次依然还记得到底是哪个用户进行的会话。
Token的字面意思是"令牌",它本身是由是用户身份的验证方式,可以看成是用户的唯一标识、time(当前时间的时间戳)、sign(签名),由token的前几位+盐以hash算法压缩成一定长的十六进制字符串,可以防止恶意第三方拼接token请求服务器。还可以把不变的参数也放进token,避免多次查库。而我们的JWT就是以token为媒介做的。
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature):
Header头部:
头部包含了两部分信息,token 类型和采用的加密算法
payload载荷:
载荷就是存放有效信息的地方。这个名字开始让我想到了电力系统中的负荷,因为本身学的是电气,相当于承载的用电设备量,这里也差不多,这些有效信息包含三个部分:
另外里面的参数是:
signature:
字面意思为签名JWT的第三部分是一个签证信息,这个签证信息由三部分组成:
前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。
基于django-rest-framework的登陆认证方式常用的大体可分为四种,从上往下,关系越来越强:
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,后面我们会介绍一下。
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中是有响应的处理器的,我们需要拿到对应的处理器,然后,相当于一个变量,我们可以看看处理器的源码。
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
...
因为这部分代码太多,所以挑选我认为比较重要的看看(其实我也根本不知道哪里重要。。)这里我们可以获取到两个重要的信息:
而后我们可以看看序列化器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)
序列化器里其实和上面视图对接最大的那么就是验证部分了。
通过上面的分析,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中的数据。
参考文献: