Django 无username字段用户模型与 JWT 认证集成解决方案
Django 无username字段用户模型与 JWT 认证集成解决方案文档
一、问题背景
在使用 Django REST Framework 结合 SimpleJWT 实现用户认证时,若用户模型移除了默认的username字段(例如使用mobile或email作为登录标识),可能会导致以下问题:
- SimpleJWT 默认依赖username字段进行用户认证,无username字段时认证失败;
- UUID 主键(带/不带连字符)查询异常(实际已验证 Django ORM 可正确处理,但需确保认证流程适配)。
本文档提供完整解决方案,适配无username字段的用户模型,并确保 JWT 认证流程正常工作。
二、关键发现
通过测试验证:
- Django ORM 对 UUID 的兼容性:无论 UUID 带连字符(如7c8417bb-9ba3-4ec0-866b-ca9324848693)还是不带连字符(如7c8417bb9ba34ec0866bca9324848693),ORM 均可正确查询用户;
- 问题核心:SimpleJWT 默认使用username字段进行用户标识,当用户模型移除username时,认证流程无法找到用户。
三、完整解决方案
3.1 自定义用户模型(无username字段)
修改用户模型,移除username字段,使用mobile或email作为登录标识,并配置 UUID 主键。
3.1.1 模型定义(models.py)
from django.contrib.auth.models import AbstractUser
from django.db import models
import uuid
from .managers import UserManager # 导入自定义用户管理器
class User(AbstractUser):
# 移除默认的 username 字段
username = None
# UUID 主键(唯一标识用户)
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False,
unique=True,
verbose_name='用户唯一标识'
)
# 登录标识字段(手机号/邮箱)
mobile = models.CharField(
max_length=15,
unique=True,
verbose_name='手机号'
)
email = models.EmailField(
unique=True,
verbose_name='邮箱'
)
# 设置认证字段(使用手机号作为登录标识)
USERNAME_FIELD = 'mobile' # 也可设置为 'email'
REQUIRED_FIELDS = [] # 创建超级用户时无需额外字段(如无需求可留空)
# 关联自定义用户管理器
objects = UserManager()
class Meta:
verbose_name = '用户'
verbose_name_plural = '用户'
3.1.2 自定义用户管理器(managers.py)
处理无username字段的用户创建逻辑:
from django.contrib.auth.models import BaseUserManager
from django.utils.translation import gettext_lazy as _
class UserManager(BaseUserManager):
def create_user(self, mobile, password=None, **extra_fields):
"""
使用手机号创建普通用户
"""
if not mobile:
raise ValueError(_('必须提供手机号'))
# 标准化手机号(可选,根据业务需求)
mobile = mobile.strip()
user = self.model(mobile=mobile, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, mobile, password, **extra_fields):
"""
创建超级用户(需设置管理员权限)
"""
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError(_('超级用户必须设置 is_staff=True'))
if extra_fields.get('is_superuser') is not True:
raise ValueError(_('超级用户必须设置 is_superuser=True'))
return self.create_user(mobile, password, **extra_fields)
3.2 配置 SimpleJWT(settings.py)
指定 JWT 中存储用户标识的字段(使用 UUID 主键):
# settings.py
from datetime import timedelta
SIMPLE_JWT = {
# 令牌生命周期配置(可选,根据业务需求调整)
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
# 关键配置:指定用户标识字段
'USER_ID_FIELD': 'id', # 用户模型中的唯一标识字段(UUID 主键)
'USER_ID_CLAIM': 'user_id', # JWT 中存储用户标识的字段名(需与令牌负载一致)
}
# 注册自定义用户模型
AUTH_USER_MODEL = 'users.User' # 格式:'应用名.模型名'
3.3 自定义 JWT 认证后端(适配无username模型)
覆盖默认的JWTAuthentication类,直接通过 UUID 主键查询用户:
3.3.1 认证类实现(auth.py)
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.exceptions import AuthenticationFailed, InvalidToken
from rest_framework_simplejwt.settings import api_settings
from django.contrib.auth import get_user_model
from django.utils.translation import gettext as _
class CustomJWTAuthentication(JWTAuthentication):
def get_user(self, validated_token):
"""
自定义用户获取逻辑(通过 UUID 主键查询用户)
"""
try:
# 从令牌负载中获取用户标识(对应 settings.py 中的 USER_ID_CLAIM)
user_id = validated_token.get(api_settings.USER_ID_CLAIM)
if not user_id:
raise InvalidToken(_('令牌中缺少用户标识字段'))
User = get_user_model() # 获取自定义用户模型
# 通过 UUID 主键查询用户(支持带/不带连字符的格式)
return User.objects.get(id=user_id)
except User.DoesNotExist:
raise AuthenticationFailed(_('用户不存在'))
except Exception as e:
raise InvalidToken(_('认证失败: ') + str(e))
3.3.2 注册自定义认证类(settings.py)
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'apps.users.auth.CustomJWTAuthentication', # 替换默认认证类
],
}
3.4 调试视图(验证认证流程)
添加调试端点,输出用户模型信息和认证结果,辅助排查问题:
# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework_simplejwt.tokens import AccessToken
from .auth import CustomJWTAuthentication
from .models import User
class AuthDebugView(APIView):
permission_classes = [AllowAny] # 调试接口,允许未认证访问
def get(self, request):
auth_info = {
"user_model_info": {
"id_field": "id (UUID)",
"login_fields": ["mobile", "email"],
"has_username": False
},
"status": "unauthenticated"
}
# 尝试从请求头中提取令牌(可选,仅调试使用)
auth_header = request.headers.get('Authorization')
if auth_header:
try:
# 解析令牌并验证
token = auth_header.split(' ')[1] # 提取 Bearer 后的令牌部分
validated_token = AccessToken(token)
# 使用自定义认证类获取用户
authenticator = CustomJWTAuthentication()
user = authenticator.get_user(validated_token)
# 更新认证信息
auth_info.update({
"status": "valid",
"token_payload": dict(validated_token),
"user_info": {
"id": str(user.id),
"mobile": user.mobile,
"email": user.email,
"is_authenticated": user.is_authenticated
}
})
except Exception as e:
auth_info["error"] = str(e)
auth_info["status"] = "invalid"
return Response(auth_info)
调试接口路由配置(urls.py)
from django.urls import path
from .views import AuthDebugView
urlpatterns = [
path('api/auth/debug/', AuthDebugView.as_view(), name='auth_debug'),
]
四、验证步骤
4.1 创建测试用户(Django Shell)
# 进入 Django Shell
python manage.py shell
# 创建用户
from apps.users.models import User
user = User.objects.create_user(
mobile='13995030596', # 替换为实际手机号
password='your_password' # 设置密码
)
print(f"用户 ID: {user.id}") # 输出 UUID(如 7c8417bb-9ba3-4ec0-866b-ca9324848693)
4.2 生成并验证令牌(Django Shell)
from rest_framework_simplejwt.tokens import AccessToken
from apps.users.auth import CustomJWTAuthentication
# 生成用户令牌
token = AccessToken.for_user(user)
print(f"访问令牌: {token}") # 输出格式:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# 使用自定义认证类验证令牌
authenticator = CustomJWTAuthentication()
validated_user = authenticator.get_user(token)
print(f"认证用户手机号: {validated_user.mobile}") # 应输出 13995030596
4.3 测试调试接口(Postman/Apipost)
- 请求 URL:http://localhost:8000/api/auth/debug/
- 请求方法:GET
- 请求头:Authorization: Bearer <生成的令牌>
预期成功响应:
{
"user_model_info": {
"id_field": "id (UUID)",
"login_fields": ["mobile", "email"],
"has_username": false
},
"status": "valid",
"token_payload": {
"user_id": "7c8417bb-9ba3-4ec0-866b-ca9324848693",
"exp": 1751393603,
"iat": 1751389703,
"jti": "a1b2c3d4-5678-90ef-ghij-klmnopqrstuv",
"token_type": "access"
},
"user_info": {
"id": "7c8417bb-9ba3-4ec0-866b-ca9324848693",
"mobile": "13995030596",
"email": null,
"is_authenticated": true
}
}
失败响应示例(令牌无效):
{
"user_model_info": {
"id_field": "id (UUID)",
"login_fields": ["mobile", "email"],
"has_username": false
},
"status": "invalid",
"error": "Token has expired"
}
五、关键点说明
1. 用户标识字段:
使用 UUID 主键(id字段)作为用户唯一标识,JWT 中通过user_id字段存储,确保跨服务一致性。
2. 认证流程适配:
自定义认证类CustomJWTAuthentication直接通过 UUID 主键查询用户,避免依赖username字段。
3. 模型兼容性:
移除username字段后,通过USERNAME_FIELD指定登录标识(如mobile),并使用自定义用户管理器处理用户创建。
4. 调试支持:
调试接口AuthDebugView输出用户模型元数据、令牌负载和用户信息,方便快速定位认证问题(如令牌过期、用户不存在)。
通过本方案,可完美适配无username字段的用户模型,确保 JWT 认证流程正常工作,并支持 UUID 主键的灵活查询。