feat(auth): replace JWT with Sa-Token, add auth context and interceptor

This commit is contained in:
2026-07-07 14:52:18 +08:00
parent 14c2c3095b
commit f50a3fd0c9
6 changed files with 154 additions and 139 deletions

View File

@@ -57,6 +57,11 @@
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId> <artifactId>spring-security-crypto</artifactId>
</dependency> </dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.39.0</version>
</dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>

View File

@@ -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<String> 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<String>) StpUtil.getSession().get("roleCodes"));
return user;
}
public static String currentTenantCode() {
AuthUser user = get();
return user == null ? null : user.getTenantCode();
}
}

View File

@@ -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;
}
}

View File

@@ -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");
}
}

View File

@@ -21,6 +21,16 @@ mybatis-plus:
logic-delete-value: 1 logic-delete-value: 1
logic-not-delete-value: 0 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: management:
endpoints: endpoints:
web: web:

View File

@@ -112,133 +112,59 @@
--- ---
## Task 1: 后端 JWT 与鉴权基础设施 ## Task 1: 后端 Sa-Token 与鉴权基础设施
**Files:** **Files:**
- Modify: `backend/mci-server/pom.xml` - 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/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/admin/security/AuthInterceptor.java`
- Create: `backend/mci-server/src/main/java/com/sino/mci/config/WebConfig.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``<dependencies>` 节点,添加: 修改 `backend/mci-server/pom.xml``<dependencies>` 节点,添加:
```xml ```xml
<dependency> <dependency>
<groupId>io.jsonwebtoken</groupId> <groupId>cn.dev33</groupId>
<artifactId>jjwt-api</artifactId> <artifactId>sa-token-spring-boot3-starter</artifactId>
<version>0.12.6</version> <version>1.39.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency> </dependency>
``` ```
- [ ] **Step 2: 创建 JwtTokenProvider** - [ ] **Step 2: 创建 AuthContext**
创建 `backend/mci-server/src/main/java/com/sino/mci/admin/security/JwtTokenProvider.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 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<String> 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`
```java ```java
package com.sino.mci.admin.security; package com.sino.mci.admin.security;
import cn.dev33.satoken.stp.StpUtil;
import lombok.Data; import lombok.Data;
public class AuthContext { import java.util.List;
private static final ThreadLocal<AuthUser> CURRENT = new ThreadLocal<>(); public class AuthContext {
@Data @Data
public static class AuthUser { public static class AuthUser {
private Long userId; private Long userId;
private String tenantCode; private String tenantCode;
private String username; private String username;
private java.util.List<String> roleCodes; private List<String> roleCodes;
}
public static void set(AuthUser user) {
CURRENT.set(user);
} }
public static AuthUser get() { public static AuthUser get() {
return CURRENT.get(); if (!StpUtil.isLogin()) {
} return null;
}
public static void clear() { AuthUser user = new AuthUser();
CURRENT.remove(); user.setUserId(StpUtil.getLoginIdAsLong());
user.setTenantCode((String) StpUtil.getSession().get("tenantCode"));
user.setUsername((String) StpUtil.getSession().get("username"));
user.setRoleCodes((List<String>) StpUtil.getSession().get("roleCodes"));
return user;
} }
public static String currentTenantCode() { 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 ```java
package com.sino.mci.admin.security; 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.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import java.util.List;
@Component @Component
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor { public class AuthInterceptor implements HandlerInterceptor {
private final JwtTokenProvider jwtTokenProvider;
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String path = request.getRequestURI(); 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; return true;
} }
String header = request.getHeader("Authorization"); try {
if (header == null || !header.startsWith("Bearer ")) { StpUtil.checkLogin();
} catch (SaTokenException e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false; 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; 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` 创建 `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: Run:
```bash ```bash
@@ -344,12 +261,12 @@ JAVA_HOME=/opt/homebrew/opt/openjdk@21 mvn -f mci-server/pom.xml compile -DskipT
Expected: BUILD SUCCESS Expected: BUILD SUCCESS
- [ ] **Step 7: Commit** - [ ] **Step 6: Commit**
```bash ```bash
cd /Users/marsal/Projects/sino-project/sino-mci 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 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 JWT provider, auth context and interceptor" 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` 修改 `backend/mci-server/src/main/java/com/sino/mci/admin/service/AccountService.java`
`JwtTokenProvider` Sa-Token
```java ```java
import com.sino.mci.admin.security.JwtTokenProvider; import cn.dev33.satoken.stp.StpUtil;
private final JwtTokenProvider jwtTokenProvider;
``` ```
构造器已使用 `@RequiredArgsConstructor`,无需改动构造器。
改造 `login` 方法: 改造 `login` 方法:
```java ```java
@@ -418,8 +331,12 @@ public TokenResponse login(String tenantCode, String username, String password)
userInfo.setPhone(user.getPhone()); userInfo.setPhone(user.getPhone());
userInfo.setRoleCodes(loadRoleCodes(user.getId())); userInfo.setRoleCodes(loadRoleCodes(user.getId()));
String token = jwtTokenProvider.generateToken( StpUtil.login(user.getId());
user.getTenantCode(), user.getId(), user.getUsername(), userInfo.getRoleCodes()); 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); return new TokenResponse(token, userInfo);
} }
``` ```