DRF SimpleJWT深度定制实战:从Token载荷到响应结构的全链路重构
如果你正在使用Django REST framework构建企业级API,并且已经引入了SimpleJWT来处理认证,那么你很可能遇到过这样的困境:默认的JWT令牌只包含最基本的user_id信息,而登录接口返回的响应结构又过于简单,无法满足前端团队对用户数据和状态码规范化的要求。这不是SimpleJWT的缺陷,而是它为了保持简洁性所做的设计选择。但现实中的项目需求往往更加复杂,我们需要在保持JWT安全性的同时,对其进行深度定制。
这篇文章将带你深入SimpleJWT的内部机制,通过实际代码演示如何解决两个核心问题:一是在JWT令牌中嵌入额外的用户信息(如用户名、角色、权限等),二是完全重构登录接口的响应结构,使其符合企业级的API设计规范。我不会仅仅停留在表面配置,而是会剖析SimpleJWT的工作流程,让你理解每一步修改背后的原理,从而能够灵活应对各种定制需求。
1. 理解SimpleJWT的核心架构与定制入口
在开始动手修改之前,我们需要先搞清楚SimpleJWT是如何工作的。很多人直接跳进代码修改,却不知道为什么这样改,结果遇到各种奇怪的问题。SimpleJWT的架构其实相当清晰,它主要由几个关键组件构成:
- Token类:负责JWT令牌的生成、验证和解析
- Serializer类:处理请求数据的验证和响应数据的构建
- View类:提供标准的API端点
- Authentication类:集成到DRF的认证系统中
当我们谈论“定制”时,主要是在两个层面进行操作:一是定制Token的载荷(payload),二是定制认证流程的序列化器(Serializer)。前者决定了令牌中包含什么信息,后者决定了API接口的输入输出格式。
1.1 SimpleJWT的默认行为分析
让我们先看看SimpleJWT默认生成的JWT令牌是什么样子的。如果你使用默认配置,一个典型的访问令牌(access token)解码后可能包含以下信息:
{
"token_type": "access",
"exp": 1691234567,
"iat": 1691234507,
"jti": "a1b2c3d4e5f6g7h8",
"user_id": 42
}
这个结构非常精简,只包含了JWT标准声明和最基本的用户标识。对于很多应用来说,这已经足够了,但考虑以下场景:
- 前端需要在本地显示当前登录用户的用户名,而不想额外发起一次用户信息查询
- 权限检查需要在API网关或中间件层面进行,需要从令牌中直接读取用户角色
- 审计日志需要记录操作者的详细信息,而不仅仅是ID
在这些情况下,我们都需要在令牌中添加额外的信息。但这里有一个重要的安全原则需要记住:JWT令牌不应该包含敏感信息,因为令牌本身虽然经过签名,但内容是可以被解码查看的(只是不能篡改)。所以,我们只应该添加必要的、非敏感的用户信息。
1.2 配置基础环境
在开始定制之前,确保你的项目已经正确配置了SimpleJWT。如果你还没有安装,可以通过以下命令安装:
pip install djangorestframework-simplejwt
然后在settings.py中进行基本配置:
# settings.py
INSTALLED_APPS = [
# ...
'rest_framework',
'rest_framework_simplejwt',
# ...
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
],
}
# SimpleJWT基础配置
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY, # 使用Django的SECRET_KEY
'VERIFYING_KEY': None,
'AUTH_HEADER_TYPES': ('Bearer',),
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
}
注意:在生产环境中,建议为JWT使用独立的签名密钥,而不是直接使用Django的SECRET_KEY。这可以通过设置
SIGNING_KEY为另一个值来实现。
2. 深度定制JWT令牌载荷
现在进入第一个核心主题:如何在JWT令牌中添加额外的用户信息。SimpleJWT提供了清晰的扩展点,但需要正确理解它的继承关系和工作流程。
2.1 理解Token的生成流程
SimpleJWT中令牌的生成主要发生在TokenObtainPairSerializer的get_token方法中。默认情况下,这个方法创建了一个AccessToken实例,并将用户ID添加到令牌载荷中。如果我们想要添加更多信息,就需要重写这个方法。
让我们创建一个自定义的序列化器来扩展这个行为。首先在适当的应用下创建serializers.py文件(如果还没有的话):
# users/serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.tokens import Token
from django.contrib.auth import get_user_model
User = get_user_model()
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
"""
自定义令牌获取序列化器,用于在JWT令牌中添加额外用户信息
"""
@classmethod
def get_token(cls, user: User) -> Token:
"""
重写get_token方法,添加自定义声明
"""
# 调用父类方法获取基础令牌
token = super().get_token(user)
# 添加自定义声明
# 这些信息将被编码到JWT令牌中
token['username'] = user.username
token['email'] = user.email
token['is_staff'] = user.is_staff
token['is_active'] = user.is_active
# 如果需要添加更多业务相关字段
# 例如用户角色、部门等信息
# token['role'] = user.profile.role if hasattr(user, 'profile') else 'user'
# token['department'] = user.department.name if user.department else None
return token
这段代码的关键点在于get_token方法。我们首先调用父类的方法获取基础令牌(包含user_id等标准声明),然后向令牌对象添加额外的键值对。这些键值对将成为JWT载荷的一部分。
2.2 处理用户模型扩展字段
在实际项目中,用户信息往往不止存在于基础的User模型中。我们可能通过OneToOne关系扩展了用户资料,或者有相关的业务模型。在令牌中添加这些信息需要特别注意性能问题。
考虑以下两种场景的处理方式:
场景一:直接关联字段 如果信息在User模型或通过属性可以快速访问,可以直接添加到令牌中:
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
# 添加用户全名(如果模型中有first_name和last_name字段)
full_name = f"{user.first_name} {user.last_name}".strip()
if full_name:
token['full_name'] = full_name
else:
token['full_name'] = user.username
# 添加最后登录时间
if user.last_login:
token['last_login'] = user.last_login.isoformat()
return token
场景二:需要关联查询的字段 对于需要通过关系查询的字段,我们需要考虑性能影响。JWT令牌在每次请求时都会被验证,但生成令牌只在登录时发生一次,所以可以接受稍微复杂一点的查询:
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
# 假设用户有一个关联的Profile模型
try:
profile = user.profile
token['avatar'] = profile.avatar_url
token['phone'] = profile.phone
token['title'] = profile.job_title
# 添加用户权限组信息
groups = user.groups.all().values_list('name', flat=True)
token['groups'] = list(groups)
except AttributeError:
# 处理没有profile的情况
token['avatar'] = None
token['phone'] = None
token['title'] = None
token['groups'] = []
return token
提示:虽然可以在令牌中添加较多信息,但需要记住JWT令牌会随着每个请求发送,过大的令牌会增加网络开销。通常建议将令牌大小控制在4KB以内。
2.3 配置自定义序列化器
创建好自定义序列化器后,需要告诉SimpleJWT使用它。有两种配置方式:
方式一:通过SIMPLE_JWT设置(推荐用于快速集成)
# settings.py
SIMPLE_JWT = {
# ... 其他配置保持不变
# 指定自定义的令牌获取序列化器
'TOKEN_OBTAIN_SERIALIZER': 'users.serializers.CustomTokenObtainPairSerializer',
}
方式二:创建自定义视图并配置URL(推荐用于复杂定制)
# users/views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from .serializers import CustomTokenObtainPairSerializer
class CustomTokenObtainPairView(TokenObtainPairView):
"""
自定义令牌获取视图,使用自定义序列化器
"""
serializer_class = CustomTokenObtainPairSerializer
然后在URL配置中使用这个视图:
# urls.py
from django.urls import path
from users.views import CustomTokenObtainPairView
from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView
urlpatterns = [
# ... 其他URL模式
path('api/auth/login/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('api/auth/verify/', TokenVerifyView.as_view(), name='token_verify'),
]
2.4 验证自定义令牌
配置完成后,让我们测试一下自定义令牌是否正常工作。首先通过登录接口获取令牌:
curl -X POST http://localhost:8000/api/auth/login/ \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "testpass"}'
你会得到类似这样的响应:
{
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}
现在,我们可以使用Python解码访问令牌,查看其中包含的自定义信息:
import jwt
from django.conf import settings
# 假设获取到的access令牌
access_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
# 解码令牌(不验证签名,仅查看内容)
decoded = jwt.decode(access_token, options={"verify_signature": False})
print(decoded)
输出应该包含我们添加的自定义字段:
{
"token_type": "access",
"exp": 1691234567,
"iat": 1691234507,
"jti": "a1b2c3d4e5f6g7h8",
"user_id": 42,
"username": "testuser",
"email

1103

被折叠的 条评论
为什么被折叠?



