eagleye

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)

  • 请求 URLhttp://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 主键的灵活查询。

 

posted on 2025-07-02 18:16  GoGrid  阅读(30)  评论(0)    收藏  举报

导航