feat: implement JWT authentication and RBAC authorization filters in gateway
This commit is contained in:
@@ -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>
|
||||
|
||||
+49
@@ -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");
|
||||
}
|
||||
}
|
||||
+66
@@ -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 {
|
||||
}
|
||||
}
|
||||
+58
@@ -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 {
|
||||
}
|
||||
}
|
||||
+74
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user