DRF之JWT签发Token源码分析
【一】JWT介绍
- JWT(JSON Web Token)是一种用于身份认证和授权的开放标准(RFC 7519)。
- 它基于JSON格式定义了一种安全的令牌,用于在客户端和服务器之间传输信息。
【二】JWT三段式
- JWT(JSON Web Token)是一种用于身份认证和授权的开放标准(RFC 7519)。
- 它基于JSON格式定义了一种安全的令牌,用于在客户端和服务器之间传输信息。
JWT由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature):
- 头部通常由两部分组成:
- 令牌类型和算法。
- 令牌类型通常为"JWT"。
- 算法定义了用于生成签名的算法,例如HMAC、RSA或者ECDSA等。
- 示例:
| { |
| "alg": "HS256", |
| "typ": "JWT" |
| } |
【2】载荷(Payload):
- 载荷包含了关于用户或实体的声明和其他附加信息。
- JWT规范定义了一些标准的声明(例如:iss-签发者、exp-过期时间、sub-主题、aud-受众),并且允许自定义声明。
- 示例:
| { |
| "sub": "1234567890", |
| "name": "John Doe", |
| "admin": true |
| } |
【3】签名(Signature):
- 签名是将头部和载荷进行签名,以确保令牌的完整性和真实性。
- 签名通常使用密钥进行加密,以防止其被篡改。
- 服务器在接收到请求时使用同样的密钥对签名进行验证。
- 示例:
| HMACSHA256( |
| base64UrlEncode(header) + "." + |
| base64UrlEncode(payload), |
| secretKey |
| ) |
【4】最终的JWT
- 由上述三个部分用
.
拼成一个完整的字符串,构成最终的JWT
【三】JWT开发流程
【1】JWT工作流程如下:
- 用户向服务器发送登录请求,提供用户名和密码。
- 服务器验证用户凭证,如果通过验证,生成JWT并将其返回给客户端。
- 客户端收到JWT后,保存在本地(通常在本地存储或者Cookie中)。
- 客户端每次访问受限资源时,在请求头中附带JWT。
- 服务器接收到请求后,使用相同的密钥解析JWT,验证其有效性和完整性,然后根据需要执行相应的操作。
【2】JWT开发流程
第一部分:签发token的过程(登录)
- 用户携带用户名和密码,访问我,我们校验通过,生成token串,返回给前端
- 用户携带用户名和密码访问应用后端。
- 应用后端校验用户提供的用户名和密码是否正确。
- 如果校验通过,应用后端生成一个JWT Token,并将其返回给前端。
- JWT Token包含三个部分:Header、Payload和Signature。
- Header包含了关于Token类型和所使用的算法的信息。
- Payload包含了要传递的数据,例如用户ID、角色等。
- Signature是使用密钥对Header和Payload进行签名的结果,用于验证Token的真实性。
- 这些部分通常会经过Base64编码组合成一个字符串。
- 前端收到JWT Token后,可以将其保存在本地
- 例如LocalStorage或者Cookie中,在后续需要使用Token的请求中携带它。
第二部分:token认证过程
- token认证过程,登录认证时使用,其实就是咱们之前讲的认证类,在认证类中完成对token的认证操作
- 用户访问我们需要登陆后才能访问的接口,必须携带我们签发的token串(请求头)
- 我们取出token,验证该token是否过期,是否被篡改,是否是伪造的
- 如果正常,说明荷载中的数据,就是安全的,可以根据荷载中的用户id,查询出当前登录用户,放到request中即可
- 用户访问需要登录才能访问的接口,请求头中携带JWT Token。
- 后端从请求头中获取JWT Token。
- 后端对Token进行解析和验证,确保Token的完整性和真实性。
- 验证Token的完整性可以通过验签来实现,即使用密钥对Token的签名部分进行验证。
- 验证Token的真实性可以通过检查Token的有效期以及其他业务逻辑来实现。
- 如果Token验证通过,后端能够从Token的Payload部分获取到用户的相关信息,例如用户ID。
- 后端可以根据用户ID查询数据库或其他存储系统,获取该用户的详细信息。
- 后端将获取到的用户信息添加到请求的上下文中,以便后续的处理逻辑可以使用该信息进行权限控制、数据处理等操作。
【四】JWT的优点
- 简洁:由于使用JSON格式,JWT具有易读性和可理解性。
- 自包含:JWT中包含了所有必要的信息,减少服务器端的存储开销。
- 可扩展:JWT允许自定义声明来满足特定需求。
- 跨域支持:JWT可以在不同域之间进行传递,并实现跨域认证。
然而,使用JWT需要注意以下几点:
- JWT无法撤销:一旦JWT被签发,就无法主动撤销,只能等待过期时间到达或者通过其他方式进行处理。
- 令牌大小:由于JWT包含载荷信息,其大小较大,可能会影响网络传输和存储开销。
总结来说,JWT是一种灵活且安全的身份认证和授权机制,可以用于构建分布式系统和跨域认证场景。但在使用时需注意安全性和令牌的大小。
【五】djangorestframework-jwt自动签发token
【1】安装
| pip3 install djangorestframework-jwt |
【2】签发过程
- 登录过程(快速签发)
- 登录接口
- 在URL路由中添加登录接口路径,使用
obtain_jwt_token
函数进行用户身份验证并签发JWT Token。
- 基于 auth 的user表签发
| from django.contrib import admin |
| from django.urls import path |
| from rest_framework_jwt.views import obtain_jwt_token |
| |
| urlpatterns = [ |
| path('admin/', admin.site.urls), |
| path('login/', obtain_jwt_token), |
| ] |
【3】总结
【1】签发:只需要在路由中配置
| from rest_framework_jwt.views import obtain_jwt_token |
| urlpatterns = [ |
| path('login/', obtain_jwt_token), |
| ] |
- 在路由中配置登录接口路径,使用
obtain_jwt_token
函数进行用户身份验证并签发JWT Token。
【2】认证:视图类上加
| from rest_framework_jwt.authentication import JSONWebTokenAuthentication |
| from rest_framework.permissions import IsAuthenticated |
| |
| |
| class BookView(APIView): |
| authentication_classes = [JSONWebTokenAuthentication] |
| permission_classes = [IsAuthenticated] |
- 使用JWT认证方法需要在视图类中添加相应的认证类和权限类。
- 认证类:
JSONWebTokenAuthentication
,该类提供了JWT的认证功能。
- 权限类:
IsAuthenticated
,该类用于验证请求是否来自已认证的用户。
【4】Django + JWT 自定制返回格式
-
登录签发token的接口,要返回code,msg,username,token等信息
-
写个函数,函数返回字典格式,返回的格式,会被序列化,前端看到
| def common_response(token, user=None, request=None): |
| return { |
| 'code': '100', |
| 'msg': '登录成功', |
| 'username': user.username, |
| 'token': token, |
| } |
-
写的函数配置一下
| JWT_AUTH = { |
| 'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.jwt_response.common_response', |
| 'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300), |
| } |
【六】Django + JWT 自定义用户表签发认证(手动签发)
【1】签发部分
【1】创建用户表
| from django.db import models |
| |
| |
| |
| class User(models.Model): |
| username = models.CharField(max_length=32) |
| password = models.CharField(max_length=255) |
| age = models.IntegerField() |
【2】登录接口
| path('login/', views.UserView.as_view({"post": "login"})), |
| |
| from rest_framework.viewsets import ViewSet |
| from app01 import models |
| from rest_framework.response import Response |
| from rest_framework_jwt.settings import api_settings |
| |
| jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER |
| jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER |
| |
| |
| class UserView(ViewSet): |
| back_dict = {"code": 100, "msg": ""} |
| |
| def login(self, request): |
| username = request.data.get('username') |
| password = request.data.get('password') |
| user_obj = models.User.objects.filter(username=username, password=password).first() |
| if user_obj: |
| |
| |
| payload = jwt_payload_handler(user_obj) |
| print(payload) |
| |
| token = jwt_encode_handler(payload) |
| self.back_dict['code'] = 100 |
| self.back_dict['msg'] = "用户登录成功" |
| self.back_dict['username'] = user_obj.username |
| self.back_dict['token'] = token |
| return Response(self.back_dict) |
| else: |
| |
| self.back_dict['code'] = 102 |
| self.back_dict['msg'] = "用户名或密码错误" |
| return Response(self.back_dict) |
-
携带错误信息
| { |
| "username":"admin", |
| "password":521 |
| } |
| |
| { |
| "code": 102, |
| "msg": "用户名或密码错误" |
| } |
-
携带正确信息
| { |
| "username":"dream", |
| "password":521 |
| } |
| |
| |
| { |
| "code": 100, |
| "msg": "用户登录成功", |
| "username": "dream", |
| "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImRyZWFtIiwiZXhwIjoxNjkwODA0MDMzfQ.VGcEd0HkMH4aAG_2EoorOx90Rw8G5bPGe4eGWaaDgI4" |
| } |
【2】认证部分
| from rest_framework.authentication import BaseAuthentication |
| from rest_framework.exceptions import AuthenticationFailed |
| from rest_framework_jwt.settings import api_settings |
| import jwt |
| from app01 import models |
| |
| jwt_decode_handler = api_settings.JWT_DECODE_HANDLER |
| |
| |
| class JWTAuthentication(BaseAuthentication): |
| |
| def authenticate(self, request): |
| |
| token = request.META.get('HTTP_TOKEN') |
| |
| |
| |
| try: |
| payload = jwt_decode_handler(token) |
| |
| user_id = payload.get('user_id') |
| |
| |
| |
| |
| |
| |
| user = models.User(username=payload.get('username'), user_id=user_id) |
| except jwt.ExpiredSignature: |
| raise AuthenticationFailed("token超时") |
| except jwt.DecodeError: |
| raise AuthenticationFailed("解码失败") |
| except jwt.InvalidTokenError: |
| raise AuthenticationFailed("token异常") |
| except Exception: |
| raise AuthenticationFailed("token认证异常") |
| |
| return user, token |
| from app01.JWT_ap.jwt_authentication import JWTAuthentication |
| |
| |
| class UserInfoView(ViewSet): |
| back_dict = {"code": 100, "msg": ""} |
| authentication_classes = [JWTAuthentication] |
| |
| def login(self, request): |
| username = request.data.get('username') |
| password = request.data.get('password') |
| user_obj = models.User.objects.filter(username=username, password=password).first() |
| if user_obj: |
| |
| |
| payload = jwt_payload_handler(user_obj) |
| print( |
| payload) |
| |
| token = jwt_encode_handler(payload) |
| self.back_dict['code'] = 100 |
| self.back_dict['msg'] = "用户登录成功" |
| self.back_dict['username'] = user_obj.username |
| self.back_dict['token'] = token |
| return Response(self.back_dict) |
| else: |
| |
| self.back_dict['code'] = 102 |
| self.back_dict['msg'] = "用户名或密码错误" |
| return Response(self.back_dict) |
| path('books/', views.UserInfoView.as_view({"post": "login"})), |
【七】rest_framework_jwt源码分析
【1】流程分析
obtain_jwt_token
---->ObtainJSONWebToken.as_view()
| 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 JSONWebTokenAPIView(APIView): |
| """ |
| Base API View that various JWT interactions inherit from. |
| """ |
| |
| permission_classes = () |
| authentication_classes = () |
| |
| def get_serializer_context(self): |
| """ |
| Extra context provided to the serializer class. |
| """ |
| return { |
| 'request': self.request, |
| 'view': self, |
| } |
| |
| def get_serializer_class(self): |
| """ |
| Return the class to use for the serializer. |
| Defaults to using `self.serializer_class`. |
| You may want to override this if you need to provide different |
| serializations depending on the incoming request. |
| (Eg. admins get full serialization, others get basic serialization) |
| """ |
| assert self.serializer_class is not None, ( |
| "'%s' should either include a `serializer_class` attribute, " |
| "or override the `get_serializer_class()` method." |
| % self.__class__.__name__) |
| return self.serializer_class |
| |
| def get_serializer(self, *args, **kwargs): |
| """ |
| Return the serializer instance that should be used for validating and |
| deserializing input, and for serializing output. |
| """ |
| serializer_class = self.get_serializer_class() |
| kwargs['context'] = self.get_serializer_context() |
| return serializer_class(*args, **kwargs) |
| |
| |
| 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 |
| |
| return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) |
-
和
字段:
- 这两个字段用于定义API视图的权限和认证类,默认情况下为空元组,即没有任何限制和认证要求。
-
方法:
- 此方法返回给序列化器类的额外上下文信息。
- 默认情况下,提供了
request
和view
信息作为上下文。
-
方法:
- 该方法返回用于序列化的类。
- 默认情况下使用
self.serializer_class
字段的值。
- 如果需要根据请求的不同提供不同的序列化方式,可以重写此方法。
-
方法:
- 该方法返回一个序列化器的实例,用于验证、反序列化输入和序列化输出。
- 首先通过
get_serializer_class
方法获取序列化器类。
- 然后将额外的上下文信息传递给序列化器实例的
context
参数。
- 最后返回序列化器的实例。
-
方法:
- 这是处理POST请求的方法,负责签发JWT令牌。
- 首先通过
self.get_serializer(data=request.data)
获取序列化器的实例。
- 利用序列化器的
is_valid()
方法对请求数据进行校验,包括全局钩子和字段自定义的校验规则。
- 如果校验通过,从序列化器中获取用户对象和令牌对象。
- 接下来调用
jwt_response_payload_handler
方法自定义返回格式,并将令牌、用户和请求作为参数传递给它,获取返回数据字典。
- 创建一个响应对象,将返回数据作为内容进行响应。
- 如果配置文件中使用了JWT的cookie认证(
api_settings.JWT_AUTH_COOKIE
),则设置JWT的cookie,设置过期时间为当前时间加上JWT的过期时间差(api_settings.JWT_EXPIRATION_DELTA
)。
- 最后返回响应对象。
- 如果校验失败,返回错误信息和HTTP 400错误状态码的响应。
JSONWebTokenSerializer
的全局钩子/局部钩子
| 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) |
- 上述代码是一个JSONWebTokenSerializer的全局钩子(validate方法)。
- 在该代码段中,validate方法接收前端传入的经过验证的数据(attrs),然后提取用户名和密码,并进行用户认证。
- 首先,定义了一个credentials字典,其中包含用户名(self.username_field)和密码('password')。
- 通过attrs.get()方法从前端传入的数据中获取对应的值,并将其赋给credentials字典中的相应键。
- 接下来,使用all()函数判断credentials字典中的值是否全部存在(即用户名和密码都不为空)。
- 如果是,则调用authenticate()函数进行用户认证。
- authenticate()函数是在Django的auth模块中进行用户名和密码认证的函数。
- 它接收用户名和密码作为参数,在auth的user表中校验用户是否存在。
- 相当于执行了类似于User.objects.filter(username=username, password=加密后的密码).first()的查询,返回用户对象。
- 如果用户认证成功,继续进行进一步的操作。
- 首先判断用户是否激活,如果用户未激活,则抛出ValidationError异常,提示用户账户已被禁用。
- 接着,生成payload(有效载荷)对象,该对象包含了用户的信息。然后,通过jwt_encode_handler()函数对payload进行加密处理,得到一个token。
- 如果用户认证失败,则抛出ValidationError异常,提示无法使用提供的凭据登录。
- 如果credentials字典中的值存在空值(即用户名或密码为空),则抛出ValidationError异常,提醒必须包含用户名和密码。
- 总结:该全局钩子的作用是对前端传入的数据进行校验和用户认证,并返回包含token和用户信息的字典。如果验证或认证失败,则抛出相应的异常。
| jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER |
【2】源码分析
- BaseJSONWebTokenAuthentication
| from rest_framework_jwt.settings import api_settings |
| |
| |
| jwt_decode_handler = api_settings.JWT_DECODE_HANDLER |
| jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER |
| |
| |
| class BaseJSONWebTokenAuthentication(BaseAuthentication): |
| """ |
| Token based authentication using the JSON Web Token standard. |
| """ |
| |
| |
| def authenticate(self, request): |
| """ |
| Returns a two-tuple of `User` and token if a valid signature has been |
| supplied using JWT-based authentication. Otherwise returns `None`. |
| """ |
| |
| jwt_value = self.get_jwt_value(request) |
| if jwt_value is None: |
| return None |
| |
| try: |
| |
| payload = jwt_decode_handler(jwt_value) |
| except jwt.ExpiredSignature: |
| |
| msg = _('Signature has expired.') |
| raise exceptions.AuthenticationFailed(msg) |
| except jwt.DecodeError: |
| |
| msg = _('Error decoding signature.') |
| raise exceptions.AuthenticationFailed(msg) |
| except jwt.InvalidTokenError: |
| |
| raise exceptions.AuthenticationFailed() |
| |
| |
| user = self.authenticate_credentials(payload) |
| |
| |
| return (user, jwt_value) |
| |
| |
| def authenticate_credentials(self, payload): |
| """ |
| Returns an active user that matches the payload's user id and email. |
| """ |
| |
| User = get_user_model() |
| |
| username = jwt_get_username_from_payload(payload) |
| |
| |
| if not username: |
| msg = _('Invalid payload.') |
| raise exceptions.AuthenticationFailed(msg) |
| |
| try: |
| |
| user = User.objects.get_by_natural_key(username) |
| except User.DoesNotExist: |
| |
| msg = _('Invalid signature.') |
| raise exceptions.AuthenticationFailed(msg) |
| |
| |
| if not user.is_active: |
| msg = _('User account is disabled.') |
| raise exceptions.AuthenticationFailed(msg) |
| |
| |
| return user |
- JSONWebTokenAuthentication
| class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication): |
| """ |
| Clients should authenticate by passing the token key in the "Authorization" |
| HTTP header, prepended with the string specified in the setting |
| `JWT_AUTH_HEADER_PREFIX`. For example: |
| |
| Authorization: JWT eyJhbGciOiAiSFMyNTYiLCAidHlwIj |
| """ |
| |
| www_authenticate_realm = 'api' |
| |
| |
| def get_jwt_value(self, request): |
| |
| |
| auth = get_authorization_header(request).split() |
| |
| auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower() |
| |
| |
| if not auth: |
| |
| if api_settings.JWT_AUTH_COOKIE: |
| |
| return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE) |
| return None |
| |
| |
| if smart_text(auth[0].lower()) != auth_header_prefix: |
| return None |
| |
| |
| |
| if len(auth) == 1: |
| msg = _('Invalid Authorization header. No credentials provided.') |
| raise exceptions.AuthenticationFailed(msg) |
| elif len(auth) > 2: |
| msg = _('Invalid Authorization header. Credentials string ' |
| 'should not contain spaces.') |
| raise exceptions.AuthenticationFailed(msg) |
| |
| |
| return auth[1] |
| |
| def authenticate_header(self, request): |
| """ |
| Return a string to be used as the value of the `WWW-Authenticate` |
| header in a `401 Unauthenticated` response, or `None` if the |
| authentication scheme should return `403 Permission Denied` responses. |
| """ |
| |
| |
| return '{0} realm="{1}"'.format(api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm) |
【八】djangorestframework-simplejwt源码分析(自动签发token)
【1】流程分析
(1)安装 djangorestframework-simplejwt
模块
| pip install djangorestframework-simplejwt |
(2)settings文件注册APP
| from datetime import timedelta |
| |
| |
| INSTALLED_APPS = [ |
| ... |
| |
| 'rest_framework_simplejwt', |
| ... |
| ] |
| |
| |
| AUTHENTICATION_BACKENDS = [ |
| 'DreamShopApi.utils.common_authentications.Authentice', |
| ] |
| |
| |
| |
| SIMPLE_JWT = { |
| |
| "ACCESS_TOKEN_LIFETIME":timedelta(minutes=5), |
| } |
| |
| |
| SIMPLE_JWT = { |
| |
| 'ACCESS_TOKEN_LIFETIME': datetime.timedelta(seconds=10), |
| |
| 'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=1), |
| |
| |
| 'ROTATE_REFRESH_TOKENS': False, |
| |
| 'BLACKLIST_AFTER_ROTATION': True, |
| |
| |
| |
| 'ALGORITHM': 'HS256', |
| |
| 'SIGNING_KEY': SECRET_KEY, |
| |
| 'VERIFYING_KEY': None, |
| |
| 'AUDIENCE': None, |
| |
| 'ISSUER': None, |
| |
| |
| 'AUTH_HEADER_TYPES': ("Bearer"), |
| |
| "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", |
| |
| Bearer < token > |
| |
| 'USER_ID_FIELD': "id", |
| |
| "USER_ID_CLAIM": "user_id" |
| } |
(3)路由配置
| from rest_framework_simplejwt.views import TokenRefreshView,TokenVerifyView |
| |
| path('login_admin/', AutoTokenLoginView.as_view()), |
| |
| path('token/refresh/', TokenRefreshView.as_view()), |
| |
| path('token/verify/', TokenVerifyView.as_view()), |
(4)视图函数
| class AutoTokenLoginView(TokenObtainPairView): |
| |
| def post(self, request, *args, **kwargs): |
| |
| serializer = self.get_serializer(data=request.data) |
| try: |
| |
| |
| serializer.is_valid(raise_exception=True) |
| except Exception as e: |
| raise InvalidToken(e.args[0]) |
| result = serializer.validated_data |
| |
| id = serializer.user.id |
| phone = serializer.user.phone |
| email = serializer.user.email |
| username = serializer.user.username |
| token = result.pop("access") |
| re_token = result.pop("refresh") |
| return CommonResponse(msg="成功了", id=id, username=username, |
| phone=phone, email=email, |
| token=token, re_token=re_token, |
| status=status.HTTP_200_OK) |
【2】源码分析
(1)api_settings
| from datetime import timedelta |
| from typing import Any, Dict |
| |
| from django.conf import settings |
| from django.test.signals import setting_changed |
| from django.utils.translation import gettext_lazy as _ |
| from rest_framework.settings import APISettings as _APISettings |
| |
| from .utils import format_lazy |
| |
| USER_SETTINGS = getattr(settings, "SIMPLE_JWT", None) |
| |
| |
| DEFAULTS = { |
| |
| "ACCESS_TOKEN_LIFETIME": timedelta(minutes=5), |
| |
| |
| "REFRESH_TOKEN_LIFETIME": timedelta(days=1), |
| |
| |
| "ROTATE_REFRESH_TOKENS": False, |
| |
| |
| "BLACKLIST_AFTER_ROTATION": False, |
| |
| |
| "UPDATE_LAST_LOGIN": False, |
| |
| |
| "ALGORITHM": "HS256", |
| |
| |
| "SIGNING_KEY": settings.SECRET_KEY, |
| |
| |
| "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", |
| |
| |
| "JTI_CLAIM": "jti", |
| |
| |
| "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", |
| |
| |
| "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", |
| |
| |
| "CHECK_REVOKE_TOKEN": False, |
| |
| |
| "REVOKE_TOKEN_CLAIM": "hash_password", |
| } |
| |
| |
| IMPORT_STRINGS = ( |
| |
| "AUTH_TOKEN_CLASSES", |
| |
| |
| "JSON_ENCODER", |
| |
| |
| "TOKEN_USER_CLASS", |
| |
| |
| "USER_AUTHENTICATION_RULE", |
| ) |
| |
| REMOVED_SETTINGS = ( |
| |
| "AUTH_HEADER_TYPE", |
| |
| |
| "AUTH_TOKEN_CLASS", |
| |
| |
| "SECRET_KEY", |
| |
| |
| "TOKEN_BACKEND_CLASS", |
| ) |
| |
| |
| |
| class APISettings(_APISettings): |
| |
| |
| def __check_user_settings(self, user_settings: Dict[str, Any]) -> Dict[str, Any]: |
| |
| |
| SETTINGS_DOC = "https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html" |
| |
| |
| for setting in REMOVED_SETTINGS: |
| |
| if setting in user_settings: |
| |
| |
| raise RuntimeError( |
| format_lazy( |
| _( |
| "The '{}' setting has been removed. Please refer to '{}' for available settings." |
| ), |
| setting, |
| SETTINGS_DOC, |
| ) |
| ) |
| |
| return user_settings |
| |
| |
| |
| api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) |
| |
| |
| def reload_api_settings(*args, **kwargs) -> None: |
| global api_settings |
| |
| setting, value = kwargs["setting"], kwargs["value"] |
| |
| |
| if setting == "SIMPLE_JWT": |
| |
| api_settings = APISettings(value, DEFAULTS, IMPORT_STRINGS) |
| |
| |
| setting_changed.connect(reload_api_settings) |
(2)views
| class TokenViewBase(generics.GenericAPIView): |
| |
| |
| permission_classes = () |
| authentication_classes = () |
| |
| |
| |
| serializer_class = None |
| _serializer_class = "" |
| |
| |
| www_authenticate_realm = "api" |
| |
| |
| |
| def get_serializer_class(self) -> Serializer: |
| """ |
| If serializer_class is set, use it directly. Otherwise get the class from settings. |
| """ |
| |
| if self.serializer_class: |
| return self.serializer_class |
| try: |
| |
| return import_string(self._serializer_class) |
| except ImportError: |
| msg = "Could not import serializer '%s'" % self._serializer_class |
| raise ImportError(msg) |
| |
| |
| |
| def get_authenticate_header(self, request: Request) -> str: |
| return '{} realm="{}"'.format( |
| AUTH_HEADER_TYPES[0], |
| self.www_authenticate_realm, |
| ) |
| |
| |
| def post(self, request: Request, *args, **kwargs) -> Response: |
| |
| |
| serializer = self.get_serializer(data=request.data) |
| |
| try: |
| |
| serializer.is_valid(raise_exception=True) |
| except TokenError as e: |
| |
| raise InvalidToken(e.args[0]) |
| |
| return Response(serializer.validated_data, status=status.HTTP_200_OK) |
| |
| |
| class TokenObtainPairView(TokenViewBase): |
| """ |
| # 这意味着 TokenObtainPairView 类继承了 TokenViewBase 类的属性和方法。 |
| # TokenViewBase 类似乎是一个通用的基础视图,用于处理令牌相关的操作。 |
| Takes a set of user credentials and returns an access and refresh JSON web |
| token pair to prove the authentication of those credentials. |
| """ |
| |
| |
| |
| _serializer_class = api_settings.TOKEN_OBTAIN_SERIALIZER |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| token_obtain_pair = TokenObtainPairView.as_view() |
| |
| |
| class TokenRefreshView(TokenViewBase): |
| """ |
| # TokenRefreshView 类继承了 TokenViewBase 类的属性和方法。TokenViewBase 类似乎是一个通用的基础视图,用于处理令牌相关的操作 |
| Takes a refresh type JSON web token and returns an access type JSON web |
| token if the refresh token is valid. |
| """ |
| |
| |
| |
| _serializer_class = api_settings.TOKEN_REFRESH_SERIALIZER |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| token_refresh = TokenRefreshView.as_view() |
| |
| |
| class TokenObtainSlidingView(TokenViewBase): |
| """ |
| # TokenObtainSlidingView 类继承了 TokenViewBase 类的属性和方法。 |
| # TokenViewBase 类似乎是一个通用的基础视图,用于处理令牌相关的操作。 |
| Takes a set of user credentials and returns a sliding JSON web token to |
| prove the authentication of those credentials. |
| """ |
| |
| |
| |
| _serializer_class = api_settings.SLIDING_TOKEN_OBTAIN_SERIALIZER |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| token_obtain_sliding = TokenObtainSlidingView.as_view() |
| |
| |
| class TokenRefreshSlidingView(TokenViewBase): |
| """ |
| Takes a sliding JSON web token and returns a new, refreshed version if the |
| token's refresh period has not expired. |
| """ |
| |
| |
| |
| _serializer_class = api_settings.SLIDING_TOKEN_REFRESH_SERIALIZER |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| token_refresh_sliding = TokenRefreshSlidingView.as_view() |
| |
| |
| class TokenVerifyView(TokenViewBase): |
| """ |
| Takes a token and indicates if it is valid. This view provides no |
| information about a token's fitness for a particular use. |
| """ |
| |
| |
| |
| _serializer_class = api_settings.TOKEN_VERIFY_SERIALIZER |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| token_verify = TokenVerifyView.as_view() |
| |
| |
| class TokenBlacklistView(TokenViewBase): |
| """ |
| Takes a token and blacklists it. Must be used with the |
| `rest_framework_simplejwt.token_blacklist` app installed. |
| """ |
| |
| |
| |
| |
| _serializer_class = api_settings.TOKEN_BLACKLIST_SERIALIZER |
| |
| |
| |
| |
| |
| |
| |
| |
| token_blacklist = TokenBlacklistView.as_view() |
(3)serializers
| |
| AuthUser = TypeVar("AuthUser", AbstractBaseUser, TokenUser) |
| |
| |
| if api_settings.BLACKLIST_AFTER_ROTATION: |
| from .token_blacklist.models import BlacklistedToken |
| |
| |
| |
| class PasswordField(serializers.CharField): |
| def __init__(self, *args, **kwargs) -> None: |
| kwargs.setdefault("style", {}) |
| |
| kwargs["style"]["input_type"] = "password" |
| |
| kwargs["write_only"] = True |
| |
| super().__init__(*args, **kwargs) |
| |
| |
| class TokenObtainSerializer(serializers.Serializer): |
| |
| username_field = get_user_model().USERNAME_FIELD |
| |
| token_class: Optional[Type[Token]] = None |
| |
| |
| default_error_messages = { |
| "no_active_account": _("No active account found with the given credentials") |
| } |
| |
| def __init__(self, *args, **kwargs) -> None: |
| super().__init__(*args, **kwargs) |
| |
| |
| self.fields[self.username_field] = serializers.CharField(write_only=True) |
| self.fields["password"] = PasswordField() |
| |
| |
| def validate(self, attrs: Dict[str, Any]) -> Dict[Any, Any]: |
| authenticate_kwargs = { |
| self.username_field: attrs[self.username_field], |
| "password": attrs["password"], |
| } |
| try: |
| authenticate_kwargs["request"] = self.context["request"] |
| except KeyError: |
| pass |
| |
| self.user = authenticate(**authenticate_kwargs) |
| |
| |
| if not api_settings.USER_AUTHENTICATION_RULE(self.user): |
| raise exceptions.AuthenticationFailed( |
| self.error_messages["no_active_account"], |
| "no_active_account", |
| ) |
| |
| return {} |
| |
| |
| @classmethod |
| def get_token(cls, user: AuthUser) -> Token: |
| return cls.token_class.for_user(user) |
| |
| |
| |
| class TokenObtainPairSerializer(TokenObtainSerializer): |
| token_class = RefreshToken |
| |
| def validate(self, attrs: Dict[str, Any]) -> Dict[str, str]: |
| data = super().validate(attrs) |
| |
| refresh = self.get_token(self.user) |
| |
| data["refresh"] = str(refresh) |
| data["access"] = str(refresh.access_token) |
| |
| |
| if api_settings.UPDATE_LAST_LOGIN: |
| update_last_login(None, self.user) |
| |
| return data |
| |
| |
| class TokenObtainSlidingSerializer(TokenObtainSerializer): |
| token_class = SlidingToken |
| |
| def validate(self, attrs: Dict[str, Any]) -> Dict[str, str]: |
| data = super().validate(attrs) |
| |
| token = self.get_token(self.user) |
| |
| data["token"] = str(token) |
| |
| |
| if api_settings.UPDATE_LAST_LOGIN: |
| update_last_login(None, self.user) |
| |
| return data |
| |
| |
| class TokenRefreshSerializer(serializers.Serializer): |
| refresh = serializers.CharField() |
| access = serializers.CharField(read_only=True) |
| token_class = RefreshToken |
| |
| def validate(self, attrs: Dict[str, Any]) -> Dict[str, str]: |
| refresh = self.token_class(attrs["refresh"]) |
| |
| data = {"access": str(refresh.access_token)} |
| |
| |
| if api_settings.ROTATE_REFRESH_TOKENS: |
| if api_settings.BLACKLIST_AFTER_ROTATION: |
| try: |
| |
| refresh.blacklist() |
| except AttributeError: |
| |
| pass |
| |
| refresh.set_jti() |
| refresh.set_exp() |
| refresh.set_iat() |
| |
| data["refresh"] = str(refresh) |
| |
| return data |
| |
| |
| class TokenRefreshSlidingSerializer(serializers.Serializer): |
| token = serializers.CharField() |
| token_class = SlidingToken |
| |
| def validate(self, attrs: Dict[str, Any]) -> Dict[str, str]: |
| token = self.token_class(attrs["token"]) |
| |
| |
| token.check_exp(api_settings.SLIDING_TOKEN_REFRESH_EXP_CLAIM) |
| |
| |
| token.set_exp() |
| token.set_iat() |
| |
| return {"token": str(token)} |
| |
| |
| class TokenVerifySerializer(serializers.Serializer): |
| token = serializers.CharField(write_only=True) |
| |
| def validate(self, attrs: Dict[str, None]) -> Dict[Any, Any]: |
| token = UntypedToken(attrs["token"]) |
| |
| |
| if ( |
| api_settings.BLACKLIST_AFTER_ROTATION |
| and "rest_framework_simplejwt.token_blacklist" in settings.INSTALLED_APPS |
| ): |
| jti = token.get(api_settings.JTI_CLAIM) |
| if BlacklistedToken.objects.filter(token__jti=jti).exists(): |
| raise ValidationError("Token is blacklisted") |
| |
| return {} |
| |
| |
| class TokenBlacklistSerializer(serializers.Serializer): |
| refresh = serializers.CharField(write_only=True) |
| token_class = RefreshToken |
| |
| def validate(self, attrs: Dict[str, Any]) -> Dict[Any, Any]: |
| refresh = self.token_class(attrs["refresh"]) |
| try: |
| refresh.blacklist() |
| except AttributeError: |
| pass |
| return {} |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通