DRF之JWT认证
【一】JWT
- WT(JSON Web Token)是一种开放标准(RFC 7519),用于在网络上传输声明的一种紧凑且自包含的方式。JWT 可以使用 HMAC 算法或是使用 RSA 或 ECDSA 等公钥/私钥对进行签名。通常,它用于在身份提供者和服务之间传递被认证的用户身份信息,以便于在用户和服务之间安全地传递声明。
- JWT 主要由三部分组成,用
.
分隔开:
- Header(头部):包含了类型(typ)和所使用的算法(alg)等信息,通常是
{"alg": "HS256", "typ": "JWT"}
。
- Payload(负载):包含了要传递的声明信息,如用户的身份、角色等,以及其他自定义的数据,例如
{"sub": "user123", "role": "admin"}
。
- Signature(签名):将编码后的 Header 和 Payload 以及一个秘密(对称加密)或公钥/私钥(非对称加密)一起进行签名,以保证数据的完整性和真实性。
- JWT 的优势在于它的紧凑性和自包含性,使得它适合在不同的环境中进行传递,如在 HTTP 请求头、URL 参数或 POST 参数中。JWT 的缺点是一旦签发后,其内容无法更改,也无法撤销,除非设置了较短的过期时间并在服务端进行验证。
- 在 Web 开发中,JWT 被广泛用于实现用户认证和授权机制,常见的应用场景包括单点登录(SSO)、API 认证等。
【二】SimpleJwt
【0】SimpleJwt源码
- SimpleJwt其实与drf类似,也是一个app,需要在django项目文件中注册
【0.1】登录
| |
| from rest_framework_simplejwt.views import token_obtain_pair |

【0.2】认证
| |
| from rest_framework_simplejwt.authentication import JWTAuthentication |

【1】SimpleJwt配置文件
| |
| SIMPLE_JWT = { |
| 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), |
| 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), |
| |
| |
| |
| |
| 'ROTATE_REFRESH_TOKENS': False, |
| |
| 'BLACKLIST_AFTER_ROTATION': False, |
| 'ALGORITHM': 'HS256', |
| 'SIGNING_KEY': settings.SECRET_KEY, |
| |
| "UPDATE_LAST_LOGIN": False, |
| |
| "VERIFYING_KEY": "", |
| "AUDIENCE": None, |
| "ISSUER": None, |
| "JSON_ENCODER": None, |
| "JWK_URL": None, |
| "LEEWAY": 0, |
| |
| "AUTH_HEADER_TYPES": ("Bearer",), |
| |
| "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", |
| |
| "USER_ID_FIELD": "id", |
| |
| "USER_ID_CLAIM": "user_id", |
| |
| |
| "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule", |
| |
| "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), |
| |
| "TOKEN_TYPE_CLAIM": "token_type", |
| |
| "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", |
| |
| "JTI_CLAIM": "jti", |
| |
| "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", |
| |
| "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5), |
| |
| "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), |
| |
| "TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer", |
| |
| "TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer", |
| |
| "TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer", |
| |
| "TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer", |
| |
| "SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer", |
| |
| "SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer", |
| } |
【2】基本使用
【2.1】使用SimpleJwt登录
- urls.py
token_obtain_pair
,路由中配置即可
| from django.urls import path, include |
| |
| from rest_framework_simplejwt.views import token_obtain_pair |
| |
| urlpatterns = [ |
| path('login/', token_obtain_pair), |
| ] |
【2.2】使用SimpleJwt认证
- 【注】进行认证时,携带的请求头格式由配置文件中配置


