feat(auth): replace JWT with Sa-Token, add auth context and interceptor
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user