1.简介
Josn Web Token:常用于前后端分离中/微信小程序/app开发中的用户认证
官网地址:https://jwt.io/
2. 传统的用户认证流程
基于传统的token进行用户验证流程
- 用户登录,携带用户登录信息
- 后端校验用户数据,并生成对应的token
- 后端将token返回给前端,并保存在后端(数据库或者缓存中)
- 用户再次访问,携带token
- 后端检查用户携带的token是否和保存的一致
- 返回响应给前端
案例
视图
from rest_framework.views import APIView from rest_framework.response import Response from api import models import uuid # Create your views here. class LoginAPIView(APIView): def post(self, request, *args, **kwargs): # 检验用户信息。验证成功,返回token username = request.data.get('username') password = request.data.get('password') user_obj = models.UserModel.objects.filter(username=username, password=password).first() if not user_obj: return Response({'code': 1000, 'error': '用户名或者密码错误'}) token = str(uuid.uuid4()) user_obj.token = token user_obj.save() return Response({'code': 2000, 'token': token}) class OrderAPIView(APIView): def get(self, request, *args, **kwargs): # 获取订单信息,但是用户必须是已登录的 token = request.query_params.get('token') if not token: return Response({'coder': 1000, 'error': '用户需要登录才能访问'}) user_obj = models.UserModel.objects.filter(token=token).first() if not user_obj: return Response({'coder': 1000, 'error': 'token不正确'}) return Response({'coder': 2000, 'data': '订单列表'})
模型类
from django.db import models # Create your models here. class UserModel(models.Model): username = models.CharField(verbose_name='用户名', max_length=32, null=False) password = models.CharField(verbose_name='密码', max_length=32, null=False) token = models.CharField(verbose_name='TOKEN', max_length=128, null=True) class Meta: db_table = 'user_tb' verbose_name = '用户表' verbose_name_plural = '用户表'
路由
from django.urls import path from api import views urlpatterns = [ path('login/', views.LoginAPIView.as_view()), path('order/', views.OrderAPIView.as_view()), ]
3. JWT
备注:base64加密是可以反向解密的,HS256和md5则是不可逆的
jwt格式解析
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
字符串由两个点分割成三段
- 第一段是JWT的header,包含着加密算法和type,常常为固定值
将header转成字符串之后进行base64加密
{ "alg": "HS256", "typ": "JWT" }
- 第二段为payload,是真实数据,常为不敏感信息
将数据转化为字符串之后进行base64加密
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
- 第三段为verify,将前两段的密文拼接然后HS256加密+加盐,最后将密文再次进行base64加密
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), ) secret base64 encoded
校验过程解析
- 后端获取jwt token
- 通过点进行分割成三段
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- 对第一段进行base64解密,获取加密方式
{ "alg": "HS256", "typ": "JWT" }
- 对第二段进行base64解密,获取payload数据
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }
- 将第三段进行base64解密,得到HS256的密文A
- 将前两段的密文进行拼接然后进行HS256加密 + 加盐 得到密文B
- 将密文A B进行比较,判断token是否正确
JWT加密的核心就是加盐,盐必须保证只有服务端知道
基于JWT的token进行用户验证流程
- 用户登录,携带用户登录信息
- 后端校验用户数据,并基于jwt生成对应的token
- 后端将token返回给前端
- 用户再次访问,携带token
- 后端检查用户携带的token是否和保存的一致
- 返回响应给前端
两种验证最大的区别就是JWT验证不再需要存储token数值,在多用户的情况下,这可以很大程度上减轻服务压力
4. 应用
了解了JWT的原理之后,我们可以手动实现JWT的加密解密过程,,不过通过第三方库可以更加便捷
官网地址:https://jwt.io/libraries?language=Python
pip install pyjwt
加密:
可以指定加密的算法,加密数据和加密的盐
解密:指定token值和解密算法,解密的盐,返回的是payload数据
import jwt encoded_jwt = jwt.encode({"some": "payload"}, "secret", algorithm="HS256") print(encoded_jwt) eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg jwt.decode(encoded_jwt, "secret", algorithms=["HS256"]) # {'some': 'payload'}
示例JWT的加密:
import jwt import datetime # 构造salt salt = "asdkljal2o384u290slkafjl@U&#^(Q@#dsjfha" # 构造header header = { "alg": "HS256", "typ": "JWT" } # 构造payload data = { "sub": "1234567890", "name": "John Doe", "iat": 1516239022, 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=5) # 超时时间 } print(jwt.encode(payload=data, key=salt, headers=header)) # # eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE2Njk0NTExMjR9.I3dSGGKMNKilgLBtPDpvxjHapNYOVoLFEnfgHkqEkww
示例JWT解密
print(jwt.decode( jwt='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE2Njk0NTExMjR9.I3dSGGKMNKilgLBtPDpvxjHapNYOVoLFEnfgHkqEkww', key=salt, algorithms='HS256')) # {'sub': '1234567890', 'name': 'John Doe', 'iat': 1516239022, 'exp': 1669451124}
drf中开发实战
在用户登录成功之后,生成token并返回,用户再次来访问时需携带token。基于上述案例进行改进开发
视图函数
from rest_framework.views import APIView from rest_framework.response import Response from api import models import uuid from api.authention.jwtAuthentication import JwtAuthentication from api.utils import jwt_create class ProLoginAPIView(APIView): def post(self, request, *args, **kwargs): # 检验用户信息。验证成功,返回token username = request.data.get('username') password = request.data.get('password') user_obj = models.UserModel.objects.filter(username=username, password=password).first() if not user_obj: return Response({'code': 1000, 'error': '用户名或者密码错误'}) token = jwt_create.jwt_create({'username': username}) return Response({'code': 2000, 'token': token}) class ProOrderAPIView(APIView): authentication_classes = [JwtAuthentication, ] def get(self, request, *args, **kwargs): print(request.user) return Response({'coder': 2000, 'data': '订单列表'})
自定义验证类JwtAuthentication
# encoding:utf-8 # author:kunmzhao # email:1102669474@qq.com from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed from django.conf import settings import jwt class JwtAuthentication(BaseAuthentication): def authenticate(self, request): salt = settings.SECRET_KEY token = request.query_params.get('token') if not token: raise AuthenticationFailed('没有token') try: data = jwt.decode(jwt=token, key=salt, algorithms='HS256') except jwt.exceptions.ExpiredSignatureError: raise AuthenticationFailed('token已过期') except jwt.exceptions.InvalidTokenError: raise AuthenticationFailed('无效的token') except Exception as e: raise AuthenticationFailed(str(e)) return data, token
自定义jwt_create
# encoding:utf-8 # author:kunmzhao # email:1102669474@qq.com import jwt import datetime from django.conf import settings def jwt_create(data, exp=None): """ :param data: 加密数据 :param exp: 过期时间,单位为分钟 :return: token """ # 构造header header = { "alg": "HS256", "typ": "JWT" } key = settings.SECRET_KEY if exp: data['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=exp) return jwt.encode(payload=data, key=key, headers=header)