For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 将商家 JWT 签名、有效期和 Redis 会话校验集中到 @Auth 认证切面,删除业务 Controller 对 MerchantTokenSessionService.requireUserId 的二次认证。
Architecture: @Auth(session = true) 显式声明需要 Redis 会话的商家接口;AuthAspect 一次解析已验证 JWT、校验 JTI 并把可信用户 ID/JTI 写入当前请求属性。Controller 通过只读的 AuthContext 获取认证结果并继续执行门店、订单和商品归属校验;MerchantTokenSessionService 只管理退出、撤销和在线状态。
Tech Stack: Java 21、Spring Boot、Spring AOP、Auth0 Java JWT、Redis、JUnit 5、Mockito、Maven。
ruoyi-admin -> ruoyi-system,ruoyi-system 不导入 com.ruoyi.app.*。session = true 的 @Auth 接口保持现有 JWT 校验行为。@RequestHeader String token,不使用 HttpServletRequest 读取 Controller 入参。merchant.session.invalid,不新增硬编码单语言业务错误。C:\Users\qmj\.jdks\graalvm-jdk-21.0.7,只在当前命令设置 JAVA_HOME 和 PATH。.claude/homunculus/observations.jsonl、.tmp/ 或其他已有脏文件。Files:
ruoyi-system/src/main/java/com/ruoyi/system/utils/Auth.javaruoyi-system/src/main/java/com/ruoyi/system/utils/AuthAspect.javaruoyi-system/src/main/java/com/ruoyi/system/utils/JwtUtil.javaruoyi-system/src/main/java/com/ruoyi/system/utils/AuthContext.javaruoyi-system/src/test/java/com/ruoyi/system/utils/AuthAspectTest.javaInterfaces:
Auth.session(): boolean,默认 false。AuthContext.requireUserId(): Long 与 AuthContext.requireJti(): String。Produces: 请求属性 AuthContext.USER_ID_ATTRIBUTE、AuthContext.JTI_ATTRIBUTE。
[ ] Step 1: 编写失败测试
在 AuthAspectTest 定义分别标注 @Auth 和 @Auth(session = true) 的样例方法,并验证三项行为:默认认证不访问 Redis;启用会话认证且 Redis 含 JTI 时调用 joinPoint.proceed();Redis 缺少 JTI 时抛出 ServiceException 且不进入目标方法。
static class SecuredMethods {
@Auth
public void jwtOnly() {}
@Auth(session = true)
public void merchantSession() {}
}
assertSame(expected, aspect.around(joinPoint));
verify(redisCache, never()).hasKey(anyString());
when(redisCache.hasKey(jti)).thenReturn(false);
assertThrows(ServiceException.class, () -> aspect.around(joinPoint));
verify(joinPoint, never()).proceed();
Run:
$env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7'
$env:PATH="$env:JAVA_HOME\bin;$env:PATH"
mvn -pl ruoyi-system -Dtest=AuthAspectTest test
Expected: FAIL,因为 Auth.session、Redis 会话校验和可信身份请求属性尚不存在。
为 Auth 增加默认关闭的选项:
boolean session() default false;
修正 JwtUtil.verifyToken,将已验证的 id 等 Claim 写入返回结果,避免认证切面再次验签:
result.put(key, value);
AuthAspect 使用 verifyToken 完成一次认证;session = true 时检查 JTI Redis Key,并在放行前写入可信请求属性:
Map<String, Object> claims = JwtUtil.verifyToken(rawToken);
if (!Boolean.TRUE.equals(claims.get("valid"))) {
return AjaxResult.error(401, "token已过期,请重新登录!");
}
Long userId = parseUserId(claims.get("id"));
Object jtiValue = claims.get("jti");
String jti = jtiValue instanceof String ? (String) jtiValue : null;
if (auth.session() && (userId == null || jti == null || jti.isBlank()
|| !redisCache.hasKey(jti))) {
throw new ServiceException(MessageUtils.message("merchant.session.invalid"));
}
request.setAttribute(AuthContext.USER_ID_ATTRIBUTE, userId);
request.setAttribute(AuthContext.JTI_ATTRIBUTE, jti);
AuthContext 只读取认证切面写入的属性,不解析或重新校验 Token:
public static Long requireUserId() {
Object value = ServletUtils.getRequest().getAttribute(USER_ID_ATTRIBUTE);
if (value instanceof Long userId) {
return userId;
}
throw new ServiceException(MessageUtils.message("merchant.session.invalid"));
}
Run: mvn -pl ruoyi-system -Dtest=AuthAspectTest test
Expected: PASS,且无 Redis 访问发生在默认 @Auth 场景。
Files:
ruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantTokenSessionService.javaruoyi-admin/src/main/java/com/ruoyi/app/user/InfoUserController.javaruoyi-admin/src/main/java/com/ruoyi/app/user/MerchantSubaccountController.javaruoyi-admin/src/test/java/com/ruoyi/app/user/InfoUserControllerTest.javaruoyi-admin/src/test/java/com/ruoyi/app/user/MerchantSubaccountControllerTest.javaInterfaces:
@Auth(session = true)、AuthContext.requireUserId()、AuthContext.requireJti()。MerchantTokenSessionService.logoutCurrent(String jti) 只删除已经过认证的当前 JTI。Preserves: revokeAll(Long)、isOnline(Long)、hasAppSession(Long)。
[ ] Step 1: 更新失败测试
账号管理 Controller 测试不再模拟 tokenSessionService.requireUserId(token),改为静态模拟认证上下文;退出测试验证删除可信 JTI 而不是重新解析 Token:
try (MockedStatic<AuthContext> auth = mockStatic(AuthContext.class)) {
auth.when(AuthContext::requireUserId).thenReturn(936L);
controller.list("merchant-token");
verify(applicationService).listForOwner(936L);
}
auth.when(AuthContext::requireJti).thenReturn("qtw_tokens:sh:app:936:test");
verify(merchantTokenSessionService).logoutCurrent("qtw_tokens:sh:app:936:test");
Run:
mvn -pl ruoyi-admin -am -Dtest=InfoUserControllerTest,MerchantSubaccountControllerTest -Dsurefire.failIfNoSpecifiedTests=false test
Expected: FAIL,因为 Controller 仍调用 requireUserId(token),退出仍把原 Token 传给会话服务。
将商家退出和五个分管账号接口改为 @Auth(session = true);Controller 使用 AuthContext:
@Auth(session = true)
public AjaxResult list(@RequestHeader String token) {
return success(applicationService.listForOwner(AuthContext.requireUserId()));
}
@Auth(session = true)
public AjaxResult merchantLogout(@RequestHeader String token) {
merchantTokenSessionService.logoutCurrent(AuthContext.requireJti());
return success();
}
删除 MerchantTokenSessionService.requireUserId,logoutCurrent 只删除非空 JTI;撤销和在线查询方法不变。
Run: mvn -pl ruoyi-admin -am -Dtest=InfoUserControllerTest,MerchantSubaccountControllerTest -Dsurefire.failIfNoSpecifiedTests=false test
Expected: PASS。
Files:
ruoyi-admin/src/main/java/com/ruoyi/app/mendian/FoodSpecController.javaruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosFenleiController.javaruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosFoodController.javaruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosStoreController.javaruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderController.javaruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.javaruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosFoodControllerAccessTest.javaruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosFoodControllerShelfFilterTest.javaruoyi-admin/src/test/java/com/ruoyi/app/mendian/PosStoreControllerTest.javaruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderShOprateControllerTest.javaInterfaces:
@Auth(session = true) 与 AuthContext.requireUserId()。MerchantStoreAccessService 的门店、商品和订单归属校验。Removes: 六个业务 Controller 对 MerchantTokenSessionService 的注入和调用。
[ ] Step 1: 更新 Controller 定向测试形成 RED
在直接调用 Controller 的测试中静态模拟 AuthContext.requireUserId(),删除 MerchantTokenSessionService.requireUserId 桩;保留并断言原有归属校验:
authContext = mockStatic(AuthContext.class);
authContext.when(AuthContext::requireUserId).thenReturn(936L);
controller.getmystorelist("merchant-token");
verify(merchantStoreAccessService).getAccessibleStoreIds(936L);
Run:
mvn -pl ruoyi-admin -am -Dtest=PosStoreControllerTest,PosFoodControllerAccessTest,PosFoodControllerShelfFilterTest,PosOrderShOprateControllerTest -Dsurefire.failIfNoSpecifiedTests=false test
Expected: FAIL,因为生产 Controller 仍依赖 MerchantTokenSessionService.requireUserId(token)。
所有当前依赖 requireUserId(token) 的公开入口改为 @Auth(session = true),方法体改取可信上下文:
Long loginUserId = AuthContext.requireUserId();
merchantStoreAccessService.requireStoreAccess(loginUserId, mdId);
私有辅助方法不再接收 Token 来认证,统一读取或接收已经取得的 loginUserId;删除六个 Controller 中不再使用的 MerchantTokenSessionService import 和字段。不得删除 requireStoreAccess、requireFoodAccess、requireOrderAccess 或订单所有权检查。
Run:
rg -n "MerchantTokenSessionService.*requireUserId|merchantTokenSessionService\.requireUserId|tokenSessionService\.requireUserId" ruoyi-admin/src/main/java
Expected: 无匹配。
Run: mvn -pl ruoyi-admin -am -Dtest=PosStoreControllerTest,PosFoodControllerAccessTest,PosFoodControllerShelfFilterTest,PosOrderShOprateControllerTest -Dsurefire.failIfNoSpecifiedTests=false test
Expected: PASS。
Files:
specs/022-merchant-store-subaccounts/tasks.mdInterfaces:
Produces: 一个不混入既有脏文件的中文实现提交并推送当前分支。
[ ] Step 1: 运行全部定向测试
Run:
$env:JAVA_HOME='C:\Users\qmj\.jdks\graalvm-jdk-21.0.7'
$env:PATH="$env:JAVA_HOME\bin;$env:PATH"
mvn -pl ruoyi-admin -am -Dtest=AuthAspectTest,InfoUserControllerTest,MerchantSubaccountControllerTest,PosStoreControllerTest,PosFoodControllerAccessTest,PosFoodControllerShelfFilterTest,PosOrderShOprateControllerTest -Dsurefire.failIfNoSpecifiedTests=false test
Expected: BUILD SUCCESS,所有指定测试零失败。
Run: mvn -pl ruoyi-admin -am -DskipTests package
Expected: BUILD SUCCESS。
将 tasks.md 的 T012 六项全部勾选;运行:
git diff --check
rg -n "MerchantTokenSessionService.*requireUserId|merchantTokenSessionService\.requireUserId|tokenSessionService\.requireUserId" ruoyi-admin/src/main/java
git status --short
Expected: git diff --check 无错误,rg 无匹配,状态清单只包含本任务文件和既有未暂存文件。
仅暂存 Task 1~3 的源码、测试及 specs/022-merchant-store-subaccounts 本任务文件,核对 git diff --cached --name-only 后提交:
git commit -m "统一商家会话认证入口" -m "将 JWT 与 Redis 会话校验集中到认证切面,业务控制器仅使用可信身份执行门店和订单归属校验。"
Run: git push
Expected: 当前分支推送成功,记录提交 SHA 和远端分支。