--- req_id: REQ-USR-004 date: 2026-04-30 spec_ref: docs/superpowers/specs/2026-04-30-REQ-USR-004.md --- # REQ-USR-004 用户登录 Implementation Plan > **Execution:** Parent skill `feature-tdd` executes this plan task-by-task. **Goal:** Phase A 实现 POST /api/usr/auth/login + 失败计数锁定 + JWT 双 token;Phase B 闭环 stub permitAll → authenticated。 --- ## Schema 改动 无。 ## 文件变更清单 ### 新增 - backend/src/main/java/com/xly/erp/module/usr/dto/LoginDTO.java - backend/src/main/java/com/xly/erp/module/usr/vo/LoginVO.java - backend/src/main/java/com/xly/erp/module/usr/vo/UserBriefVO.java - backend/src/main/java/com/xly/erp/module/usr/security/LoginAttemptStore.java - backend/src/main/java/com/xly/erp/module/usr/controller/AuthController.java - backend/src/main/java/com/xly/erp/common/security/JwtAuthenticationEntryPoint.java - backend/src/test/java/com/xly/erp/module/usr/security/LoginAttemptStoreTest.java - backend/src/test/java/com/xly/erp/module/usr/controller/AuthControllerIT.java ### 修改 - common/security/JwtUtil.java — 加 `signRefresh(userNo)` + 区分 access/refresh TTL - common/security/SecurityConfig.java — 重写路径规则 + 注册 entryPoint - module/usr/mapper/UserMapper.java — 加 `selectByUserName` + `updateLastLoginDate` - module/usr/service/UserService.java + impl — 加 `login(LoginDTO)` - module/mod/service/impl/ModuleServiceImpl.java — 移除 stub fallback - module/usr/service/impl/UserServiceImpl.java — 移除 stub fallback(create + update) - ModuleControllerIT (4 处) + UserControllerIT (3 处) — 改 stub 期望 --- ## 任务步骤 ### Phase A: 登录接口本体 #### Task 1: JwtUtil 加 signRefresh - 加 `Duration REFRESH_TTL = Duration.ofDays(30)` + `sign(userNo, ttl)` 通用方法 + `signRefresh(userNo)` 包装;现有 `sign(userNo)` 走 `Duration.ofHours(8)` - 单测:`signRefresh_signsWithLongerTtl` - Commit: `feat(usr): jwtutil signRefresh REQ-USR-004` #### Task 2: LoginAttemptStore + 单测 - 新建 `@Component LoginAttemptStore`:`recordFailure(userNo) → newCount` / `isLocked(userNo) → Optional` / `clearFailures(userNo)`;常量 `MAX_ATTEMPTS=5` / `LOCK_DURATION=Duration.ofMinutes(15)` - 单测 5 用例覆盖隔离 / 累加 / 锁定 / 自动解锁 / 清空 - Commit: `feat(usr): login attempt store + lock logic REQ-USR-004` #### Task 3: UserMapper 加 selectByUserName + updateLastLoginDate - `@Select("SELECT ... FROM tUser WHERE sUserName = #{name}")` `User selectByUserName(String name)` - `@Update("UPDATE tUser SET tLastLoginDate = #{ts} WHERE iIncrement = #{id}")` `int updateLastLoginDate(Integer id, LocalDateTime ts)` - IT 2 用例 - Commit: `feat(usr): mapper selectByUserName + updateLastLoginDate REQ-USR-004` #### Task 4: LoginDTO + LoginVO + UserBriefVO + UserService.login + 单测 - 6 个 service 单测(spec 列表) - Commit: `feat(usr): login service + dto/vo REQ-USR-004` #### Task 5: AuthController + IT (5 用例) - 新建 `AuthController @RequestMapping("/api/usr/auth")`,`@PostMapping("/login")` - IT 5 用例覆盖 spec - Commit: `feat(usr): auth controller + login it REQ-USR-004` ### Phase B: Stub 闭环 #### Task 6: SecurityConfig 收紧 + AuthenticationEntryPoint - `JwtAuthenticationEntryPoint`:未认证写 JSON `Result.fail(20001, "未认证")` + status 200 - SecurityConfig: - 移除 `/api/mod/**` + `/api/usr/**` permitAll - 加 `/api/usr/auth/login` permitAll - `anyRequest().authenticated()` - `exceptionHandling(eh -> eh.authenticationEntryPoint(authEntryPoint))` - 移除 `// REQ-MOD-001 stub: see USR-004 follow-up` 注释 - Commit: `refactor(usr): tighten security to authenticated REQ-USR-004` #### Task 7: ModuleServiceImpl + UserServiceImpl 移除 stub fallback - `ModuleServiceImpl#create`:`sCreatedBy = SecurityContextHelper.currentUserNo()`,去掉 `?: stub.getStubUserNo()`;同 `UserServiceImpl#create` + `update` - 不破坏 `StubSecurityProperties` bean 本身(仍可保留以防其他用途;本 REQ 仅停止 fallback 引用) - 单测调整:MOD-001 `createWithValidDto_persistsWithStandardCols` 期望 sCreatedBy 不再是 STUB_ADMIN(而要在测试里手动 SecurityContextHolder.set principal);其他类似 - Commit: `refactor(usr): remove stub fallback in services REQ-USR-004` #### Task 8: 修改现有 stub IT 期望 - ModuleControllerIT 4 条:`postWithoutJwt_permitAllStub_returns200_andCreatedBySTUBADMIN` / `putWithoutJwt_*` / `deleteWithoutJwt_*` / `getWithoutJwt_*` → 改期望 `code=20001`,DB 无新增行 - UserControllerIT 3 条:`postWithoutJwt_*` / `putWithoutJwt_*` / `getWithoutJwt_*` → 改期望 `code=20001` - 全量回归绿 - Commit: `test(usr): update stub regression to authenticated REQ-USR-004` ## 提交计划 8 commits:6 feat + 2 refactor + 1 test(或合并为更少 commit 视进度)。