认证系统案例设计

本案例实现一个简化的企业任务系统。用户可以注册、登录、查看个人信息、刷新凭证、退出当前设备和退出全部设备。

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;
}

对必须实时生效的高风险权限,仍应查询数据库中的最新角色或权限。

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 的签发和验证必须使用一致的 issueraudience 和算法约束。

6. 为什么 Refresh Token 选择随机字符串

Refresh Token 不需要客户端读取身份信息。随机字符串配合会话表具有以下优点:

使用 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 与“账号或密码错误”,避免泄露有效账号信息。