| |
| from rest_framework_simplejwt.authentication import JWTAuthentication |
| from rest_framework.permissions import IsAuthenticated |
| from rest_framework.viewsets import GenericViewSet |
| from .models import Book |
| from .ser import CommonSerializer |
| |
| |
| class CommonView(GenericViewSet): |
| queryset = Book.objects.all() |
| serializer_class = CommonSerializer |
| |
| authentication_classes = [JWTAuthentication] |
| |
| permission_classes = [IsAuthenticated] |
【3】基于SimpleJwt自定制
【3.1】定制登录成功返回格式
- 继承
TokenObtainPairSerializer
并重写validate
方法并修改返回值来控制返回格式
【3.1.1】重写validate
方法
【3.1.1.1】直接使用simple-jwt
的登录接口
| |
| from rest_framework_simplejwt.views import token_obtain_pair |
| |
| |
| urlpatterns = [ |
| path('login/', token_obtain_pair), |
| ] |
| |
| class LoginSerializer(TokenObtainPairSerializer): |
| ''' |
| 登录接口调用simple-jwt的序列化类,重写validate定制返回格式 |
| ''' |
| |
| def validate(self, attrs): |
| data = super().validate(attrs) |
| response = { |
| 'status': 100, |
| 'msg': '登录成功', |
| 'token': data.get('access') |
| } |
| return response |
【3.1.1.2】使用context
获取token
- 自定义登录接口
- 在序列化类中进行校验
- 由于序列化类继承
TokenObtainPairSerializer
,可以直接调用simple-jwt
的get_token方法
获得token 对象
- 将token 放在序列化类对象的
context属性
中
- 在
view
中通过ser.contex
t获取序列化类中放置的数据
| class UserView(GenericViewSet): |
| serializer_class = LoginSerializer |
| authentication_classes = [] |
| |
| @action(methods=['POST'], detail=False) |
| def login(self, request): |
| ser = self.get_serializer(data=request.data) |
| if ser.is_valid(): |
| token = ser.context['token'] |
| refresh = ser.context['refresh'] |
| return Response({'code': 100, 'msg': '登录成功', 'token': token, 'refresh': refresh}) |
| else: |
| return Response({'code': 101, 'msg': ser.errors}) |
| |
| class LoginSerializer(TokenObtainPairSerializer): |
| username = serializers.CharField() |
| password = serializers.CharField() |
| |
| |
| def validate(self, attrs): |
| username = attrs.get('username') |
| password = attrs.get('password') |
| user = User.objects.filter(username=username, password=password).first() |
| if not user: |
| raise ValidationError("用户名或密码错误") |
| |
| token = self.get_token(user) |
| self.context['refresh'] = str(token) |
| self.context['token'] = str(token.access_token) |
| return attrs |
【3.1.2】重写data
方法
| |
| class UserView(GenericViewSet): |
| |
| authentication_classes = [] |
| permission_classes = [] |
| serializer_class = RegisterSerializer |
| |
| @action(methods=['POST'], detail=False) |
| def register(self, request): |
| ser = self.get_serializer(data=request.data) |
| ser.is_valid(raise_exception=True) |
| ser.save() |
| return Response(ser.data) |
| |
| class RegisterSerializer(serializers.ModelSerializer): |
| class Meta: |
| model = UserInfo |
| fields ='__all__' |
| |
| def create(self, validated_data): |
| user = UserInfo.objects.create_user(**validated_data) |
| return user |
| |
| @property |
| def data(self): |
| |
| res = super().data |
| return {'code': 100, 'msg': '注册成功', 'result': res} |
【3.2】定制认证类
- 继承
JWTAuthentication
并重写authenticate
方法
- 由于继承了
JWTAuthentication
,所以可以直接调用原来进行验证的方法
| from rest_framework_simplejwt.authentication import JWTAuthentication |
| from rest_framework.exceptions import AuthenticationFailed |
| |
| |
| class CommonAuthentication(JWTAuthentication): |
| def authenticate(self, request): |
| token = self.get_header(request) |
| if not token: |
| raise AuthenticationFailed("请先登录或携带token信息") |
| payload = self.get_raw_token(token) |
| validated_token = self.get_validated_token(payload) |
| user = self.get_user(validated_token) |
| if not user.locked: |
| return user, validated_token |
| else: |
| raise AuthenticationFailed("当前用户已冻结") |
【4】多方式登陆
- 继承
TokenObtainPairSerializer
并重写validate
方法就能够实现手动校验
| class LoginSerializer(TokenObtainPairSerializer): |
| username = serializers.CharField() |
| password = serializers.CharField() |
| |
| def _get_user(self, username, password): |
| if re.match('^(13|14|15|18)[0-9]{9}$', username): |
| user = User.objects.filter(mobile=username, password=password).first() |
| elif re.match('^[a-z\d]+(\.[a-z\d]+)*@([\da-z](-[\da-z])?)+\.[a-z]+$', username): |
| user = User.objects.filter(email=username, password=password).first() |
| else: |
| user = User.objects.filter(username=username, password=password).first() |
| return user |
| |
| def get_token(cls, user): |
| token = super().get_token(user) |
| token['name'] = user.username |
| return token |
| |
| def validate(self, attrs): |
| username = attrs.get('username') |
| password = attrs.get('password') |
| user = self._get_user(username, password) |
| if not user: |
| raise ValidationError("用户名或密码错误") |
| token = self.get_token(user) |
| self.context['refresh'] = str(token) |
| self.context['token'] = str(token.access_token) |
| return attrs |
【三】自定义JWT认证
- 基于base64、md5和django密钥进行加密
- headers:使用base64对包含头信息的字典进行加密
- payload:载荷中包含用户的用户名和密码及其他信息
- Signature:签名由经过base64加密过的headers和payload并加入django密钥作为盐,由md5进行加密
- 由于md5和base64,严格来说都不是加密算法,同样的数据经过同样的加密方式获取的数据一致
- 通过后端加密的数据与前端传入的token进行比对
- 由于我们的django密钥是绝对私密的,经过比对,就能校验出该token是否由我们签发的
| |
| import hashlib |
| import json |
| import base64 |
| import time |
| from django.conf import settings |
| |
| |
| class CommonJWT: |
| def __init__(self, header=None, salt=None, expired=180): |
| ''' |
| 初始化token的参数 |
| ''' |
| if not header: |
| |
| self.header = header = {'alg': 'md5', 'type': 'JWT'} |
| self.header = header |
| |
| self.token_headers = self._encode_base64(header) |
| if not salt: |
| |
| salt = settings.SECRET_KEY |
| self.salt = salt.encode(encoding='utf-8') |
| self.create_time = time.time() |
| self.expired = expired |
| |
| @staticmethod |
| def _encode_base64(data: dict): |
| data_string = json.dumps(data).encode(encoding='utf8') |
| return base64.b64encode(data_string) |
| |
| @staticmethod |
| def _decode_base64(data): |
| return base64.b64decode(data) |
| |
| def _encrypt_md5(self, data): |
| md5 = hashlib.md5() |
| md5.update(data + self.salt) |
| return md5.hexdigest().encode(encoding='utf8') |
| |
| def create_token(self, create_time=None, **kwargs): |
| if not create_time: |
| create_time = self.create_time |
| |
| payload = kwargs |
| |
| payload.update({'expired': self.expired, 'create_time': create_time}) |
| |
| token_payload = self._encode_base64(payload) |
| |
| |
| signature = self._encrypt_md5(data=self.token_headers + token_payload) |
| |
| signature = base64.b64encode(signature) |
| |
| return b'.'.join((self.token_headers, token_payload, signature)) |
| |
| def get_payload_from_token(self, token): |
| |
| token_headers, token_payload, token_sign = token.split('.') |
| |
| payload = json.loads(self._decode_base64(token_payload)) |
| return payload |
| |
| |
| class UserView(ViewSetMixin, CreateAPIView): |
| queryset = UserInfo.objects.all() |
| serializer_class = UserInfoSerializer |
| authentication_classes = [] |
| |
| @action(methods=['post'], detail=False) |
| def login(self, request, *args, **kwargs): |
| username = request.data.get('username') |
| password = request.data.pop('password') |
| user = auth.authenticate(username=username, password=password) |
| if not user: |
| return Response({'code': 101, 'msg': '用户名或密码错误'}) |
| token = CommonJWT().create_token(**request.data) |
| return Response({'code': 100, 'msg': '登录成功', 'token': token}) |
| |
| class UserAuthentication(BaseAuthentication): |
| def authenticate(self, request): |
| jwt = CommonJWT() |
| token = request.META.get('HTTP_TOKEN') |
| if not token: |
| raise AuthenticationFailed("请先登录") |
| payload = jwt.get_payload_from_token(token=token) |
| create_time = payload.pop('create_time') |
| expired = payload.get('expired') |
| |
| |
| |
| username = payload.get('username') |
| user = UserInfo.objects.filter(username=username).first() |
| if not user: |
| raise AuthenticationFailed("token有误") |
| if time.time() - create_time > expired: |
| raise AuthenticationFailed("当前token已过期") |
| true_token = jwt.create_token(create_time=create_time, **payload) |
| if token.encode('utf-8') == true_token: |
| return user, token |
| else: |
| raise AuthenticationFailed("请检查token") |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了