基于手机号的Django自定义用户模型与认证序列化器问题咨询
药房管理系统后端手机号认证方案代码检查与最佳实践
问题背景
作为Django新手,正在开发药房管理系统后端,采用DRF+dj-rest-auth+django-allauth实现手机号注册登录(替代默认邮箱/用户名),仅关注账号模块,以下是自定义用户模型、序列化器及配置代码,请求检查错误并提供最佳实践。
自定义用户模型代码
from django.contrib.auth.models import AbstractUser from django.db import models from django.utils.translation import gettext_lazy as _ from .managers import CustomUserManager from django.core.validators import RegexValidator phone_validator = RegexValidator(regex=r"^[1-9]\d{9}$", message="Invalid Number") class UserRole(models.TextChoices): RETAILER = "RETAILER", "retailer" STAFF = "STAFF", "staff" class CustomUser(AbstractUser): username = None mobile = models.CharField(max_length=10, validators=[phone_validator], unique=True) role = models.CharField( max_length=20, choices=UserRole.choices, default=UserRole.STAFF ) tenant = models.ForeignKey( "tenants.Tenant", on_delete=models.CASCADE, null=True, blank=True, related_name="users", ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) objects = CustomUserManager() USERNAME_FIELD = "mobile" REQUIRED_FIELDS = [] def __str__(self): return f"{self.mobile} ({self.get_full_name()})"
自定义序列化器代码
from dj_rest_auth.serializers import LoginSerializer from dj_rest_auth.registration.serializers import RegisterSerializer from rest_framework import serializers from allauth.account.adapter import get_adapter class MobileLoginSerializer(LoginSerializer): username = None mobile = serializers.CharField(required=True) def validate(self, attrs): attrs["username"] = attrs.get("mobile") return super().validate(attrs) class MobileRegisterSerializer(RegisterSerializer): username = None first_name = serializers.CharField() last_name = serializers.CharField() mobile = serializers.CharField() pharmacy_name = serializers.CharField(write_only=True) role = serializers.ChoiceField(choices=["RETAILER", "STAFF"]) def get_cleaned_data(self): data = super().get_cleaned_data() data.update( { "first_name": self.validated_data.get("first_name", ""), "last_name": self.validated_data.get("last_name", ""), "mobile": self.validated_data.get("mobile", ""), "pharmacy_name": self.validated_data.get("pharmacy_name", ""), "role": self.validated_data.get("role", ""), } ) return data def save(self, request): adapter = get_adapter() user = adapter.new_user(request) self.cleaned_data = self.get_cleaned_data() user = adapter.save_user(request, user, self, commit=False) user.first_name = self.cleaned_data.get("first_name") user.last_name = self.cleaned_data.get("last_name") user.mobile = self.cleaned_data.get("mobile") # user.pharmacy_name = self.cleaned_data.get("pharmacy_name") user.role = self.cleaned_data.get("role") user.save() self.custom_signup(request, user) return user
settings.py相关配置
... REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ["dj_rest_auth.jwt_auth.JWTCookieAuthentication"] } REST_AUTH = { "USE_JWT": True, "JWT_AUTH_COOKIE": "access", "JWT_AUTH_REFRESH_COOKIE": "refresh", "JWT_AUTH_HTTPONLY": True, "LOGIN_SERIALIZER": "accounts.serializers.MobileLoginSerializer", "REGISTER_SERIALIZER": "accounts.serializers.MobileRegisterSerializer", } AUTH_USER_MODEL = "accounts.CustomUser" ACCOUNT_USER_MODEL_USERNAME_FIELD = "mobile" ACCOUNT_EMAIL_REQUIRED = False ACCOUNT_USERNAME_REQUIRED = False ACCOUNT_AUTHENTICATION_METHOD = "username" ... SITE_ID = 1
代码错误检查与最佳实践建议
一、自定义用户模型优化点
- 手机号验证器增强
- 当前正则仅校验10位数字,建议调整为
r"^\+?86?1[3-9]\d{9}$",覆盖带+86前缀的国内手机号场景,同时将max_length改为15以容纳前缀。 - 已设置
unique=True,迁移时需确保数据库层面唯一性约束生效。
- 当前正则仅校验10位数字,建议调整为
- 冗余字段清理
AbstractUser自带date_joined和last_login字段,无需重复定义created_at;若需自定义更新逻辑,可保留updated_at并删除created_at,或直接使用现有字段。
- UserRole枚举优化
- TextChoices的第二个参数建议使用翻译字符串,比如
_("零售商")和_("员工"),支持多语言扩展。
- TextChoices的第二个参数建议使用翻译字符串,比如
- 租户外键处理
- 外键使用字符串引用
"tenants.Tenant"需确保tenants应用已在INSTALLED_APPS中注册,否则会导致迁移错误。
- 外键使用字符串引用
二、序列化器优化点
- MobileLoginSerializer简化
- 无需手动将
mobile映射到username,直接设置序列化器的username_field属性即可,更符合框架设计逻辑:class MobileLoginSerializer(LoginSerializer): username = None mobile = serializers.CharField(required=True) username_field = "mobile"
- 无需手动将
- MobileRegisterSerializer改进
- 角色选项复用:直接引用
UserRole.choices而非硬编码["RETAILER", "STAFF"],避免枚举变更时同步修改序列化器。 - 手机号验证:在序列化器中复用模型的
phone_validator,添加到mobile字段的validators中。 - pharmacy_name处理:若该字段属于
Tenant模型,应在custom_signup方法中创建或关联租户,而非尝试赋值给用户模型。 - save方法简化:
adapter.save_user已支持通过cleaned_data传递字段,无需重复设置first_name、last_name等,减少冗余代码。
- 角色选项复用:直接引用
三、settings配置修正
- allauth配置完善
- 添加
ACCOUNT_UNIQUE_EMAIL = False,避免allauth强制检查邮箱唯一性;设置ACCOUNT_EMAIL_VERIFICATION = "none",关闭邮箱验证流程。
- 添加
- JWT配置补充
- 添加生产环境安全配置:
JWT_AUTH_SECURE = True # HTTPS环境开启 JWT_AUTH_REFRESH_COOKIE_PATH = '/api/auth/refresh/' - 可自定义JWT过期时间:
SIMPLE_JWT = {'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15), 'REFRESH_TOKEN_LIFETIME': timedelta(days=1)}
- 添加生产环境安全配置:
四、其他最佳实践
- 自定义用户管理器实现
- 确保
CustomUserManager正确适配手机号作为用户名:from django.contrib.auth.base_user import BaseUserManager class CustomUserManager(BaseUserManager): def create_user(self, mobile, password=None, **extra_fields): if not mobile: raise ValueError("手机号必须填写") user = self.model(mobile=mobile, **extra_fields) user.set_password(password) user.save(using=self._db) return user def create_superuser(self, mobile, password=None, **extra_fields): extra_fields.setdefault('is_staff', True) extra_fields.setdefault('is_superuser', True) return self.create_user(mobile, password, **extra_fields)
- 确保
- 测试覆盖
- 编写单元测试覆盖手机号注册、登录、角色分配等核心场景,确保逻辑正确性。
- 生产环境安全
- 开启密码强度验证,添加
AUTH_PASSWORD_VALIDATORS配置;确保JWT的HTTPONLY属性开启,防止XSS攻击。
- 开启密码强度验证,添加
内容的提问来源于stack exchange,提问作者Rhythm Dutta
相关产品推荐
相关产品推荐

