用户模型设计:从字段到状态,一个都不能少
好,咱们进入实战的第二块核心内容——用户模型设计。说白了,就是数据库里那张用户表该怎么建。我见过太多项目,前期图省事,用户表就三五个字段,结果后面权限一复杂,改得想哭。今天咱们就把这事一次性说透。
用户表字段设计:别漏了,也别多
先说说字段。我个人习惯,用户表至少要有这几类字段:身份标识、认证信息、状态控制、时间戳。缺了哪个,后面都会补坑。
下面这张表是我在多个项目中沉淀下来的标准结构,你可以直接拿去用:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT UNSIGNED | 主键,自增。别用UUID做主键,性能扛不住 |
| username | VARCHAR(50) | 唯一索引。我建议限制长度,防止注入 |
| VARCHAR(100) | 唯一索引。用于找回密码和通知 | |
| password_hash | VARCHAR(255) | 存bcrypt加密后的哈希值,不是明文! |
| status | TINYINT | 0=禁用, 1=正常, 2=未激活, 3=锁定 |
| role_id | INT UNSIGNED | 外键,关联角色表。后面章节会细讲 |
| last_login_at | DATETIME | 最后登录时间,排查问题很有用 |
| created_at | DATETIME | 创建时间,默认当前时间 |
| updated_at | DATETIME | 更新时间,每次更新自动修改 |
密码加密策略:为什么必须用 bcrypt?
说到密码存储,我得先讲个真实案例。我曾经接手过一个老项目,密码字段存的是 MD5 哈希。你猜怎么着?用户表被拖库后,攻击者用彩虹表半小时就还原了 80% 的密码。嗯,那场面,真是惨不忍睹。
所以,千万别用 MD5、SHA-1、SHA-256 这些快速哈希算法存密码。它们设计出来是为了快,而密码存储恰恰需要慢。bcrypt 就是专门干这个的——它内置了 salt(盐值),还能调整计算成本,让暴力破解变得极其昂贵。
来看一段 Python 代码,用 bcrypt 加密和验证密码:
import bcrypt
# 注册时:加密密码
def hash_password(plain_password: str) -> str:
# 自动生成盐值,cost factor 默认12,我建议至少12
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(plain_password.encode('utf-8'), salt)
return hashed.decode('utf-8')
# 登录时:验证密码
def verify_password(plain_password: str, hashed_password: str) -> bool:
return bcrypt.checkpw(
plain_password.encode('utf-8'),
hashed_password.encode('utf-8')
)
# 使用示例
if __name__ == '__main__':
pwd = "MySecureP@ss123"
hashed = hash_password(pwd)
print(f"哈希值: {hashed}")
print(f"验证结果: {verify_password(pwd, hashed)}")
你可能会问:为什么不选 argon2?它比 bcrypt 更安全啊。没错,argon2 是 2015 年密码哈希竞赛的冠军。但说实话,bcrypt 的生态更成熟,几乎所有语言都有稳定库,而且对于大多数业务系统来说,bcrypt 的安全性已经足够了。我个人习惯,新项目用 argon2,老项目升级到 bcrypt 就行。
用户状态管理:别只用一个字段
用户状态这事,看着简单,坑却不少。我见过有人用一个字段存「正常/禁用/删除」,结果后面要加个「未激活」状态,改代码改到崩溃。
我的做法是:状态字段用位掩码(bitmask)设计。每个 bit 代表一种状态,互不干扰。比如:
- bit 0 (值1):是否激活
- bit 1 (值2):是否禁用
- bit 2 (值4):是否锁定(多次登录失败)
- bit 3 (值8):是否删除(软删除)
这样,一个字段就能组合多种状态。比如 status=3 表示「已激活 + 已禁用」,status=5 表示「已激活 + 已锁定」。判断起来也很方便:
# 状态常量定义
STATUS_ACTIVE = 1 # 0001
STATUS_DISABLED = 2 # 0010
STATUS_LOCKED = 4 # 0100
STATUS_DELETED = 8 # 1000
def is_active(status: int) -> bool:
return bool(status & STATUS_ACTIVE)
def is_disabled(status: int) -> bool:
return bool(status & STATUS_DISABLED)
def is_locked(status: int) -> bool:
return bool(status & STATUS_LOCKED)
# 登录时检查
def can_login(status: int) -> bool:
# 必须激活,且未被禁用或锁定
return (status & STATUS_ACTIVE) and not (status & (STATUS_DISABLED | STATUS_LOCKED))
另外,状态变更一定要记录日志。谁改的、什么时候改的、从什么状态改到什么状态、原因是什么。这些信息在排查问题时能救命。我习惯单独建一张 user_status_log 表,每次状态变化都写一条记录。
总结一下
用户模型设计,说白了就是三件事:字段要全但不冗余、密码要用 bcrypt 或 argon2、状态要灵活可扩展。别小看这些基础设计,它们决定了你的系统能走多远。下一章咱们聊角色和权限的关联,那才是真正的重头戏。
对了,如果你现在去翻翻自己项目的用户表,发现密码字段还是 MD5,嗯……赶紧改吧,趁还没出事。