2026-05-08-REQ-USR-004.md 6.03 KB

req_id: REQ-USR-004 date: 2026-05-08

module: module_usr

Spec: REQ-USR-004 — 用户登录

目标

用户在登录页选择所属公司(brand)、输入用户名和密码,完成身份认证后获取 JWT Access Token 及 Refresh Token,用于后续所有接口鉴权。多租户隔离:每次登录的用户查询限定在所选 brand 的 sBrandsId 范围内。

输入 / 触发

POST /api/auth/login(公开接口,无需 Bearer Token)

{
  "brandNo": "string",   // brand.sNo,前端版本下拉选中项
  "username": "string",  // usr_user.sUsername
  "password": "string"   // 明文密码(HTTPS 传输),后端用 BCrypt 校验
}

辅助:版本下拉数据
前端需预先调用 GET /api/auth/brands 获取 brand 列表(sNo / sName),填充版本下拉框。默认选中 sName = "标准版" 对应的项(若无则选第一项)。此接口亦无需鉴权。

POST /api/auth/refresh(无需 Bearer Token,需 Refresh Token)

{ "refreshToken": "string" }

输出 / 结果

登录成功(HTTP 200)

{
  "code": 200,
  "message": "登录成功",
  "data": {
    "accessToken": "<jwt>",
    "refreshToken": "<jwt>",
    "expiresIn": 86400,
    "userInfo": {
      "userId": "string",     // usr_user.sId
      "username": "string",   // usr_user.sUsername
      "userType": "string",   // 普通用户 | 超级管理员
      "language": "string",   // 中文 | 英文 | 繁体
      "brandId": "string"     // brand.sId,写入 token claims 用于后续多租户隔离
    }
  }
}

失败(业务错误,HTTP 200 + code ≠ 0)

code message 场景
40100 用户名或密码错误 密码错误 / 用户不存在 / brand 不存在(统一文案,防枚举)
40101 账号已被禁用,请联系管理员 bIsDisabled = 1
40102 账号已被锁定,请 {N} 分钟后重试 tLockUntil > NOW()
40103 Refresh Token 已失效,请重新登录 refresh 接口 token 过期或篡改

业务规则

  1. 多租户查询:先 SELECT * FROM brand WHERE sNo = ?brandId = brand.sId;再 SELECT * FROM usr_user WHERE sUsername = ? AND sBrandsId = ?(brandId)。brand 不存在或用户不存在统一返回 40100(防枚举)。

  2. 密码校验BCryptPasswordEncoder.matches(plainPassword, usr_user.sPasswordHash)

  3. 禁用检查bIsDisabled = 1 → 返回 40101,不更新失败计数。

  4. 锁定检查tLockUntil IS NOT NULL AND tLockUntil > NOW() → 返回 40102,剩余分钟数 = CEIL((tLockUntil - NOW()) / 60)

  5. 失败计数:密码错误时 iLoginFailCount += 1;达到 5 次时设 tLockUntil = NOW() + 30 分钟,同时返回 40102。

  6. 登录成功

    • 重置 iLoginFailCount = 0tLockUntil = NULL
    • 更新 tLastLoginDate = NOW()
    • 签发 Access Token(24h)+ Refresh Token(7d),均为 JWT,签名密钥来自 JWT_SECRET(从 .env.local 注入,application.yml${JWT_SECRET}
  7. JWT Claims

    • Access Token:sub = usr_user.sIdusernameuserTypebrandId = brand.sIdexp = now + 24h
    • Refresh Token:sub = usr_user.sIdbrandIdtype = refreshexp = now + 7d
  8. Token 刷新(POST /api/auth/refresh):校验 Refresh Token 签名与 type=refresh;从 claims 读 sub(userId)和 brandId 重新查库确认用户仍有效;签发新 Access Token(24h);Refresh Token 不续期(滑动窗口留给后续迭代)。

  9. 版本下拉(GET /api/auth/brands):SELECT sNo, sName FROM brand ORDER BY sName;无需分页(品牌数量通常极少)。

边界与约束

  • 所有接口均走 HTTPS(Nginx 层保障,后端不强制)
  • 登录日志(IP + 时间戳)通过 Spring Security 的 AuthenticationSuccessHandler / AuthenticationFailureHandler 写 Logback,不写业务表
  • Refresh Token 无服务端存储(无状态 JWT);强制退出由后续迭代(引入 token 黑名单)实现;本期用 7 天过期控制
  • 密码明文绝不落库,sPasswordHash 仅存 BCrypt 60 字符哈希
  • Access Token 不含密码 hash,不含任何敏感字段
  • 防暴力破解:5 次失败锁 30 分钟(数据库层,不依赖内存,重启后有效)
  • GET /api/auth/brands 结果可加短缓存(30s),减少登录页加载压力

依赖的 schema 表 / 字段

usr_user(主要读写字段)

字段 操作 说明
sUsername READ 登录名匹配
sBrandsId READ 多租户范围限定
sPasswordHash READ BCrypt 校验
bIsDisabled READ 禁用检查
tLockUntil READ / WRITE 锁定截止时间
iLoginFailCount READ / WRITE 连续失败次数
tLastLoginDate WRITE 成功后更新
sId READ 写入 JWT sub
sUserType READ 写入 JWT claims
sLanguage READ 返回 userInfo

brand

字段 操作 说明
sNo READ 版本下拉匹配键
sId READ 写入 JWT brandId claim
sName READ 版本下拉显示名

依赖的接口

  • 自身即认证入口,无上游接口依赖
  • 对外暴露:POST /api/auth/loginPOST /api/auth/refreshGET /api/auth/brands(均无鉴权)

验收标准

  1. 正确 brandNo + username + password → HTTP 200,返回有效 accessToken,POST /api/usr/users 携带该 token 鉴权通过
  2. 错误密码 → code=40100,且不暴露是用户名还是密码错了
  3. 连续 5 次错误密码 → 第 5 次返回 code=40102(含剩余分钟数),后续在锁定期内再请求仍返回 40102
  4. 锁定 30 分钟后自动解锁,正确凭据可再次登录
  5. bIsDisabled = 1 的账号登录 → code=40101
  6. brand.sNo 不存在 → code=40100(与密码错误同一文案)
  7. 用同一账号、不同 brandId 登录 → 互不干扰(多租户隔离)
  8. 有效 refreshToken → POST /api/auth/refresh 返回新 accessToken
  9. 过期 / 伪造 refreshToken → code=40103
  10. GET /api/auth/brands 返回所有 brand 列表(sNo + sName)