diff --git a/backend/mci-server/pom.xml b/backend/mci-server/pom.xml
index d279cc6..0c2ca74 100644
--- a/backend/mci-server/pom.xml
+++ b/backend/mci-server/pom.xml
@@ -57,6 +57,11 @@
org.springframework.security
spring-security-crypto
+
+ cn.dev33
+ sa-token-spring-boot3-starter
+ 1.39.0
+
org.projectlombok
lombok
diff --git a/backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthContext.java b/backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthContext.java
new file mode 100644
index 0000000..6625281
--- /dev/null
+++ b/backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthContext.java
@@ -0,0 +1,34 @@
+package com.sino.mci.admin.security;
+
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.Data;
+
+import java.util.List;
+
+public class AuthContext {
+
+ @Data
+ public static class AuthUser {
+ private Long userId;
+ private String tenantCode;
+ private String username;
+ private List roleCodes;
+ }
+
+ public static AuthUser get() {
+ if (!StpUtil.isLogin()) {
+ return null;
+ }
+ AuthUser user = new AuthUser();
+ user.setUserId(StpUtil.getLoginIdAsLong());
+ user.setTenantCode((String) StpUtil.getSession().get("tenantCode"));
+ user.setUsername((String) StpUtil.getSession().get("username"));
+ user.setRoleCodes((List) StpUtil.getSession().get("roleCodes"));
+ return user;
+ }
+
+ public static String currentTenantCode() {
+ AuthUser user = get();
+ return user == null ? null : user.getTenantCode();
+ }
+}
diff --git a/backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthInterceptor.java b/backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthInterceptor.java
new file mode 100644
index 0000000..ed63b14
--- /dev/null
+++ b/backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthInterceptor.java
@@ -0,0 +1,28 @@
+package com.sino.mci.admin.security;
+
+import cn.dev33.satoken.exception.SaTokenException;
+import cn.dev33.satoken.stp.StpUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+@Component
+public class AuthInterceptor implements HandlerInterceptor {
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+ String path = request.getRequestURI();
+ if (path.contains("/api/v1/account/login")) {
+ return true;
+ }
+
+ try {
+ StpUtil.checkLogin();
+ } catch (SaTokenException e) {
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/backend/mci-server/src/main/java/com/sino/mci/config/WebConfig.java b/backend/mci-server/src/main/java/com/sino/mci/config/WebConfig.java
new file mode 100644
index 0000000..9d673e0
--- /dev/null
+++ b/backend/mci-server/src/main/java/com/sino/mci/config/WebConfig.java
@@ -0,0 +1,21 @@
+package com.sino.mci.config;
+
+import com.sino.mci.admin.security.AuthInterceptor;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+@RequiredArgsConstructor
+public class WebConfig implements WebMvcConfigurer {
+
+ private final AuthInterceptor authInterceptor;
+
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ registry.addInterceptor(authInterceptor)
+ .addPathPatterns("/msg-platform/api/v1/**", "/api/v1/**")
+ .excludePathPatterns("/msg-platform/api/v1/account/login", "/api/v1/account/login");
+ }
+}
diff --git a/backend/mci-server/src/main/resources/application.yml b/backend/mci-server/src/main/resources/application.yml
index 8b9b89e..30f388b 100644
--- a/backend/mci-server/src/main/resources/application.yml
+++ b/backend/mci-server/src/main/resources/application.yml
@@ -21,6 +21,16 @@ mybatis-plus:
logic-delete-value: 1
logic-not-delete-value: 0
+sa-token:
+ token-name: Authorization
+ token-prefix: Bearer
+ timeout: 86400
+ active-timeout: -1
+ is-concurrent: true
+ is-share: true
+ token-style: uuid
+ is-log: false
+
management:
endpoints:
web:
diff --git a/docs/superpowers/plans/2026-07-07-admin-auth-flow-testing-plan.md b/docs/superpowers/plans/2026-07-07-admin-auth-flow-testing-plan.md
index 64dadd0..af2e538 100644
--- a/docs/superpowers/plans/2026-07-07-admin-auth-flow-testing-plan.md
+++ b/docs/superpowers/plans/2026-07-07-admin-auth-flow-testing-plan.md
@@ -112,133 +112,59 @@
---
-## Task 1: 后端 JWT 与鉴权基础设施
+## Task 1: 后端 Sa-Token 与鉴权基础设施
**Files:**
- Modify: `backend/mci-server/pom.xml`
-- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/security/JwtTokenProvider.java`
- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthContext.java`
- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthInterceptor.java`
- Create: `backend/mci-server/src/main/java/com/sino/mci/config/WebConfig.java`
+- Modify: `backend/mci-server/src/main/resources/application.yml`
-- [ ] **Step 1: 添加 jjwt 依赖**
+- [ ] **Step 1: 添加 Sa-Token 依赖**
修改 `backend/mci-server/pom.xml` 的 `` 节点,添加:
```xml
- io.jsonwebtoken
- jjwt-api
- 0.12.6
-
-
- io.jsonwebtoken
- jjwt-impl
- 0.12.6
- runtime
-
-
- io.jsonwebtoken
- jjwt-jackson
- 0.12.6
- runtime
+ cn.dev33
+ sa-token-spring-boot3-starter
+ 1.39.0
```
-- [ ] **Step 2: 创建 JwtTokenProvider**
+- [ ] **Step 2: 创建 AuthContext**
-创建 `backend/mci-server/src/main/java/com/sino/mci/admin/security/JwtTokenProvider.java`:
-
-```java
-package com.sino.mci.admin.security;
-
-import io.jsonwebtoken.*;
-import io.jsonwebtoken.security.Keys;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
-
-import javax.crypto.SecretKey;
-import java.nio.charset.StandardCharsets;
-import java.util.Date;
-import java.util.List;
-
-@Component
-public class JwtTokenProvider {
-
- @Value("${mci.jwt.secret:sino-mci-default-secret-key-at-least-32-bytes}")
- private String secret;
-
- @Value("${mci.jwt.expiration-ms:86400000}")
- private long expirationMs;
-
- private SecretKey getKey() {
- return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
- }
-
- public String generateToken(String tenantCode, Long userId, String username, List roleCodes) {
- Date now = new Date();
- Date expiry = new Date(now.getTime() + expirationMs);
- return Jwts.builder()
- .subject(String.valueOf(userId))
- .claim("tenantCode", tenantCode)
- .claim("username", username)
- .claim("roleCodes", roleCodes)
- .issuedAt(now)
- .expiration(expiry)
- .signWith(getKey())
- .compact();
- }
-
- public Claims parseToken(String token) {
- return Jwts.parser()
- .verifyWith(getKey())
- .build()
- .parseSignedClaims(token)
- .getPayload();
- }
-
- public boolean validateToken(String token) {
- try {
- parseToken(token);
- return true;
- } catch (JwtException | IllegalArgumentException e) {
- return false;
- }
- }
-}
-```
-
-- [ ] **Step 3: 创建 AuthContext**
-
-创建 `backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthContext.java`:
+基于 Sa-Token `StpUtil` 创建 `backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthContext.java`:
```java
package com.sino.mci.admin.security;
+import cn.dev33.satoken.stp.StpUtil;
import lombok.Data;
-public class AuthContext {
+import java.util.List;
- private static final ThreadLocal CURRENT = new ThreadLocal<>();
+public class AuthContext {
@Data
public static class AuthUser {
private Long userId;
private String tenantCode;
private String username;
- private java.util.List roleCodes;
- }
-
- public static void set(AuthUser user) {
- CURRENT.set(user);
+ private List roleCodes;
}
public static AuthUser get() {
- return CURRENT.get();
- }
-
- public static void clear() {
- CURRENT.remove();
+ if (!StpUtil.isLogin()) {
+ return null;
+ }
+ AuthUser user = new AuthUser();
+ user.setUserId(StpUtil.getLoginIdAsLong());
+ user.setTenantCode((String) StpUtil.getSession().get("tenantCode"));
+ user.setUsername((String) StpUtil.getSession().get("username"));
+ user.setRoleCodes((List) StpUtil.getSession().get("roleCodes"));
+ return user;
}
public static String currentTenantCode() {
@@ -248,65 +174,42 @@ public class AuthContext {
}
```
-- [ ] **Step 4: 创建 AuthInterceptor**
+- [ ] **Step 3: 创建 AuthInterceptor**
-创建 `backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthInterceptor.java`:
+创建 `backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthInterceptor.java`,使用 Sa-Token 校验登录:
```java
package com.sino.mci.admin.security;
-import io.jsonwebtoken.Claims;
+import cn.dev33.satoken.exception.SaTokenException;
+import cn.dev33.satoken.stp.StpUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
-import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
-import java.util.List;
-
@Component
-@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {
- private final JwtTokenProvider jwtTokenProvider;
-
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String path = request.getRequestURI();
- if (path.contains("/api/v1/account/login") || path.contains("/api/v1/account/tenant")) {
+ if (path.contains("/api/v1/account/login")) {
return true;
}
- String header = request.getHeader("Authorization");
- if (header == null || !header.startsWith("Bearer ")) {
+ try {
+ StpUtil.checkLogin();
+ } catch (SaTokenException e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
-
- String token = header.substring(7);
- if (!jwtTokenProvider.validateToken(token)) {
- response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
- return false;
- }
-
- Claims claims = jwtTokenProvider.parseToken(token);
- AuthContext.AuthUser user = new AuthContext.AuthUser();
- user.setUserId(Long.valueOf(claims.getSubject()));
- user.setTenantCode(claims.get("tenantCode", String.class));
- user.setUsername(claims.get("username", String.class));
- user.setRoleCodes(claims.get("roleCodes", List.class));
- AuthContext.set(user);
return true;
}
-
- @Override
- public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
- AuthContext.clear();
- }
}
```
-- [ ] **Step 5: 注册拦截器**
+- [ ] **Step 4: 注册拦截器并配置 Sa-Token**
创建 `backend/mci-server/src/main/java/com/sino/mci/config/WebConfig.java`:
@@ -334,7 +237,21 @@ public class WebConfig implements WebMvcConfigurer {
}
```
-- [ ] **Step 6: 编译验证**
+在 `backend/mci-server/src/main/resources/application.yml` 中添加 Sa-Token 基础配置:
+
+```yaml
+sa-token:
+ token-name: Authorization
+ token-prefix: Bearer
+ timeout: 86400
+ active-timeout: -1
+ is-concurrent: true
+ is-share: true
+ token-style: uuid
+ is-log: false
+```
+
+- [ ] **Step 5: 编译验证**
Run:
```bash
@@ -344,12 +261,12 @@ JAVA_HOME=/opt/homebrew/opt/openjdk@21 mvn -f mci-server/pom.xml compile -DskipT
Expected: BUILD SUCCESS
-- [ ] **Step 7: Commit**
+- [ ] **Step 6: Commit**
```bash
cd /Users/marsal/Projects/sino-project/sino-mci
-git add backend/mci-server/pom.xml backend/mci-server/src/main/java/com/sino/mci/admin/security/ backend/mci-server/src/main/java/com/sino/mci/config/WebConfig.java
-git commit -m "feat(auth): add JWT provider, auth context and interceptor"
+git add backend/mci-server/pom.xml backend/mci-server/src/main/java/com/sino/mci/admin/security/ backend/mci-server/src/main/java/com/sino/mci/config/WebConfig.java backend/mci-server/src/main/resources/application.yml
+git commit -m "feat(auth): add Sa-Token auth context and interceptor"
```
---
@@ -387,16 +304,12 @@ public class TokenResponse {
修改 `backend/mci-server/src/main/java/com/sino/mci/admin/service/AccountService.java`:
-注入 `JwtTokenProvider`:
+引入 Sa-Token:
```java
-import com.sino.mci.admin.security.JwtTokenProvider;
-
-private final JwtTokenProvider jwtTokenProvider;
+import cn.dev33.satoken.stp.StpUtil;
```
-构造器已使用 `@RequiredArgsConstructor`,无需改动构造器。
-
改造 `login` 方法:
```java
@@ -418,8 +331,12 @@ public TokenResponse login(String tenantCode, String username, String password)
userInfo.setPhone(user.getPhone());
userInfo.setRoleCodes(loadRoleCodes(user.getId()));
- String token = jwtTokenProvider.generateToken(
- user.getTenantCode(), user.getId(), user.getUsername(), userInfo.getRoleCodes());
+ StpUtil.login(user.getId());
+ StpUtil.getSession().set("tenantCode", user.getTenantCode());
+ StpUtil.getSession().set("username", user.getUsername());
+ StpUtil.getSession().set("roleCodes", userInfo.getRoleCodes());
+
+ String token = StpUtil.getTokenValue();
return new TokenResponse(token, userInfo);
}
```