diff --git a/docs/superpowers/plans/2026-04-14-auth-company-optimization.md b/docs/superpowers/plans/2026-04-14-auth-company-optimization.md
new file mode 100644
index 0000000..9b68b0e
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-14-auth-company-optimization.md
@@ -0,0 +1,66 @@
+# Auth And Company Optimization Implementation Plan
+
+> **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:** Replace the remaining Shiro authorization layer with project-owned Redis token authentication and add company CRUD APIs.
+
+**Architecture:** Keep the existing UUID token, Redis session storage, and `CompanyContext` tenant injection. Add project-owned `@RequireAuth` and `@RequireRole` annotations plus a Spring MVC `AuthInterceptor`, then remove Shiro config/classes/dependencies. Add `CompanyService` and `CompanyController` for `sys_company` management.
+
+**Tech Stack:** Java 21, Spring Boot 3.1.5, Spring MVC HandlerInterceptor, RedisTemplate, MyBatis-Plus, JUnit 5, Mockito, AssertJ.
+
+---
+
+### Task 1: Replace Shiro With Custom Auth Interceptor
+
+**Files:**
+- Create: `src/main/java/com/label/annotation/RequireAuth.java`
+- Create: `src/main/java/com/label/annotation/RequireRole.java`
+- Create: `src/main/java/com/label/interceptor/AuthInterceptor.java`
+- Create: `src/main/java/com/label/common/auth/TokenPrincipal.java`
+- Create: `src/main/java/com/label/common/context/UserContext.java`
+- Modify: `src/main/java/com/label/config/ShiroConfig.java`
+- Modify: `src/main/java/com/label/common/shiro/TokenFilter.java`
+- Modify: `src/main/java/com/label/common/shiro/BearerToken.java`
+- Modify: `src/main/java/com/label/common/shiro/UserRealm.java`
+- Modify: `src/main/java/com/label/controller/*.java`
+- Modify: `src/main/java/com/label/service/*.java`
+- Modify: `pom.xml`
+- Test: `src/test/java/com/label/unit/AuthInterceptorTest.java`
+
+- [x] Write failing tests for token loading, TTL refresh, role hierarchy, and context cleanup.
+- [x] Implement annotations, principal, context, and interceptor.
+- [x] Register the interceptor via Spring MVC config.
+- [x] Replace controller `@RequiresRoles` usage with `@RequireRole`.
+- [x] Remove Shiro-only classes, tests, dependencies, and exception handling.
+- [x] Run `mvn -q "-Dtest=AuthInterceptorTest,OpenApiAnnotationTest" test` and `mvn -q -DskipTests compile`.
+
+### Task 2: Add Company Management
+
+**Files:**
+- Create: `src/main/java/com/label/service/CompanyService.java`
+- Create: `src/main/java/com/label/controller/CompanyController.java`
+- Modify: `src/main/java/com/label/mapper/SysUserMapper.java`
+- Test: `src/test/java/com/label/unit/CompanyServiceTest.java`
+- Test: `src/test/java/com/label/unit/OpenApiAnnotationTest.java`
+
+- [x] Write failing tests for create/list/update/status/delete behavior.
+- [x] Implement service validation and duplicate checks.
+- [x] Implement admin-only controller endpoints under `/api/companies`.
+- [x] Run `mvn -q "-Dtest=CompanyServiceTest,OpenApiAnnotationTest" test` and `mvn -q -DskipTests compile`.
+
+### Task 3: Configuration And Verification
+
+**Files:**
+- Modify: `src/main/resources/application.yml`
+- Modify: `src/test/java/com/label/unit/ApplicationConfigTest.java`
+
+- [x] Rename `shiro.auth.*` config to `auth.*`.
+- [x] Update safe defaults and type-aliases package.
+- [x] Run targeted unit tests and compile.
+- [x] Run `mvn clean test` once and record any external environment blockers.
+
+### Verification Notes
+
+- `mvn -q "-Dtest=LabelBackendApplicationTests,ApplicationConfigTest,AuthInterceptorTest,CompanyServiceTest,OpenApiAnnotationTest" test` passed.
+- `mvn -q -DskipTests compile` passed.
+- `mvn clean test` compiled main/test sources and passed unit tests, then failed only because 10 Testcontainers integration tests could not find a valid Docker environment.
diff --git a/pom.xml b/pom.xml
index 6ecd4d6..5180e89 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,19 +3,16 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
-
org.springframework.boot
spring-boot-starter-parent
3.1.5
-
com.label
label-backend
1.0.0-SNAPSHOT
jar
-
21
UTF-8
@@ -24,7 +21,6 @@
2.3.0
UTF-8
-
@@ -45,32 +41,27 @@
-
org.springframework.boot
spring-boot-starter-web
-
org.springframework.boot
spring-boot-starter-actuator
-
org.springframework.boot
spring-boot-starter-data-redis
-
org.springframework.boot
spring-boot-starter-aop
-
org.postgresql
@@ -78,106 +69,61 @@
${postgrescp.version}
runtime
-
-
com.baomidou
mybatis-plus-boot-starter
${mybatis-plus.version}
-
-
+
- com.baomidou
- mybatis-plus-jsqlparser
- 3.5.10
+ com.github.jsqlparser
+ jsqlparser
+ 4.4
-
org.springdoc
springdoc-openapi-starter-webmvc-ui
- 2.3.0
+ 2.3.0
-
-
-
-
-
- org.apache.shiro
- shiro-core
- jakarta
- 2.0.0
-
-
-
- org.apache.shiro
- shiro-web
- jakarta
- 2.0.0
-
-
-
- org.apache.shiro
- shiro-spring
- jakarta
- 2.0.0
-
-
- org.apache.shiro
- shiro-web
-
-
-
-
software.amazon.awssdk
s3
-
software.amazon.awssdk
sts
-
org.springframework.security
spring-security-crypto
-
org.projectlombok
lombok
true
-
org.springframework.boot
spring-boot-starter-test
test
-
org.testcontainers
postgresql
test
-
org.testcontainers
@@ -185,10 +131,8 @@
test
-
-
org.apache.maven.plugins
@@ -203,7 +147,6 @@
-
org.apache.maven.plugins
@@ -222,7 +165,6 @@
-
org.apache.maven.plugins
@@ -244,8 +186,6 @@
-
-
-
+
\ No newline at end of file
diff --git a/src/main/java/com/label/LabelBackendApplication.java b/src/main/java/com/label/LabelBackendApplication.java
index 056e5bc..533e0da 100644
--- a/src/main/java/com/label/LabelBackendApplication.java
+++ b/src/main/java/com/label/LabelBackendApplication.java
@@ -1,4 +1,3 @@
-
package com.label;
import org.springframework.boot.SpringApplication;
@@ -6,18 +5,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 应用入口。
- *
- * 排除 Shiro Web 自动配置(ShiroWebAutoConfiguration、ShiroWebFilterConfiguration、
- * ShiroWebMvcAutoConfiguration),避免其依赖的 ShiroFilter(javax.servlet.Filter) 与
- * Spring Boot 3. 的 jakarta.servlet 命名空间冲突。 认证/ 授权逻辑改由
- * TokenFilter(OncePerRequestFilter)+ ShiroConfig 手动装配。
*/
-
-// (excludeName = {
-
-// "org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration",
-// "org.apache.shiro.spring.config.web.autoconfigure.ShiroWebFilterConfiguration",
-// "org.apache.shiro.spring.config.web.autoconfigure.ShiroWebMvcAutoConfiguration" })
@SpringBootApplication
public class LabelBackendApplication {
diff --git a/src/main/java/com/label/annotation/RequireAuth.java b/src/main/java/com/label/annotation/RequireAuth.java
new file mode 100644
index 0000000..9ccb677
--- /dev/null
+++ b/src/main/java/com/label/annotation/RequireAuth.java
@@ -0,0 +1,11 @@
+package com.label.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RequireAuth {
+}
diff --git a/src/main/java/com/label/annotation/RequireRole.java b/src/main/java/com/label/annotation/RequireRole.java
new file mode 100644
index 0000000..aded7a0
--- /dev/null
+++ b/src/main/java/com/label/annotation/RequireRole.java
@@ -0,0 +1,13 @@
+package com.label.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@RequireAuth
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RequireRole {
+ String value();
+}
diff --git a/src/main/java/com/label/common/shiro/TokenPrincipal.java b/src/main/java/com/label/common/auth/TokenPrincipal.java
similarity index 75%
rename from src/main/java/com/label/common/shiro/TokenPrincipal.java
rename to src/main/java/com/label/common/auth/TokenPrincipal.java
index 39aa63e..219e80f 100644
--- a/src/main/java/com/label/common/shiro/TokenPrincipal.java
+++ b/src/main/java/com/label/common/auth/TokenPrincipal.java
@@ -1,12 +1,10 @@
-package com.label.common.shiro;
+package com.label.common.auth;
import lombok.AllArgsConstructor;
import lombok.Getter;
+
import java.io.Serializable;
-/**
- * Shiro principal carrying the authenticated user's session data.
- */
@Getter
@AllArgsConstructor
public class TokenPrincipal implements Serializable {
diff --git a/src/main/java/com/label/common/context/UserContext.java b/src/main/java/com/label/common/context/UserContext.java
new file mode 100644
index 0000000..466a1ef
--- /dev/null
+++ b/src/main/java/com/label/common/context/UserContext.java
@@ -0,0 +1,23 @@
+package com.label.common.context;
+
+import com.label.common.auth.TokenPrincipal;
+
+public final class UserContext {
+ private static final ThreadLocal PRINCIPAL = new ThreadLocal<>();
+
+ public static void set(TokenPrincipal principal) {
+ PRINCIPAL.set(principal);
+ }
+
+ public static TokenPrincipal get() {
+ return PRINCIPAL.get();
+ }
+
+ public static void clear() {
+ PRINCIPAL.remove();
+ }
+
+ private UserContext() {
+ throw new UnsupportedOperationException("Utility class");
+ }
+}
diff --git a/src/main/java/com/label/common/exception/GlobalExceptionHandler.java b/src/main/java/com/label/common/exception/GlobalExceptionHandler.java
index 676fc89..07896f6 100644
--- a/src/main/java/com/label/common/exception/GlobalExceptionHandler.java
+++ b/src/main/java/com/label/common/exception/GlobalExceptionHandler.java
@@ -2,8 +2,6 @@ package com.label.common.exception;
import com.label.common.result.Result;
import lombok.extern.slf4j.Slf4j;
-import org.apache.shiro.authz.AuthorizationException;
-import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@@ -16,26 +14,15 @@ public class GlobalExceptionHandler {
public ResponseEntity> handleBusinessException(BusinessException e) {
log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
return ResponseEntity
- .status(e.getHttpStatus())
- .body(Result.failure(e.getCode(), e.getMessage()));
- }
-
- /**
- * 处理 Shiro 权限不足异常(@RequiresRoles / subject.checkRole() 抛出)→ 403
- */
- @ExceptionHandler(AuthorizationException.class)
- public ResponseEntity> handleAuthorizationException(AuthorizationException e) {
- log.warn("权限不足: {}", e.getMessage());
- return ResponseEntity
- .status(HttpStatus.FORBIDDEN)
- .body(Result.failure("FORBIDDEN", "权限不足"));
+ .status(e.getHttpStatus())
+ .body(Result.failure(e.getCode(), e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity> handleException(Exception e) {
log.error("系统异常", e);
return ResponseEntity
- .internalServerError()
- .body(Result.failure("INTERNAL_ERROR", "系统内部错误"));
+ .internalServerError()
+ .body(Result.failure("INTERNAL_ERROR", "系统内部错误"));
}
}
diff --git a/src/main/java/com/label/common/shiro/BearerToken.java b/src/main/java/com/label/common/shiro/BearerToken.java
deleted file mode 100644
index 5febfc9..0000000
--- a/src/main/java/com/label/common/shiro/BearerToken.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.label.common.shiro;
-
-import org.apache.shiro.authc.AuthenticationToken;
-
-/**
- * Shiro AuthenticationToken wrapper for Bearer token strings.
- */
-public class BearerToken implements AuthenticationToken {
- private final String token;
- private final TokenPrincipal principal;
-
- public BearerToken(String token, TokenPrincipal principal) {
- this.token = token;
- this.principal = principal;
- }
-
- @Override
- public Object getPrincipal() {
- return principal;
- }
-
- @Override
- public Object getCredentials() {
- return token;
- }
-}
diff --git a/src/main/java/com/label/common/shiro/TokenFilter.java b/src/main/java/com/label/common/shiro/TokenFilter.java
deleted file mode 100644
index 2f893f0..0000000
--- a/src/main/java/com/label/common/shiro/TokenFilter.java
+++ /dev/null
@@ -1,139 +0,0 @@
-package com.label.common.shiro;
-
-import java.io.IOException;
-import java.util.Map;
-
-import org.apache.shiro.SecurityUtils;
-import org.apache.shiro.util.ThreadContext;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.MediaType;
-import org.springframework.web.filter.OncePerRequestFilter;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.label.common.context.CompanyContext;
-import com.label.common.result.Result;
-import com.label.service.RedisService;
-import com.label.util.RedisUtil;
-
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-
-/**
- * JWT-style Bearer Token 过滤器。
- * 继承 Spring 的 OncePerRequestFilter(jakarta.servlet),避免与 Shiro 1.x
- * 的 PathMatchingFilter(javax.servlet)产生命名空间冲突。
- *
- * 过滤逻辑:
- * - 跳过非 /api/ 路径和 /api/auth/login(公开端点)
- * - 解析 "Authorization: Bearer {uuid}",查询 Redis Hash token:{uuid}
- * - Token 存在 → 注入 CompanyContext,登录 Shiro Subject,继续请求链路
- * - Token 缺失或过期 → 直接返回 401
- * - finally 块中清除 CompanyContext 和 ThreadContext Subject,防止线程池串漏
- */
-@Slf4j
-@RequiredArgsConstructor
-public class TokenFilter extends OncePerRequestFilter {
-
- private final RedisService redisService;
- private final ObjectMapper objectMapper;
-
- @Value("${shiro.auth.enabled:true}")
- private boolean authEnabled;
-
- @Value("${shiro.auth.mock-company-id:1}")
- private Long mockCompanyId;
-
- @Value("${shiro.auth.mock-user-id:1}")
- private Long mockUserId;
-
- @Value("${shiro.auth.mock-role:ADMIN}")
- private String mockRole;
-
- @Value("${shiro.auth.mock-username:mock}")
- private String mockUsername;
-
- @Value("${token.ttl-seconds:7200}")
- private long tokenTtlSeconds;
-
- /**
- * 公开端点跳过过滤:非 /api/ 前缀路径,以及登录接口本身。
- */
- @Override
- protected boolean shouldNotFilter(HttpServletRequest request) {
- String path = request.getServletPath();
- return !path.startsWith("/api/")
- || path.equals("/api/auth/login")
- || path.equals("/api/video/callback")
- || path.startsWith("/swagger-ui")
- || path.startsWith("/v3/api-docs"); // AI 服务内部回调,不走用户 Token 认证
- }
-
- @Override
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
- FilterChain filterChain) throws ServletException, IOException {
- try {
- if (!authEnabled) {
- TokenPrincipal principal = new TokenPrincipal(
- mockUserId, mockRole, mockCompanyId, mockUsername, "mock-token");
- CompanyContext.set(mockCompanyId);
- SecurityUtils.getSubject().login(new BearerToken("mock-token", principal));
- request.setAttribute("__token_principal__", principal);
- filterChain.doFilter(request, response);
- return;
- }
-
- String authHeader = request.getHeader("Authorization");
- if (authHeader == null || !authHeader.toLowerCase().startsWith("bearer ")) {
- writeUnauthorized(response, "缺少或无效的认证令牌");
- return;
- }
- String[] parts = authHeader.split("\\s+");
- if (parts.length != 2 || !"Bearer".equalsIgnoreCase(parts[0])) {
- writeUnauthorized(response, "无效的认证格式");
- return;
- }
- String token = parts[1];
- // String token = authHeader.substring(7).trim();
- Map