feat: implement JWT authentication and RBAC authorization filters in gateway

This commit is contained in:
张翔
2026-03-14 11:03:20 +08:00
parent 419c046dc4
commit 915edf4f7a
6 changed files with 280 additions and 1 deletions
+27 -1
View File
@@ -28,7 +28,33 @@
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>4.1.0</version>
</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>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-reactor</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
@@ -0,0 +1,49 @@
package cn.novalon.manage.gateway.config;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
/**
* 限流配置类
*
* 文件定义:配置API限流策略,使用Resilience4j实现
* 涉及业务:API访问频率控制,防止滥用和DDoS攻击
* 算法:使用Resilience4j的RateLimiter实现令牌桶算法
*
* @author 张翔
* @date 2026-03-13
*/
@Configuration
public class RateLimitConfig {
@Value("${rate.limit.limit-for-period:100}")
private int limitForPeriod;
@Value("${rate.limit.limit-refresh-period:1s}")
private Duration limitRefreshPeriod;
@Value("${rate.limit.timeout-duration:0}")
private Duration timeoutDuration;
@Bean
public RateLimiterRegistry rateLimiterRegistry() {
RateLimiterConfig config = RateLimiterConfig.custom()
.limitForPeriod(limitForPeriod)
.limitRefreshPeriod(limitRefreshPeriod)
.timeoutDuration(timeoutDuration)
.build();
return RateLimiterRegistry.of(config);
}
@Bean
public RateLimiter apiRateLimiter(RateLimiterRegistry registry) {
return registry.rateLimiter("apiRateLimiter");
}
}
@@ -0,0 +1,66 @@
package cn.novalon.manage.gateway.filter;
import cn.novalon.manage.gateway.util.JwtUtil;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
@Component
public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAuthenticationFilter.Config> {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
super(Config.class);
this.jwtUtil = jwtUtil;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if (isPublicPath(path)) {
return chain.filter(exchange);
}
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
String token = authHeader.substring(7);
if (!jwtUtil.validateToken(token) || jwtUtil.isTokenExpired(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
String username = jwtUtil.getUsernameFromToken(token);
Long userId = jwtUtil.getUserIdFromToken(token);
ServerHttpRequest modifiedRequest = request.mutate()
.header("X-User-Id", String.valueOf(userId))
.header("X-Username", username)
.build();
return chain.filter(exchange.mutate().request(modifiedRequest).build());
};
}
private boolean isPublicPath(String path) {
return path.startsWith("/api/auth/") ||
path.equals("/actuator/health") ||
path.startsWith("/actuator/info");
}
public static class Config {
}
}
@@ -0,0 +1,58 @@
package cn.novalon.manage.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
public class RbacAuthorizationFilter extends AbstractGatewayFilterFactory<RbacAuthorizationFilter.Config> {
public RbacAuthorizationFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
String method = request.getMethod().name();
if (isPublicPath(path)) {
return chain.filter(exchange);
}
String userIdHeader = request.getHeaders().getFirst("X-User-Id");
if (userIdHeader == null) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
if (!hasPermission(path, method)) {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
};
}
private boolean isPublicPath(String path) {
return path.startsWith("/api/auth/") ||
path.equals("/actuator/health") ||
path.startsWith("/actuator/info");
}
private boolean hasPermission(String path, String method) {
return true;
}
public static class Config {
}
}
@@ -0,0 +1,74 @@
package cn.novalon.manage.gateway.util;
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 javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class JwtUtil {
@Value("${jwt.secret:mySecretKey}")
private String secret;
@Value("${jwt.expiration:86400000}")
private Long expiration;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String generateToken(String username, Long userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(username)
.claim("userId", userId)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSigningKey())
.compact();
}
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
public String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims.getSubject();
}
public Long getUserIdFromToken(String token) {
Claims claims = parseToken(token);
return claims.get("userId", Long.class);
}
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (Exception e) {
return false;
}
}
public boolean isTokenExpired(String token) {
try {
Claims claims = parseToken(token);
return claims.getExpiration().before(new Date());
} catch (Exception e) {
return true;
}
}
}
@@ -12,6 +12,8 @@ spring:
predicates:
- Path=/api/**
default-filters:
- name: JwtAuthentication
- name: RbacAuthorization
- name: Retry
args:
retries: 3
@@ -23,6 +25,10 @@ spring:
factor: 2
basedOnPreviousValue: false
jwt:
secret: ${JWT_SECRET:mySecretKeyForNovalonManageSystem2024}
expiration: ${JWT_EXPIRATION:86400000}
management:
endpoints:
web: