2026-04-30-REQ-USR-004.md
6.69 KB
req_id: REQ-USR-004 date: 2026-04-30
module: module_usr
Spec: REQ-USR-004 — 用户登录
目标
实现 POST /api/usr/auth/login 登录接口(公开),含密码 BCrypt 校验、失败计数 + 临时锁定、JWT 双 token 签发、tLastLoginDate 更新。同时收尾整个项目的 stub permitAll,把 SecurityConfig 收紧为 "登录接口外全部 authenticated()",并完成 service 层 stub fallback 的移除。
输入 / 触发
HTTP 接口(docs/05 § REQ-USR-004)
- Method / Path:
POST /api/usr/auth/login - Auth: 无(路径独立 permitAll)
请求 DTO LoginDTO
| 字段 | 类型 | 校验 |
|---|---|---|
sUserName |
String |
@NotBlank |
password |
String |
@NotBlank |
version |
String |
@NotBlank(仅记录,不参与校验:合法值 标准版 / -) |
输出 / 结果
成功响应
{
"code": 0, "msg": "ok",
"data": {
"accessToken": "<jwt>", "refreshToken": "<jwt>",
"expiresIn": 28800,
"user": { "iIncrement": 1, "sUserNo": "001", "sUserName": "u1", "sUserType": "普通用户", "sLanguage": "zh" }
}
}
VO LoginVO:accessToken / refreshToken / expiresIn / user(UserBriefVO)
持久化效果
成功登录:UPDATE tUser SET tLastLoginDate = NOW() WHERE iIncrement = ?。失败:仅更新内存中失败计数,不入 DB。
业务规则
-
必填校验:
sUserName/password空 →BizException(40001, "<字段>: 不能为空")(实际由 Bean Validation @NotBlank 触发,GlobalExceptionHandler 转 40001)。 -
用户查询:
SELECT * FROM tUser WHERE sUserName = ?;不存在 →BizException(40101, "用户名或密码错误")(不区分用户名/密码错,防泄漏)。 -
账号禁用:
user.bDeleted=1→BizException(40102, "账号已禁用")。 -
锁定检查:从
LoginAttemptStore(内存ConcurrentHashMap)查该 sUserName 的失败记录;若在锁定期 →BizException(42301, "账号临时锁定,剩余 <N> 秒"),N = (lockExpireAt - now).seconds。 -
密码校验:
bCryptPasswordEncoder.matches(password, user.sPasswordHash);- 失败:失败计数 +1;若计数 ≥
MAX_ATTEMPTS=5→ 设lockExpireAt = now + LOCK_DURATION=15分钟→ 抛BizException(42301, ...);否则抛BizException(40101, "用户名或密码错误") - 成功:清空该 sUserName 的失败计数;UPDATE tLastLoginDate;签发 access + refresh token;返回 LoginVO
- 失败:失败计数 +1;若计数 ≥
-
Token 签发:
accessToken = jwtUtil.sign(sUserNo)(默认 8 小时);refreshToken = jwtUtil.signRefresh(sUserNo)(30 天);expiresIn = 28800(access TTL 秒数常量)。 -
LoginAttemptStore内存实现:Map<String, AttemptRecord{int count, Instant firstFailAt, Instant lockExpireAt}>;线程安全;锁定到期自动放行(下次登录尝试时若now > lockExpireAt则视为未锁,重置计数)。
边界与约束
-
必填项空 →
40001 -
用户名不存在 / 密码错(未达阈值) →
40101 -
账号 bDeleted →
40102 -
锁定中 →
42301+ 剩余秒数 -
sPasswordHash不返回(VO 字段集排除) - 不入 DB 的失败计数:本期内存实现;docs/04 § 零 技术栈列了 Redis,未来引入时替换
- 不引入 Redis:本 REQ 暂不增加运行时依赖
实现范围与边界抉择(含 Stub 闭环)
Phase A — 登录接口本体
- 新建
LoginDTO/LoginVO/UserBriefVO/LoginAttemptStore(@Component) -
JwtUtil加signRefresh(userNo)方法 -
UserMapper加selectByUserName+updateLastLoginDate -
UserService加login(LoginDTO)+ 实现 - 新建
AuthController处理/api/usr/auth/login
Phase B — Stub 闭环(关键里程碑)
收紧 SecurityConfig + 移除 service 层 stub fallback + 修复现有 IT 测试期望:
-
SecurityConfig:- 移除
/api/mod/**+/api/usr/**permitAll - 改为
/api/usr/auth/loginpermitAll +anyRequest().authenticated() - 注册
AuthenticationEntryPoint把未认证 → JSONResult(20001, "未认证")(让 docs/05 § 全局约定 § 鉴权"缺失或失效返回 2xxxx" 落地)
- 移除
-
ModuleServiceImpl#create移除stub.getStubUserNo()fallback:sCreatedBy = SecurityContextHelper.currentUserNo()必须非 null(authenticated() 保证;若 null 抛兜底异常) -
UserServiceImpl#create/update同样移除 fallback -
现有 stub IT 测试更新(约 8 处):
-
*WithoutJwt_permitAllStub_*系列(MOD-001/002/003 + USR-001/002):原期望 200 + sCreatedBy=STUB_ADMIN,改期望code=20001,DB 无新行 -
getWithoutJwt_permitAllStub_returns200(MOD-004 + USR-003):改期望code=20001 - 移除
// REQ-MOD-001 stub: see USR-004 follow-up锚点(grep -r 全局批量删)
-
Phase B 是 USR-004 完成的前提,模块完成报告 § ⑩ 列出的"鉴权 stub 收尾"将一次性闭环。
依赖的 schema 表 / 字段
读:tUser.*(按 sUserName 查全行)
写:tUser.tLastLoginDate
依赖的接口
无。
验收标准
单元测试(UserServiceImplTest / LoginAttemptStoreTest 新建)
-
loginWithValidCredentials_returnsTokens_andUpdatesLastLoginDate -
loginWithUserNotFound_throws40101 -
loginWithDeletedUser_throws40102 -
loginWithWrongPassword_incrementsCounter_throws40101 -
loginAfterMaxAttemptsReached_throws42301_withRemainingSeconds -
loginWhileLocked_throws42301 -
loginAfterLockExpired_resetsCounter -
loginAttemptStore_isolation— 不同 sUserName 互不影响 -
loginSuccess_clearsFailureCounter
Mapper IT(追加到 UserMapperIT)
-
selectByUserName_returnsRowOrNull -
updateLastLoginDate_setsValue
集成测试(AuthControllerIT 新建)
-
loginWithValidCredentials_returns200_withTokens -
loginWithEmptyBody_returns40001 -
loginWithUserNotFound_returns40101 -
loginWithWrongPassword_returns40101 -
loginAfter5WrongPasswords_returns42301
Stub 闭环 IT 更新(Phase B,~8 处)
修改原"无 JWT 期望 200" 用例,新期望 code=20001,DB 无新增行。具体清单:
- ModuleControllerIT:postWithoutJwt / putWithoutJwt / deleteWithoutJwt / getWithoutJwt(4 处)
- UserControllerIT:postWithoutJwt / putWithoutJwt / getWithoutJwt(3 处)
工程验收
-
mvn -B test全绿(约 145+ 用例) - SecurityConfig 路径只有
/api/usr/auth/loginpermitAll;其他全 authenticated - 全局 grep
// REQ-MOD-001 stub: see USR-004 follow-up返回 0 结果 - grep
stub.getStubUserNo()返回 0 结果(service 层) -
AuthenticationEntryPoint配置:未认证 →code=20001