Django JWT 认证适配自定义用户模型(无username字段)解决方案
Django JWT 认证适配自定义用户模型(无username字段)解决方案文档
一、概述
在企业级Django系统中,为提升用户体验和安全性,常需自定义用户模型(如使用手机号/邮箱作为主登录标识,移除默认的username字段)。然而,Django REST Framework(DRF)的JWT认证默认依赖username字段,导致认证失败(如“身份认证信息未提供”)。本文提供完整解决方案,适配基于identifier(手机号/邮箱)的用户模型,修复认证问题,并增强企业级安全特性。
二、问题分析
2.1 错误现象
调试视图返回:
{
"user": "匿名用户",
"auth_type": "无认证",
"headers": {
"HTTP_AUTHORIZATION": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
表明:
- 请求头已正确传递JWT令牌。
- 认证系统无法通过令牌识别用户(因用户模型无username字段)。
2.2 核心原因
DRF JWT认证默认通过username字段查找用户,而自定义用户模型使用identifier(手机号/邮箱)作为主标识,且移除了username字段,导致认证失败。
三、完整解决方案
3.1 自定义JWT认证类(关键修复)
修改认证逻辑,使用identifier字段或用户ID查找用户,适配无username的用户模型。
# apps/users/token_obtain.py
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.exceptions import AuthenticationFailed
from rest_framework_simplejwt.settings import api_settings
from django.contrib.auth import get_user_model
UserModel = get_user_model()
class CustomJWTAuthentication(JWTAuthentication):
"""
自定义JWT认证类,适配无username的用户模型(使用identifier或用户ID)
"""
def get_user(self, validated_token):
try:
# 从令牌中获取用户ID声明(默认对应用户模型的id字段)
user_id = validated_token[api_settings.USER_ID_CLAIM]
# 使用用户ID查询用户(适配UUID或自增ID)
user = UserModel._default_manager.get(**{
api_settings.USER_ID_FIELD: user_id # 对应用户模型的主键字段(如id)
})
# 检查用户是否激活
if not user.is_active:
raise AuthenticationFailed(
"用户已被禁用",
code="user_inactive"
)
return user
except UserModel.DoesNotExist:
raise AuthenticationFailed(
"用户不存在",
code="user_not_found"
)
except KeyError:
raise AuthenticationFailed(
"令牌缺少用户标识声明",
code="invalid_token"
)
3.2 更新DRF认证配置
指定使用自定义认证类,替换默认的JWT认证逻辑。
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'apps.users.token_obtain.CustomJWTAuthentication', # 自定义认证类
],
# 其他DRF配置(如权限类)保持不变...
}
3.3 调整令牌生成逻辑(确保用户ID正确)
生成JWT令牌时,显式包含用户ID声明(user_id),避免依赖username。
# apps/users/token_obtain.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.tokens import RefreshToken
class EnterpriseTokenObtainSerializer(TokenObtainPairSerializer):
def validate(self, attrs: dict) -> dict:
# 1. 验证用户凭证(使用identifier登录)
user = self.user
if not user:
raise AuthenticationFailed("无效的登录凭证")
# 2. 生成刷新令牌
refresh = RefreshToken.for_user(user)
# 3. 显式添加用户ID声明(确保与认证类匹配)
refresh['user_id'] = str(user.id) # 若用户ID为UUID,需转为字符串
# 4. 可选:添加自定义声明(如用户标识、设备信息)
refresh['identifier'] = user.identifier # 用户手机号/邮箱
# 5. 返回访问令牌和刷新令牌
return {
'refresh': str(refresh),
'access': str(refresh.access_token)
}
3.4 定义自定义用户模型(无username)
使用identifier作为主登录标识,移除username字段,并配置USERNAME_FIELD。
# apps/users/models.py
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.utils import timezone
import uuid
class SecureUser(AbstractBaseUser, PermissionsMixin):
# 主登录标识(手机号/邮箱)
identifier = models.CharField(
"登录标识",
max_length=255,
unique=True,
help_text="手机号或邮箱"
)
# 系统字段(无username)
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False,
help_text="用户唯一ID(UUID)"
)
is_active = models.BooleanField("激活状态", default=True)
date_joined = models.DateTimeField("注册时间", default=timezone.now)
last_login = models.DateTimeField("最后登录时间", blank=True, null=True)
# 权限字段
is_staff = models.BooleanField("管理权限", default=False)
is_superuser = models.BooleanField("超级用户", default=False)
# 关键配置:指定主标识字段为identifier
USERNAME_FIELD = 'identifier'
# 无额外必填字段(若需其他字段,添加到REQUIRED_FIELDS)
REQUIRED_FIELDS = []
# 自定义管理器(见3.5节)
objects = SecureUserManager()
class Meta:
verbose_name = "安全用户"
verbose_name_plural = "安全用户"
def __str__(self):
return self.identifier
def get_full_name(self):
return self.identifier
def get_short_name(self):
return self.identifier.split('@')[0] if '@' in self.identifier else self.identifier
3.5 创建自定义用户管理器
确保通过identifier创建用户,适配Django用户管理接口。
# apps/users/managers.py
from django.contrib.auth.base_user import BaseUserManager
from django.utils.translation import gettext_lazy as _
class SecureUserManager(BaseUserManager):
"""
自定义用户管理器,使用identifier作为主标识(无username)
"""
def create_user(self, identifier, password=None, **extra_fields):
if not identifier:
raise ValueError(_("必须提供登录标识(手机号/邮箱)"))
user = self.model(
identifier=identifier,
**extra_fields
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, identifier, password, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
return self.create_user(identifier, password, **extra_fields)
3.6 更新JWT中间件(可选)
若使用中间件拦截请求,需适配自定义认证类,确保请求用户正确绑定。
# middleware/jwt_middleware.py
from django.utils.deprecation import MiddlewareMixin
from django.contrib.auth import get_user_model
from apps.users.token_obtain import CustomJWTAuthentication
import logging
logger = logging.getLogger(__name__)
class JWTAuthenticationMiddleware(MiddlewareMixin):
def process_request(self, request):
# 跳过非API路径
if not request.path.startswith('/api/'):
return
try:
# 使用自定义认证类验证令牌
auth = CustomJWTAuthentication()
auth_result = auth.authenticate(request)
if auth_result is not None:
# 绑定用户和令牌到请求对象
request.user, request.auth = auth_result
except Exception as e:
logger.error(f"JWT认证中间件错误: {str(e)}", exc_info=True)
四、企业级安全增强
4.1 双因子认证(2FA)集成
对敏感账户强制要求两步验证,提升安全性。
# apps/users/token_obtain.py
from rest_framework_simplejwt.exceptions import AuthenticationFailed
class CustomJWTAuthentication(CustomJWTAuthentication):
def get_user(self, validated_token):
user = super().get_user(validated_token)
# 检查用户是否启用2FA且未完成验证
if user.require_2fa and not self.request.session.get('2fa_verified'):
raise AuthenticationFailed(
"需要完成两步验证",
code="2fa_required"
)
return user
4.2 令牌绑定设备指纹
防止令牌在非授权设备上使用,避免被盗用。
# apps/users/token_obtain.py
import hashlib
class EnterpriseTokenObtainSerializer(EnterpriseTokenObtainSerializer):
def _get_device_fingerprint(self, request) -> str:
"""生成设备指纹(基于User-Agent和IP)"""
user_agent = request.META.get('HTTP_USER_AGENT', '')
ip_address = request.META.get('REMOTE_ADDR', '')
return hashlib.sha256(f"{user_agent}|{ip_address}".encode()).hexdigest()
def validate(self, attrs):
# ...原有验证逻辑...
# 添加设备指纹到令牌声明
device_id = self._get_device_fingerprint(self.context['request'])
refresh['device_id'] = device_id
return super().validate(attrs)
class CustomJWTAuthentication(CustomJWTAuthentication):
def _get_current_device_fingerprint(self, request) -> str:
"""获取当前请求的设备指纹"""
user_agent = request.META.get('HTTP_USER_AGENT', '')
ip_address = request.META.get('REMOTE_ADDR', '')
return hashlib.sha256(f"{user_agent}|{ip_address}".encode()).hexdigest()
def get_user(self, validated_token):
user = super().get_user(validated_token)
# 验证设备指纹是否匹配
current_device = self._get_current_device_fingerprint(self.request)
token_device = validated_token.get('device_id')
if token_device != current_device:
raise AuthenticationFailed(
"检测到异常设备,需重新登录",
code="device_mismatch"
)
return user
4.3 安全审计日志
记录认证成功/失败事件,满足合规审计需求。
# apps/users/token_obtain.py
import logging
from django.utils import timezone
logger = logging.getLogger('jwt_audit')
class CustomJWTAuthentication(CustomJWTAuthentication):
def authenticate(self, request):
try:
user, token = super().authenticate(request)
# 记录成功日志
logger.info({
"event": "auth_success",
"user_id": user.id,
"identifier": user.identifier,
"timestamp": timezone.now().isoformat(),
"ip": request.META.get('REMOTE_ADDR'),
"device": request.META.get('HTTP_USER_AGENT')
})
return user, token
except AuthenticationFailed as e:
# 记录失败日志
logger.warning({
"event": "auth_failure",
"reason": e.detail,
"timestamp": timezone.now().isoformat(),
"ip": request.META.get('REMOTE_ADDR'),
"device": request.META.get('HTTP_USER_AGENT')
})
raise
五、调试与验证
5.1 生成新令牌(登录接口测试)
通过curl命令测试登录接口,验证令牌生成是否包含正确声明。
curl -X POST http://localhost:8000/api/auth/login/ \
-H "Content-Type: application/json" \
-d '{"identifier": "user@example.com", "password": "your_secure_password"}'
预期响应(包含user_id和identifier声明):
{
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
5.2 验证认证接口
使用生成的令牌访问受保护接口,验证用户是否被正确识别。
curl -X GET http://localhost:8000/api/auth-test/ \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
预期响应(用户已认证):
{
"user": "user@example.com",
"auth_type": "access",
"headers": {
"HTTP_AUTHORIZATION": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
六、用户模型最佳实践
6.1 字段设计原则
- 主标识字段:使用identifier存储手机号/邮箱,设置unique=True确保唯一性。
- 用户ID字段:推荐使用UUIDField(全局唯一,安全性高),替代自增ID。
- 安全字段:添加require_2fa(强制双因子)、password_changed(密码修改时间)等字段,增强安全控制。
- 权限字段:保留is_staff和is_superuser,适配Django admin后台。
6.2 完整用户模型示例
# apps/users/models.py
class SecureUser(AbstractBaseUser, PermissionsMixin):
identifier = models.CharField("登录标识", max_length=255, unique=True)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
is_active = models.BooleanField("激活状态", default=True)
date_joined = models.DateTimeField("注册时间", default=timezone.now)
last_login = models.DateTimeField("最后登录时间", blank=True, null=True)
is_staff = models.BooleanField("管理权限", default=False)
is_superuser = models.BooleanField("超级用户", default=False)
require_2fa = models.BooleanField("强制双因子认证", default=False)
password_changed = models.DateTimeField("密码修改时间", default=timezone.now)
USERNAME_FIELD = 'identifier'
objects = SecureUserManager()
# 其他方法(如__str__、get_full_name)...
七、配置检查清单
|
配置项 |
检查点 |
推荐值/示例 |
|
AUTH_USER_MODEL |
用户模型路径 |
'users.SecureUser' |
|
REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES |
认证类配置 |
['apps.users.token_obtain.CustomJWTAuthentication'] |
|
SIMPLE_JWT.USER_ID_FIELD |
用户ID字段名(与模型一致) |
'id'(UUID字段) |
|
SIMPLE_JWT.USER_ID_CLAIM |
令牌中的用户ID声明名 |
'user_id' |
|
SIMPLE_JWT.AUTH_TOKEN_CLASSES |
令牌类(可选) |
默认['rest_framework_simplejwt.tokens.AccessToken'] |
|
MIDDLEWARE |
JWT中间件(若使用) |
'middleware.jwt_middleware.JWTAuthenticationMiddleware' |
八、数据迁移指南
8.1 迁移现有用户数据(移除username字段)
若原有用户模型包含username字段,需迁移数据到identifier字段。
8.1.1 创建迁移脚本
python manage.py makemigrations users
8.1.2 编写数据迁移逻辑
# users/migrations/0002_migrate_identifier.py
from django.db import migrations
def migrate_identifier(apps, schema_editor):
SecureUser = apps.get_model('users', 'SecureUser')
for user in SecureUser.objects.all():
# 将原username或email/mobile迁移到identifier字段
user.identifier = user.username or user.email # 根据实际字段调整
user.save()
class Migration(migrations.Migration):
dependencies = [('users', '0001_initial')]
operations = [
migrations.RunPython(migrate_identifier), # 迁移数据
migrations.RemoveField(model_name='secureuser', name='username'), # 删除原username字段
]
8.1.3 执行迁移
python manage.py migrate users
九、总结
通过本文的解决方案,系统将:
1. 完全适配基于identifier
浙公网安备 33010602011771号