1、介绍

状态保持:因为http是一种无状态协议,浏览器请求服务器是无状态的

无状态:指一次用户请求时,浏览器、服务器无法知道之前这个用户做过什么,每次请求都是一次新的请求

无状态原因:浏览器与服务器之间使用socket套接字进行通讯,服务器将请求结果返回给浏览器后,会关闭当前的socket连接,而且服务器也会在处理页面完毕之后销毁页面对象

保持登录状态,实现状态保持的方法:

  1. cookie
  2. session
  3. jwt

2、Cookie

2.1、Cookie介绍

Cookie:指某些网站为了辨别用户身份、进行会话跟踪而存储在用户本地的数据(通常经过加密)


      特点:

    • 服务器生成,发送给客户端浏览器并以key:value的形式存储在客户端,下一次请求同一网站时就发送该cookie给服务器(前提是浏览器设置为启用cookie)
      • 如果指定了cookie的过期时间,cookie则存储在磁盘里面,自动进行失效处理
      • 如果没有指定过期时间,cookie则存储在客户端浏览器的内存中,当浏览器会话结束后失效
    • cookie的key:value可以由服务器端自定义
    • cookie是存储在浏览器中的一段纯文本信息,是明文的,建议不要存储敏感信息,因为电脑的浏览器可能被其他人使用
    • 存储数据少。大多浏览器最大为4K的cookie,一般存储一个网站创建的20个cookie,如果超出规定的数据,旧的cookie会被删除
    • cookie基于域名安全,不同域名的cookie是不能相互访问的
    • 应用:
    • 最典型的应用是判断注册用户是否登录网站,是否在下一次进入此网站时保留用户信息以简化登录手续,这些都是cookie的功用
    • 网站广告推送,经常遇到访问某个网站时,会弹出小窗口,展示曾经在购物网站上看过的商品信息
    • 购物车,用户可能会在一段时间内在同一家网站的不同页面中选择不同的商品,这些信息可以写入cookie,以便在最后付款时提取信息
    • 当浏览器请求某网站时,会将本网站下所有的cookie信息提交给服务器,所有在request中可以读取cookie信息
    • cookie的生成机制

      jwt session_json


3、Session

session是一个服务端的保护机制,可以把各种类型的数据存储在session中,这些数据是存储在服务端开辟的一块内存当中,这块内存就是session。session需要借助cookie才可以达到想要的保存标识的目的

特点:

  • 优点
  • 存储空间大
  • 存储在服务器
  • 缺点
  • 记录用户数据使用的session缓存必须共享,限制了集群服务器的横向拓展
  • 存储用户数据,占用了大量的后台存储空间

login()

login() 方法实现状态保持方式:

  • 用户信息保存至 session
  • sessionsessionid 存放至cookie
  • cookie 放到响应中. 会随着响应返回给前端浏览器

使用方式:


# 导入: 
from django.contrib.auth import login

# 调用: 
login(request, user)


示例:注册用户


jwt session_jwt session_02

jwt session_服务器_03

try:
    user = User.objects.create_user(username=username,
                                    password=password,
                                    mobile=mobile)
except Exception as e:
    return http.JsonResponse({'code': 400, 'errmsg': '注册失败!'})

# 添加仅此一行代码
# 实现状态保持
login(request, user)

# 13.拼接json返回
return http.JsonResponse({'code': 0, 'errmsg': '注册成功!'})

View Code

说明:

  • login( )user 的信息(username、password、mobile)写入到 session,并保存到redis
  • 可以在 cookie 中获取 sessionid
  • 也可以在 redis 中查看 session 的保存情况.

4、JWT(Json Web Token)


4.1、token(令牌)格式

格式:以"点"分隔的三部分字符串组成;

jwt session_Code_04

4.2、token签发

头信息签发

jwt session_jwt session_02

jwt session_服务器_03

