eagleye

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

 

posted on 2025-07-01 11:48  GoGrid  阅读(64)  评论(0)    收藏  举报

导航