feat(auth): 完善API网关JWT认证和权限控制功能

- 实现网关侧JWT工具类和权限规则匹配器
- 集成JWT认证流程,支持Bearer Token验证
- 添加基于路径和HTTP方法的权限控制机制
- 配置白名单路由规则,优化认证性能
- 更新前端受保护路由组件,实现权限验证
- 添加403禁止访问页面和权限检查逻辑
- 重构登录页面,集成实际认证API调用
- 实现用户信息获取和权限加载功能
- 优化全局异常处理器中的认证错误状态码
- 集成FastJSON2和JJWT依赖库支持
This commit is contained in:
2026-02-06 13:11:08 +08:00
parent 719f54bf2e
commit 056cee11cc
33 changed files with 1462 additions and 89 deletions

View File

@@ -1,34 +1,124 @@
package com.datamate.gateway.filter;
import com.alibaba.fastjson2.JSONObject;
import com.datamate.gateway.security.GatewayJwtUtils;
import com.datamate.gateway.security.PermissionRuleMatcher;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* 用户信息过滤器
*
*/
@Slf4j
@Component
public class UserContextFilter implements GlobalFilter {
@Value("${commercial.switch:false}")
private boolean isCommercial;
public class UserContextFilter implements GlobalFilter, Ordered {
private final GatewayJwtUtils gatewayJwtUtils;
private final PermissionRuleMatcher permissionRuleMatcher;
@Value("${datamate.auth.enabled:true}")
private boolean authEnabled;
public UserContextFilter(GatewayJwtUtils gatewayJwtUtils, PermissionRuleMatcher permissionRuleMatcher) {
this.gatewayJwtUtils = gatewayJwtUtils;
this.permissionRuleMatcher = permissionRuleMatcher;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (!isCommercial) {
if (!authEnabled) {
return chain.filter(exchange);
}
try {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
HttpMethod method = request.getMethod();
} catch (Exception e) {
log.error("get current user info error", e);
if (!path.startsWith("/api/")) {
return chain.filter(exchange);
}
return chain.filter(exchange);
if (HttpMethod.OPTIONS.equals(method)) {
return chain.filter(exchange);
}
if (permissionRuleMatcher.isWhitelisted(path)) {
return chain.filter(exchange);
}
String token = extractBearerToken(request.getHeaders().getFirst("Authorization"));
if (!StringUtils.hasText(token)) {
return writeError(exchange, HttpStatus.UNAUTHORIZED, "auth.0003", "未登录或登录状态已失效");
}
Claims claims;
try {
if (!gatewayJwtUtils.validateToken(token)) {
return writeError(exchange, HttpStatus.UNAUTHORIZED, "auth.0003", "登录状态已失效");
}
claims = gatewayJwtUtils.getClaimsFromToken(token);
} catch (Exception ex) {
log.warn("JWT校验失败: {}", ex.getMessage());
return writeError(exchange, HttpStatus.UNAUTHORIZED, "auth.0003", "登录状态已失效");
}
String requiredPermission = permissionRuleMatcher.resolveRequiredPermission(method, path);
if (StringUtils.hasText(requiredPermission)) {
List<String> permissionCodes = gatewayJwtUtils.getStringListClaim(claims, "permissions");
if (!permissionCodes.contains(requiredPermission)) {
return writeError(exchange, HttpStatus.FORBIDDEN, "auth.0006", "权限不足");
}
}
String userId = String.valueOf(claims.get("userId"));
String username = claims.getSubject();
List<String> roles = gatewayJwtUtils.getStringListClaim(claims, "roles");
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-User-Id", userId)
.header("X-User-Name", username)
.header("X-User-Roles", String.join(",", roles))
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
@Override
public int getOrder() {
return -200;
}
private String extractBearerToken(String authorizationHeader) {
if (!StringUtils.hasText(authorizationHeader)) {
return null;
}
if (!authorizationHeader.startsWith("Bearer ")) {
return null;
}
String token = authorizationHeader.substring("Bearer ".length()).trim();
return token.isEmpty() ? null : token;
}
private Mono<Void> writeError(ServerWebExchange exchange,
HttpStatus status,
String code,
String message) {
exchange.getResponse().setStatusCode(status);
exchange.getResponse().getHeaders().set("Content-Type", "application/json;charset=UTF-8");
byte[] body = JSONObject.toJSONString(new ErrorBody(code, message, null))
.getBytes(StandardCharsets.UTF_8);
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(body)));
}
private record ErrorBody(String code, String message, Object data) {
}
}

View File

@@ -0,0 +1,65 @@
package com.datamate.gateway.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 网关侧JWT工具
*/
@Component
public class GatewayJwtUtils {
private static final String DEFAULT_SECRET = "datamate-secret-key-for-jwt-token-generation";
@Value("${jwt.secret:" + DEFAULT_SECRET + "}")
private String secret;
public Claims getClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
public boolean validateToken(String token) {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration != null && expiration.after(new Date());
}
public List<String> getStringListClaim(Claims claims, String claimName) {
Object claimValue = claims.get(claimName);
if (!(claimValue instanceof Collection<?> values)) {
return Collections.emptyList();
}
return values.stream()
.map(String::valueOf)
.collect(Collectors.toList());
}
private SecretKey getSigningKey() {
String secretValue = StringUtils.hasText(secret) ? secret : DEFAULT_SECRET;
try {
MessageDigest digest = MessageDigest.getInstance("SHA-512");
byte[] keyBytes = digest.digest(secretValue.getBytes(StandardCharsets.UTF_8));
return Keys.hmacShaKeyFor(keyBytes);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Cannot initialize JWT signing key", e);
}
}
}

View File

@@ -0,0 +1,85 @@
package com.datamate.gateway.security;
import lombok.Getter;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* 权限规则匹配器
*/
@Component
public class PermissionRuleMatcher {
private static final Set<HttpMethod> READ_METHODS = Set.of(HttpMethod.GET, HttpMethod.HEAD);
private static final Set<HttpMethod> WRITE_METHODS = Set.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH, HttpMethod.DELETE);
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final List<String> whiteListPatterns = List.of(
"/api/auth/login",
"/api/auth/login/**"
);
private final List<PermissionRule> rules = buildRules();
public boolean isWhitelisted(String path) {
return whiteListPatterns.stream().anyMatch(pattern -> pathMatcher.match(pattern, path));
}
public String resolveRequiredPermission(HttpMethod method, String path) {
for (PermissionRule rule : rules) {
if (rule.matches(method, path, pathMatcher)) {
return rule.getPermissionCode();
}
}
return null;
}
private List<PermissionRule> buildRules() {
List<PermissionRule> permissionRules = new ArrayList<>();
addModuleRules(permissionRules, "/api/data-management/**", "module:data-management:read", "module:data-management:write");
addModuleRules(permissionRules, "/api/annotation/**", "module:data-annotation:read", "module:data-annotation:write");
addModuleRules(permissionRules, "/api/data-collection/**", "module:data-collection:read", "module:data-collection:write");
addModuleRules(permissionRules, "/api/evaluation/**", "module:data-evaluation:read", "module:data-evaluation:write");
addModuleRules(permissionRules, "/api/synthesis/**", "module:data-synthesis:read", "module:data-synthesis:write");
addModuleRules(permissionRules, "/api/knowledge-base/**", "module:knowledge-base:read", "module:knowledge-base:write");
addModuleRules(permissionRules, "/api/operator-market/**", "module:operator-market:read", "module:operator-market:write");
addModuleRules(permissionRules, "/api/orchestration/**", "module:orchestration:read", "module:orchestration:write");
addModuleRules(permissionRules, "/api/content-generation/**", "module:content-generation:use", "module:content-generation:use");
permissionRules.add(new PermissionRule(READ_METHODS, "/api/auth/users/**", "system:user:manage"));
permissionRules.add(new PermissionRule(WRITE_METHODS, "/api/auth/users/**", "system:user:manage"));
permissionRules.add(new PermissionRule(READ_METHODS, "/api/auth/roles/**", "system:role:manage"));
permissionRules.add(new PermissionRule(WRITE_METHODS, "/api/auth/roles/**", "system:role:manage"));
permissionRules.add(new PermissionRule(READ_METHODS, "/api/auth/permissions/**", "system:permission:manage"));
permissionRules.add(new PermissionRule(WRITE_METHODS, "/api/auth/permissions/**", "system:permission:manage"));
return permissionRules;
}
private void addModuleRules(List<PermissionRule> rules,
String pathPattern,
String readPermissionCode,
String writePermissionCode) {
rules.add(new PermissionRule(READ_METHODS, pathPattern, readPermissionCode));
rules.add(new PermissionRule(WRITE_METHODS, pathPattern, writePermissionCode));
}
@Getter
private static class PermissionRule {
private final Set<HttpMethod> methods;
private final String pathPattern;
private final String permissionCode;
private PermissionRule(Set<HttpMethod> methods, String pathPattern, String permissionCode) {
this.methods = methods;
this.pathPattern = pathPattern;
this.permissionCode = permissionCode;
}
private boolean matches(HttpMethod method, String path, AntPathMatcher matcher) {
return method != null && methods.contains(method) && matcher.match(pathPattern, path);
}
}
}