# 头信息(header)
header = {
  'typ': 'JWT', # 类型说明
  'alg': 'HS256' # 加密方法
}
# 1、把header字典转化为json
header = json.dumps(header)
# 2、在把json格式的头信息通过base64编码得出最终的头信息字符串
header = base64.b64encode(header.encode()).decode()
print("header: ", header)

View Code

载荷信息签发

jwt session_jwt session_02

jwt session_服务器_03

# 载荷中除了记录自定义用户数据以外,还有jwt标准中约定的其他信息,如下(建议但不强制使用):
# iss: jwt签发者
# sub: jwt所面向的用户
# aud: 接收jwt的一方
# exp: jwt的过期时间,这个过期时间必须要大于签发时间
# nbf: 定义在什么时间之前,该jwt都是不可用的.
# iat: jwt的签发时间
# jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

# 载荷(payload)
payload = {
    'username': 'weiwei',
    'user_id': 1,
    'age': 18
}
payload = json.dumps(payload)
payload = base64.b64encode(payload.encode()).decode()
print('payload: ', payload)

View Code

签名信息生成

jwt session_jwt session_02

jwt session_服务器_03

# 签名(signature) —— 校验token真伪就是依赖这个签名的!
# 使用哈希算法,对信息进行摘要生成签名,目的是保证信息对可靠性!
# 原理:只要信息没有被改动,那么原信息与签名是匹配的!

# 1、构建哈希对象
# key: 加密使用的密钥 —— 使用该密钥生成签名;
# msg: 信息 —— 该信息就是需要被保护的信息;在jwt中,指的就是header和payload两部分信息;
# digestmod: 算法
# key
SECRET_KEY = '86j_0^*mri+88x)w*wdoiv=%7sd+c4g66f#zm7t1uen^mb!h8y'
# msg
message = header + '.' + payload
h_obj = hmac.new(key=SECRET_KEY.encode(), msg=message.encode(), digestmod=hashlib.sha256)
# 2、找对象里么的方法,生成签名
signature = h_obj.hexdigest()
print("signature: ", signature)

# ==================================
JWT_TOKEN = header + '.' + payload + '.' + signature
print("token: ", JWT_TOKEN)
# ==================================

View Code

4.3、token校验

核心原理:重新对headerpayload按照签发时相同的密钥算法加密得出新的签名,然后新旧签名比对当且仅当一致的时候才能说明数据时可靠的!

jwt session_jwt session_02

jwt session_服务器_03

# 模拟前端传参
token_from_browser = JWT_TOKEN # 有效
# token_from_browser = "fewf" + JWT_TOKEN # 无效(篡改)

# 模拟校验流程
# 原理:只要信息没有被改动,那么原信息与签名是匹配的!
# 只要信息没有被篡改,那么根据相同的密钥和算法,生成的第三部分签名和原来的签名一定一致;

# 1、获取信息(header和payload)
old_header = token_from_browser.split('.')[0]
old_payload = token_from_browser.split('.')[1]
old_signature = token_from_browser.split('.')[2]
# 2、把信息按照相同的密钥和算法,重新生成新的签名
message = old_header + '.' + old_payload
new_h_obj = hmac.new(key=SECRET_KEY.encode(), msg=message.encode(), digestmod=hashlib.sha256)
new_signature = new_h_obj.hexdigest()
print("新的签名: ", new_signature)
# 3、比对新旧签名是否一致:一致则原信息没有被篡改,否则原信息被篡改了!
if old_signature == new_signature:
    # 有效
    print('验证成功')
    # 提取用户数据
    user_json = base64.b64decode(old_payload.encode()).decode()
    user_dict = json.loads(user_json)
    print("解析出来的用户身份信息:", user_dict)
else:
    # 无效
    print("验证失败")

View Code

4.4、JWT实现单点登录

①、安装djangorestframework-jwt拓展

pip install djangorestframework-jwt

②、手动代码实现单点登录

传统身份认证接口:

jwt session_jwt session_02

jwt session_服务器_03

from django.contrib.auth import authenticate
from rest_framework import serializers
from rest_framework_jwt.utils import jwt_payload_handler,jwt_encode_handler

