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); } ```