JWT的引入
传统登录认证流程:
1. 用户第一次登录时, 生成一个token并返回给前台, 同时将其与用户主键一同存在后台服务器上(数据库或缓存中)
2. 下一次访问需要登录的页面时, 将token一起传入
3. 后台拿着token去数据库或缓存中查找是否存在该token, 存在则认证通过, 否则认证不通过
传统认证的缺点:
1. token存在后台, 增加了存储和读取的开销
2. 当存在多个后台服务器时, 需同步共享token, 比较麻烦
JWT认证流程(解决了传统认证的问题):
1. 用户第一次登录时, 生成一个token, 但后台不存储该token
2. 下一次访问需要登录的页面时, 将token一起传入
3. 后台拿着token进行解析和校验, 若解析成功则认证通过, 否则认证不通过
JWT加密原理:
生成的token分为三个部分: HEADER.PAYLOAD.SIGNATURE, 这三个部分都是可逆算法base64加密后的字符串, 最后用点号(.)拼接.如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1. HEADER
代表了加密算法和token类型, 若不显示指定, 默认为:
{
"alg": "HS256",
"typ": "JWT"
}
加密后结果为: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2. PAYLOAD
代表了想要传输的业务信息和token的过期时间(可选), 例如:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 451154141
}
加密后结果为: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
3. SIGNATURE
JWT的关键, 其规则是将前面两段加密后的密文再加上自定义的盐值一起拼接后, 再通过不可逆算法HS256(具体使用的是HEADER中的算法)进行加密, 最后再对该密文进行可逆算法base64加密
盐值(salt): 指的是加密时加入的自定义的字符串, 最好是随机或者杂乱的字符串, 这样更能够确定加密后字符串的唯一性, django中可以使用settings中的SECRET_KEY
加密后结果为: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT解密原理:
拿到请求中传过来的token后:
1. 按.号拆分token, 拿到三段值
2. 对这三段密文进行base64解密, 从明文中拿到加密算法和业务数据以及过期时间
3. 再次拼接前两段的密文和自定义的盐值(该盐值必须和创建token时的盐值一样), 使用HEADER中的算法进行加密
4. 将加密的结果和拿到的token中的第三段解密的明文进行比较, 若完全一致则说明认证成功, 若不一致则说明token被篡改过, 认证失败
对JWT三段式的思考:
JWT的根本思想就是将业务数据通过不可逆算法加密存储在token中, 那么为什么要搞成三段式这么复杂呢?
直接把业务数据加上盐值, 然后用默认不可逆算法生成一段密文字符串进行传输不就可以了吗?
这样加密时是比较简单, 但是解密时却由于不可逆算法而拿不到其中的业务数据, 所以确实需要再加一段式来单独存储业务数据
JWT又加了一段用来存储加密算法, 能够让使用者自己确定具体使用什么算法进行加密, 增加了可扩展性
python中使用JWT
pyjwt
这是python使用JWT的基础包, 在jwt官网中python语言点赞最多的就是pyjwt, 安装方式为: pip install pyjwt , 这个包已经把加密和解密的逻辑写好了, 我们只需要传入加密算法/业务数据/盐值即可
在rest_framework中使用pyjwt
定义两个接口, 登录(login)和查看订单(order), 只有登录过的用户才能成功访问查看订单接口, 我们可以在登录接口中若成功登录则返回jwt的加密token, 在订单接口中自定义一个认证类, 在认证类中校验token
1. 编辑urls.py
from django.urls import path from users import views urlpatterns = [ path('login/', views.LoginView.as_view()), path('order/', views.OrderView.as_view()), ]
2. 编辑登录和订单视图类
1. 登录成功后, 调用获取token的方法 create_token() , 传入参数为用户信息和token过期时间(单位: 分钟), 默认1分钟
2. 在订单视图类中设置认证类 JWTAuthentication
3. create_token和JWTAuthentication都定义在utils包的JWTAuth.py中
from rest_framework.views import APIView
from rest_framework.response import Response
from utils.JWTAuth import create_token, JWTAuthentication
class LoginView(APIView): def post(self, request, *args, **kwargs): # 获取用户名密码 name = request.data.get('name') pwd = request.data.get('pwd') # 获取User对象 try: user = models.User.objects.filter(name=name, pwd=pwd).first() except Exception: return Response({'status': 1, 'errmsg': '用户名或密码不正确!'}) # 获取token token = create_token({'id': user.id, 'name': user.name}, 1) # 返回成功响应 return Response({'status': 0, 'token': token}) class OrderView(APIView): authentication_classes = [JWTAuthentication, ] def get(self, request): return Response({'status': 0, 'msg': 'ok'})
3. 编辑JWTAuth.py
import jwt from jwt import exceptions as JWTException from django.conf import settings import datetime from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed def create_token(payload, timeout=1): # 给传过来的业务数据增加一个过期时间限制 payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=timeout) # 定义盐值 salt = settings.SECRET_KEY # 默认不可逆加密算法为HS256 token = jwt.encode(payload=payload, key=salt) return token class JWTAuthentication(BaseAuthentication): def authenticate(self, request): # 从url参数中获取token token = request.query_params.get('token') # 盐值 salt = settings.SECRET_KEY # 解码token try: payload = jwt.decode(jwt=token, key=salt, verify=True) except JWTException.ExpiredSignature: raise AuthenticationFailed('token已失效') except jwt.DecodeError: raise AuthenticationFailed('token认证失败') except jwt.InvalidTokenError: raise AuthenticationFailed('非法的token') return payload.get('name'), token
注意: 设置过期时间时, 一定是在payload段中设置, 且键名固定为'exp', 值为 datetime.datetime.utcnow() + datetime.timedelta(xxxx)