# 明确,定义一个序列化器对username和password这两个字段进行校验
# 校验的前端传参是:{"username": 'weiwei', 'password': 'xxxxxx'}
class LoginSerializer(serializers.Serializer):
    username = serializers.CharField(
        required=True,
        max_length=20,
        allow_blank=False,
        allow_null=False
    )
    password = serializers.CharField(
        required=True,
        max_length=20,
        min_length=8,
        allow_blank=False,
        allow_null=False
    )

    # 登陆校验用户名和密码 —— 自定义校验
    def validate(self, attrs):
        # attrs = {"username": 'weiwei', 'password': 'xxxxxx'}
        # 1、传统身份校验(校验用户和密码)
        user = authenticate(**attrs)
        if user is None:
            raise serializers.ValidationError("用户名或密码错误!")

        # 2、传统身份校验成功 —— 签发token(令牌) —— 把该token作为有效数据的一部分
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)

        # 3、返回有效数据
        return {
            'user': user,
            'token': token
        }

View Code

③、使用djangorestframework-jwt拓展提供的视图接口完成

  • 路由映射

jwt session_jwt session_02

jwt session_服务器_03

from django.urls import re_path
# obtain_jwt_token为拓展插件提供的用于验证用户名和密码并签发token的视图
from rest_framework_jwt.views import obtain_jwt_token
from .views.login_views import *

urlpatterns = [
    # re_path(r'^authorizations/$', LoginView.as_view()),
    re_path(r'^authorizations/$', obtain_jwt_token),
]

View Code

  • 自定义obtain_jwt_token视图中用于构造响应的函数

jwt session_jwt session_02

jwt session_服务器_03

def jwt_response_payload_handler(token, user=None, request=None):
    return {
      	# 补充返回username和user_id字段
        'username': user.username,
        'user_id': user.id,
        'token': token
    }

View Code

  • 全局配置

jwt session_jwt session_02

jwt session_服务器_03

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
      	# 追加Token认证后端 —— 用于验证token有效期识别用户身份
        # 验证用户的令牌(token)来确定用户身份
        # 思考:
        # 1、前端如何传递token值;
        # 答: 头部携带;Authorization: JWT gregtfefewfewfewrw.trhyt4hy.4hy46h6fewfewfewfew
        #      JSONWebTokenAuthentication后端会根据既定的格式提取并校验token有效性,进而获取用户身份
        #      把验证成功的用户对象赋值给了request.user
        # 2、如何校验; —— 重复加密,比对新旧签名;
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',

        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
}

JWT_AUTH = {
    # 有效期设置为10天
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=10),


    # 来指定拓展插件默认视图返回的响应参数构造函数
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'meiduo_mall.utils.jwt_response_handlers.jwt_response_payload_handler'
}

View Code

4.5、JWT拓展约定的前端传递token方式

①、前端保存token的方式

  • (1)、localStorage: 长期存储
  • (2)、sessionStorage: 临时存储(当浏览器关闭或者标签页关闭,数据清除)

②、rest_framework_jwt拓展的身份认证后端JSONWebTokenAuthentication所约定的前端传递token的方式:

  • (1)、在请求头中携带token值
  • (2)、具体约定的格式为



Authorization

JWT gfbhrbghjrebw.gbrehyuwbgt.grbe3yghubtrw4ghl

jwt session_jwt session_21

③、前端工程师在调用接口的时候,必须按照上述既定的格式传递token参数,用于后端身份认证

jwt session_jwt session_02

jwt session_服务器_03

this.axios.get(cons.apis + '/goods/brands/'+this.edit_id+'/', {
      headers: {
        // 请求头中携带token
        'Authorization': 'JWT ' + token
      },
      responseType: 'json',
  })
  .then(dat=>{
     this.BrandsForm.name = dat.data.name;
     this.BrandsForm.logo = dat.data.logo;
     this.BrandsForm.first_letter = dat.data.first_letter;
  }).catch(err=>{
     console.log(err.response);
});