You've already forked DataMate
feat(auth): 完善API网关JWT认证和权限控制功能
- 实现网关侧JWT工具类和权限规则匹配器 - 集成JWT认证流程,支持Bearer Token验证 - 添加基于路径和HTTP方法的权限控制机制 - 配置白名单路由规则,优化认证性能 - 更新前端受保护路由组件,实现权限验证 - 添加403禁止访问页面和权限检查逻辑 - 重构登录页面,集成实际认证API调用 - 实现用户信息获取和权限加载功能 - 优化全局异常处理器中的认证错误状态码 - 集成FastJSON2和JJWT依赖库支持
This commit is contained in:
@@ -36,6 +36,23 @@
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user