Skip to content

认证模块

src/modules/auth/ 实现双令牌 JWT 认证,提供注册、登录、令牌刷新功能。

1. 模块结构

mermaid
flowchart LR
    subgraph auth["modules/auth"]
        AC["AuthController\nPOST /auth/*"] --> AS["AuthService\n注册/登录/轮换"]
        AS --> TS["TokenService\nES256 签发/验证"]
        AG["AuthGuard(全局)"] --> TS
    end

    AS --> DB["DatabaseService\nUser CRUD"]
    TS --> CFG["constants/auth.constant.ts\nJWT 密钥 + Cookie 配置"]

2. 双令牌策略

属性Access TokenRefresh Token
过期15 分钟(JWT_ACCESS_EXPIRES_IN7 天(JWT_REFRESH_EXPIRES_IN
算法ES256(EC 非对称)ES256(EC 非对称)
传输Authorization: Bearer <token> 请求头HttpOnly Cookie(名称:refresh_token
Claimssub, jti(ULID), tokenType:'access', user{...}sub, jti(ULID), tokenType:'refresh'

选择 ES256 的理由:公钥可安全分发给第三方服务进行本地验证,无需共享签名密钥,支持密钥分离架构。ES256(ECDSA P-256)密钥更短、性能优于 RS256(RSA),HS256 共享密钥无法安全分发。

3. JWT Claims 结构

Access Token payload:

typescript
{
  sub: string,            // 用户 ID(ULID)
  iat: number,            // 签发时间戳
  exp: number,            // 过期时间戳
  jti: string,            // 令牌唯一 ID(ULID,防重放)
  tokenType: 'access',
  user: {
    id: string,
    username: string,
    email: string,
    createdAt: string,
    updatedAt: string
    // 不包含 passwordHash
  }
}

Refresh Token payload(轻量,不含用户详情):

typescript
{
  sub: string,
  iat: number,
  exp: number,
  jti: string,
  tokenType: 'refresh'
}

Refresh Token 通过 HttpOnly Cookie 传输,防止 XSS 窃取。

属性默认值环境变量
Cookie 名refresh_token
HttpOnlytrue
Path/authJWT_REFRESH_COOKIE_PATH
SameSitelaxJWT_REFRESH_COOKIE_SAME_SITE
Securefalse(开发)JWT_REFRESH_COOKIE_SECURE
最大寿命604800000ms(7 天)JWT_REFRESH_COOKIE_MAX_AGE_MS

生产环境必须设置 JWT_REFRESH_COOKIE_SECURE=true

5. 令牌生命周期

mermaid
stateDiagram-v2
    [*] --> Issued : 登录 / 注册 / 刷新成功
    Issued --> Valid : 签发后在有效期内
    Valid --> Expired : 超过过期时间
    Valid --> Used : 用于请求认证(access)
    Used --> Valid : 验证通过
    Used --> Invalid : 验证失败(签名错误 / 篡改)
    Expired --> [*] : 需重新登录或刷新
    Invalid --> [*] : 抛出 AUTH_FAILED 401
    Valid --> Rotated : 调用 POST /auth/refresh-token
    Rotated --> [*] : 旧令牌失效,新令牌对签发

6. API 端点

方法路径权限请求体响应
POST/auth/register@Public{ username, email, password }{ accessToken, user } + 设置 Cookie
POST/auth/login@Public{ account, password }{ accessToken, user } + 设置 Cookie
POST/auth/refresh-token@PublicCookie: refresh_token{ accessToken } + 更新 Cookie
GET/auth/clear-cookie@Public清除 refresh_token Cookie

account 字段同时接受 usernameemail,后端自动判断。

7. 令牌刷新流程

mermaid
flowchart TD
    A["POST /auth/refresh-token"] --> B["读取 Cookie: refresh_token"]
    B --> C{Token 存在?}
    C -- 否 --> ERR["抛出 AUTH_FAILED 401"]
    C -- 是 --> D["TokenService.verifyToken(token, 'refresh')"]
    D --> E{验证通过?}
    E -- 否 --> ERR
    E -- 是 --> F["从 claims.sub 获取 userId"]
    F --> G["DatabaseService.user.findUnique(userId)"]
    G --> H{用户存在?}
    H -- 否 --> ERR
    H -- 是 --> I["issueTokenPair(userId, username)"]
    I --> J["生成新 Access Token(15min)"]
    I --> K["生成新 Refresh Token(7d)\n写入 HttpOnly Cookie"]
    J & K --> L["返回 200 { accessToken }"]

8. 密码处理

  • 加密库:bcryptjs
  • 加密成本:10 rounds(bcrypt.hash(password, 10)
  • 验证:bcrypt.compare(inputPassword, storedPasswordHash)
  • 存储字段:passwordHash(不在任何响应体或 JWT Claims 中暴露)

9. 密钥配置

生产环境必须通过环境变量提供 EC 密钥对(PEM 格式),禁止使用内置的 Dev 默认密钥:

环境变量说明
JWT_ACCESS_PRIVATE_KEYAccess Token 签名私钥(EC PEM)
JWT_ACCESS_PUBLIC_KEYAccess Token 验证公钥(EC PEM)
JWT_REFRESH_PRIVATE_KEYRefresh Token 签名私钥(EC PEM)
JWT_REFRESH_PUBLIC_KEYRefresh Token 验证公钥(EC PEM)
JWT_ACCESS_EXPIRES_INAccess Token 过期(默认 '15m'
JWT_REFRESH_EXPIRES_INRefresh Token 过期(默认 '7d'