认证系统案例设计
本案例实现一个简化的企业任务系统。用户可以注册、登录、查看个人信息、刷新凭证、退出当前设备和退出全部设备。
1. 接口设计
| 方法 | 地址 | 是否需要认证 | 作用 |
|---|---|---|---|
POST |
/register |
否 | 注册用户 |
POST |
/login |
否 | 登录并获得两个 Token |
GET |
/profile |
Access Token | 查看当前用户 |
POST |
/token/refresh |
Refresh Token | 轮换并获得新 Token |
POST |
/logout |
Refresh Token | 退出对应设备 |
POST |
/logout-all |
Access Token | 退出该用户全部设备 |
2. 数据模型
用户
type UserRole = "user" | "admin";
interface User {
id: string;
username: string;
passwordHash: string;
role: UserRole;
createdAt: Date;
updatedAt: Date;
}
登录会话
interface AuthSession {
id: string;
userId: string;
refreshTokenHash: string;
expiresAt: Date;
revokedAt: Date | null;
createdAt: Date;
updatedAt: Date;
}
一个用户可以有多个会话,每次登录创建一个会话。Refresh Token 代表某次具体登录,而不只是某个用户。
3. MySQL 中的建议结构
教程代码用数组模拟,迁移到 MySQL 时可采用:
CREATE TABLE users (
id CHAR(36) PRIMARY KEY,
username VARCHAR(64) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'user',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
CREATE TABLE auth_sessions (
id CHAR(36) PRIMARY KEY,
user_id CHAR(36) NOT NULL,
refresh_token_hash CHAR(64) NOT NULL UNIQUE,
expires_at DATETIME NOT NULL,
revoked_at DATETIME NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
CONSTRAINT fk_auth_sessions_user
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX idx_auth_sessions_user_id (user_id),
INDEX idx_auth_sessions_expires_at (expires_at)
);
password_hash 使用 VARCHAR(255) 是为了容纳 PHC 字符串并给未来算法迁移留空间。refresh_token_hash 是 SHA-256 十六进制结果,固定为 64 个字符。
4. Access Token Payload
interface AccessTokenPayload {
sub: string;
sessionId: string;
tokenType: "access";
role: UserRole;
}
sub:用户 ID。sessionId:对应登录会话,便于审计或实现更严格的撤销检查。tokenType:防止把其他 Token 错当 Access Token。role:演示授权;角色变更后旧 Token 可能短暂过时。
对必须实时生效的高风险权限,仍应查询数据库中的最新角色或权限。
5. Token 配置
const accessTokenConfig = {
algorithm: "HS256",
issuer: "nloop-api",
audience: "nloop-client",
expiresIn: "15m",
} as const;
const refreshTokenLifetimeMs = 7 * 24 * 60 * 60 * 1000;
Access Token 的签发和验证必须使用一致的 issuer、audience 和算法约束。
6. 为什么 Refresh Token 选择随机字符串
Refresh Token 不需要客户端读取身份信息。随机字符串配合会话表具有以下优点:
- 数据本身没有可读取的 Payload。
- 服务端自然掌握会话状态和撤销能力。
- 轮换逻辑直观。
- 不会误以为“验证 JWT 签名就足够”,从而跳过会话撤销检查。
使用 JWT 作为 Refresh Token 也可以,但仍推荐检查服务端会话;仅验证签名不能判断是否已退出。
7. 内存仓库
const users: User[] = [];
const authSessions: AuthSession[] = [];
它仅用于教学,有明显限制:
- 服务器重启后数据消失。
- 多进程或多实例之间不共享。
- 并发写入没有数据库事务保护。
- 无法依靠唯一约束处理竞态条件。
生产环境需要数据库事务和唯一约束。例如注册时“先查询用户名、再插入”仍可能出现并发重复,最终必须由数据库唯一约束兜底。
8. 模块职责建议
真实项目可以在理解流程后拆分:
routes/auth.ts HTTP 路由
services/auth.ts 注册、登录、刷新业务流程
services/token.ts Token 签发和验证
services/password.ts 密码哈希与验证
repositories/*.ts 数据访问
middleware/auth.ts Access Token 认证
本项目以学习为目标,不要求立刻创建这些目录。先理解数据如何流动,再根据代码规模抽取模块。
9. 状态码约定
| 状态码 | 场景 |
|---|---|
201 |
注册成功 |
200 |
登录、刷新、查询、退出成功 |
400 |
请求参数格式错误 |
401 |
账号密码错误、Token 缺失/无效/过期 |
403 |
已认证但权限不足 |
409 |
用户名已存在 |
500 |
未预期的内部错误 |
登录时账号不存在和密码错误统一返回 401 与“账号或密码错误”,避免泄露有效账号信息。