生产级 Spring Boot 网关完整实现方案
生产级 Spring Boot 网关简单实现方案-CSDN博客
文章中存在不足。
如果这是Demo项目,我会给 85分。
如果这是准备上线承载真实流量的生产网关,我会给 65分,因为核心骨架有了,但还有不少生产级缺失点。
我按照真实互联网网关(阿里、字节、美团常见实践)帮你挑一下问题。
一、最大的问题:JWT校验位置不对
你现在:
validateToken()
↓
validateSign()
↓
转发
实际上应该:
限流
↓
签名校验
↓
JWT校验
↓
权限校验
↓
转发
原因:
JWT解析是CPU操作。
如果攻击者疯狂发垃圾请求:
Authorization: xxxxxxx
你的网关会先解析JWT。
CPU直接被打爆。
签名校验成本更低。
应该先挡掉非法请求。
二、缺少权限控制(RBAC)
你目前只做了:
header("X-User-Roles", roles)
但没校验。
例如:
/admin/**
应该:
ADMIN
才能访问。
生产环境一般增加:
RoleAuthorizationFilter
例如:
/api/admin/**
需要:
ADMIN
三、缺少IP黑名单
现在只有:
RateLimiter
但没有:
BlackList
生产上通常:
Redis
ip:blacklist:1.1.1.1
过滤器最前面:
if (blacklist.contains(ip))
直接:
403
四、缺少动态限流
现在:
20
10
写死。
生产一定会动态配置。
例如:
/api/login/**
100/s
/api/order/**
20/s
/api/pay/**
5/s
一般放:
* Redis
* Nacos
* Apollo
动态下发。
五、缺少用户维度限流
你只有:
IP限流
问题:
公司出口IP
1000个人
同一个IP
直接被限死。
通常:
userId + ip
双限流。
例如:
rate:user:10001
rate:ip:1.1.1.1
同时校验。
六、防重放基本没实现
这里:
checkReplay()
是假的。
生产真正实现:
Header:
X-Nonce
X-Timestamp
X-Sign
流程:
1 校验时间窗口
当前时间 - timestamp < 300s
2 查询Redis
nonce存在
直接拒绝
3 不存在
SETNX
过期5分钟
Redis:
nonce:xxxxxx
七、签名校验不完整
你自己也注释了:
只校验Query参数
问题很严重。
POST:
{
"amount":100
}
攻击者改成:
{
"amount":1
}
签名还能通过。
生产:
query
+
body
+
timestamp
+
nonce
一起参与签名。
八、Body缓存缺失
WebFlux读取Body:
request.getBody()
只能读一次。
你的签名要读取Body:
validateSign()
下游还要读Body。
必须:
CachedBodyOutputMessage
ServerHttpRequestDecorator
缓存请求体。
这是Spring Gateway生产开发最容易踩坑的地方之一。
九、TraceId生成方式不推荐
现在:
UUID.randomUUID()
生产一般:
MDC
结合:
SkyWalking
Zipkin
OpenTelemetry
例如:
traceId
spanId
形成完整调用链。
推荐:
OpenTelemetry
十、日志不够
现在:
path
status
cost
ip
生产还需要:
userId
clientId
traceId
requestSize
responseSize
userAgent
referer
特别是:
requestSize
responseSize
排查问题非常重要。
十一、缺少熔断降级
真实生产一定有:
CircuitBreaker
例如:
下游:
user-service
挂了。
应该:
fallback
而不是一直超时。
Spring Gateway直接支持:
Resilience4j
例如:
filters:
- name: CircuitBreaker
args:
name: user-service
fallbackUri: forward:/fallback
十二、缺少超时控制
生产必须配置:
spring:
cloud:
gateway:
httpclient:
connect-timeout: 3000
response-timeout: 5s
否则:
下游卡死。
网关线程全部堆积。
十三、JWT密钥管理有问题
现在:
private static final String SECRET
生产绝对不能这样。
应该:
KMS
或者:
环境变量
或者:
Nacos加密配置
否则:
代码泄露=全站失陷。
十四、缺少接口灰度能力
生产网关很重要的一项:
Canary
例如:
X-Version=v2
路由到:
user-service-v2
普通用户:
user-service-v1
Spring Gateway非常适合做这个。
十五、缺少网关监控指标
你引入了:
Actuator
但没真正统计:
QPS
TPS
RT
错误率
限流次数
认证失败次数
通常接:
Prometheus +
Grafana
改造:
优先级排序:
P0(必须补)
1. Body缓存
2. 真正防重放
3. 用户维度限流
4. 熔断降级
5. 超时控制
6. RBAC权限控制
P1(建议补)
1. OpenTelemetry链路追踪
2. IP黑名单
3. 动态限流
4. 灰度发布
5. Prometheus监控
P2(大型系统必备)
1. API网关权限中心
2. OAuth2
3. 多租户
4. WAF防火墙
5. 风控规则引擎
--------------------------------------------以下为改造代码-------------------------------------------------
当前架构问题
1. 自定义 Redis Lua 限流器不如 Spring Gateway 官方 RedisRateLimiter
2. JWT、签名、防重放全部耦合在一个 Filter
3. Body 缓存方案缺失
4. 权限系统(RBAC)缺失
5. 熔断降级缺失
6. OpenTelemetry 链路追踪缺失
7. Redis 故障兜底缺失
8. 配置中心动态规则缺失
9. 网关监控指标缺失
10. 灰度发布能力缺失
直接按下面的生产架构重构:
gateway
│
├── config
│ ├── GatewayConfig
│ ├── RedisConfig
│ ├── SecurityConfig
│ ├── MetricsConfig
│
├── filter
│ ├── TraceFilter
│ ├── IpBlacklistFilter
│ ├── ReplayAttackFilter
│ ├── JwtAuthenticationFilter
│ ├── SignatureVerifyFilter
│ ├── RbacAuthorizationFilter
│ ├── AccessLogFilter
│
├── component
│ ├── JwtManager
│ ├── SignManager
│ ├── ReplayManager
│ ├── UserContext
│
├── ratelimit
│ ├── UserRateLimiter
│ ├── IpRateLimiter
│
├── fallback
│ ├── GatewayFallbackController
│
├── exception
│ ├── GlobalExceptionHandler
│
├── metrics
│ ├── GatewayMetricsCollector
│
└── route
├── GrayRoutePredicateFactory
既然是生产环境,直接按 Spring Boot 3.x + Spring Cloud Gateway + Redis Reactive + Micrometer + Prometheus + JWT(JJWT 0.11.5) 的标准来写。
GatewayConfig.java
负责:
* 全局Gateway配置
* HttpClient连接池
* 超时配置
* Header过滤
package com.xxx.gateway.config;
import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.cloud.gateway.config.HttpClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.netty.http.client.HttpClient;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@Configuration
public class GatewayConfig {
/**
* Gateway底层Netty连接池配置
*/
@Bean
public HttpClientCustomizer httpClientCustomizer() {
return httpClient -> httpClient
// TCP连接超时
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
// 响应超时
.responseTimeout(Duration.ofSeconds(5))
// KeepAlive
.keepAlive(true)
// Read Timeout
.doOnConnected(conn ->
conn.addHandlerLast(
new ReadTimeoutHandler(5, TimeUnit.SECONDS))
.addHandlerLast(
new WriteTimeoutHandler(5, TimeUnit.SECONDS)
)
);
}
}
RedisConfig.java
生产推荐:
* Jackson序列化
* ReactiveRedisTemplate
* StringRedisTemplate
package com.xxx.gateway.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.*;
@Configuration
public class RedisConfig {
@Bean
public ReactiveRedisTemplate<String,Object> reactiveRedisTemplate(
ReactiveRedisConnectionFactory factory) {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(mapper,Object.class);
RedisSerializationContext<String,Object> context =
RedisSerializationContext
.<String,Object>newSerializationContext(
RedisSerializer.string())
.value(serializer)
.hashValue(serializer)
.build();
return new ReactiveRedisTemplate<>(factory,context);
}
@Bean
public StringRedisTemplate stringRedisTemplate(
RedisConnectionFactory factory) {
return new StringRedisTemplate(factory);
}
}
SecurityConfig.java
生产网关:
仅负责认证。
不负责登录。
使用JWT。
package com.xxx.gateway.config;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.crypto.SecretKey;
@Configuration
public class SecurityConfig {
/**
* 建议配置中心管理
*/
@Value("${gateway.jwt.secret}")
private String secret;
@Bean
public SecretKey jwtSecretKey() {
return Keys.hmacShaKeyFor(
Decoders.BASE64.decode(secret)
);
}
}
对应配置:
gateway:
jwt:
secret: >
dGhpc19pc19hX3Byb2R1Y3Rpb25fan
d0X3NlY3JldF9rZXlfZm9yX2dhdGV3
YXlfMjAyNl8wNl8wOA==
不要写:
secret: abc123
生产禁止。
MetricsConfig.java
生产必须接Prometheus。
Maven:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
配置类:
package com.xxx.gateway.config;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.Getter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MetricsConfig {
@Bean
public GatewayMetrics gatewayMetrics(
MeterRegistry registry) {
return new GatewayMetrics(registry);
}
@Getter
public static class GatewayMetrics {
private final Counter requestCounter;
private final Counter authFailCounter;
private final Counter rateLimitCounter;
private final Counter replayAttackCounter;
private final Timer requestTimer;
public GatewayMetrics(
MeterRegistry registry) {
this.requestCounter =
registry.counter(
"gateway_request_total");
this.authFailCounter =
registry.counter(
"gateway_auth_fail_total");
this.rateLimitCounter =
registry.counter(
"gateway_rate_limit_total");
this.replayAttackCounter =
registry.counter(
"gateway_replay_attack_total");
this.requestTimer =
registry.timer(
"gateway_request_duration");
}
}
}
application.yml(配套生产配置)
server:
port: 8080
spring:
application:
name: gateway
cloud:
gateway:
httpclient:
connect-timeout: 3000
response-timeout: 5s
discovery:
locator:
enabled: true
data:
redis:
host: redis-prod
port: 6379
password: xxxxxx
timeout: 2000ms
management:
endpoints:
web:
exposure:
include: health,prometheus,metrics
endpoint:
health:
show-details: always
gateway:
jwt:
secret: dGhpc19pc19hX3Byb2R1Y3Rpb25fan...
TraceFilter
职责:
* 生成 TraceId
* 写入 Header
* 写入 Reactor Context
* 写入 MDC
* 记录耗时
Order:
Ordered.HIGHEST_PRECEDENCE
最先执行。
package com.xxx.gateway.filter;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
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.UUID;
@Slf4j
@Component
public class TraceFilter implements GlobalFilter, Ordered {
public static final String TRACE_ID = "traceId";
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
String traceId =
UUID.randomUUID()
.toString()
.replace("-", "");
long startTime = System.currentTimeMillis();
ServerHttpRequest request =
exchange.getRequest()
.mutate()
.header("X-Trace-Id", traceId)
.build();
exchange.getAttributes()
.put(TRACE_ID, traceId);
exchange.getAttributes()
.put("startTime", startTime);
MDC.put(TRACE_ID, traceId);
return chain.filter(
exchange.mutate()
.request(request)
.build()
)
.doFinally(signal -> {
long cost =
System.currentTimeMillis()
- startTime;
log.info(
"gateway request finish traceId={} path={} cost={}ms",
traceId,
request.getURI().getPath(),
cost
);
MDC.remove(TRACE_ID);
});
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
IpBlacklistFilter
职责:
Redis实时黑名单。
支持运营后台动态封禁。
Redis结构:
gateway:blacklist:ip:1.1.1.1
value:
1
package com.xxx.gateway.filter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Slf4j
@Component
@RequiredArgsConstructor
public class IpBlacklistFilter implements GlobalFilter, Ordered {
private final ReactiveStringRedisTemplate redisTemplate;
private static final String PREFIX =
"gateway:blacklist:ip:";
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
String ip = getClientIp(exchange);
String key = PREFIX + ip;
return redisTemplate
.hasKey(key)
.flatMap(exists -> {
if (Boolean.TRUE.equals(exists)) {
log.warn(
"ip blocked {}",
ip
);
exchange.getResponse()
.setStatusCode(
HttpStatus.FORBIDDEN
);
exchange.getResponse()
.getHeaders()
.setContentType(
MediaType.APPLICATION_JSON
);
byte[] bytes =
"""
{
"code":403,
"msg":"ip blocked"
}
"""
.getBytes();
return exchange
.getResponse()
.writeWith(
Mono.just(
exchange
.getResponse()
.bufferFactory()
.wrap(bytes)
)
);
}
return chain.filter(exchange);
});
}
@Override
public int getOrder() {
return -900;
}
private String getClientIp(
ServerWebExchange exchange) {
String xff =
exchange.getRequest()
.getHeaders()
.getFirst("X-Forwarded-For");
if (xff != null) {
return xff.split(",")[0]
.trim();
}
return exchange
.getRequest()
.getRemoteAddress()
.getAddress()
.getHostAddress();
}
}
ReplayAttackFilter
职责:
防重放攻击。
Header要求:
X-Nonce
X-Timestamp
X-Sign
生产流程:
1 校验时间窗口
2 校验nonce
3 Redis SETNX
4 成功继续
5 失败拒绝
package com.xxx.gateway.filter;
import com.xxx.gateway.component.ReplayManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Slf4j
@Component
@RequiredArgsConstructor
public class ReplayAttackFilter
implements GlobalFilter, Ordered {
private final ReplayManager replayManager;
private static final long WINDOW =
5 * 60 * 1000;
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
String nonce =
exchange.getRequest()
.getHeaders()
.getFirst("X-Nonce");
String timestampStr =
exchange.getRequest()
.getHeaders()
.getFirst("X-Timestamp");
if (nonce == null ||
timestampStr == null) {
return reject(
exchange,
"missing nonce"
);
}
long timestamp;
try {
timestamp =
Long.parseLong(timestampStr);
} catch (Exception e) {
return reject(
exchange,
"invalid timestamp"
);
}
long now =
System.currentTimeMillis();
if (Math.abs(now - timestamp)
> WINDOW) {
return reject(
exchange,
"request expired"
);
}
return replayManager
.checkAndSaveNonce(
nonce,
WINDOW
)
.flatMap(pass -> {
if (!pass) {
return reject(
exchange,
"replay attack"
);
}
return chain.filter(exchange);
});
}
@Override
public int getOrder() {
return -850;
}
private Mono<Void> reject(
ServerWebExchange exchange,
String msg) {
exchange.getResponse()
.setStatusCode(
HttpStatus.FORBIDDEN
);
exchange.getResponse()
.getHeaders()
.setContentType(
MediaType.APPLICATION_JSON
);
byte[] bytes =
String.format(
"""
{
"code":403,
"msg":"%s"
}
""",
msg
).getBytes();
return exchange
.getResponse()
.writeWith(
Mono.just(
exchange
.getResponse()
.bufferFactory()
.wrap(bytes)
)
);
}
}
当前过滤器执行顺序
生产建议:
TraceFilter
order = -1000
↓
IpBlacklistFilter
order = -900
↓
ReplayAttackFilter
order = -850
↓
SignatureVerifyFilter
order = -800
↓
JwtAuthenticationFilter
order = -700
↓
RbacAuthorizationFilter
order = -600
↓
RateLimitFilter
order = -500
↓
AccessLogFilter
order = LOWEST_PRECEDENCE
前面那版代码里有个生产隐患:
MDC.put(...)
在 WebFlux 里并不可靠。
后面我给你的 AccessLogFilter 会优先从 exchange.getAttribute() 获取 TraceId,而不是直接依赖 MDC。
JwtAuthenticationFilter
职责:
* 校验JWT
* 解析用户信息
* 写入UserContext
* Header透传下游
Order:
-700
package com.xxx.gateway.filter;
import com.xxx.gateway.component.JwtManager;
import com.xxx.gateway.component.UserContext;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
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;
import reactor.core.publisher.Mono;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter
implements GlobalFilter, Ordered {
private final JwtManager jwtManager;
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
String token =
exchange.getRequest()
.getHeaders()
.getFirst(HttpHeaders.AUTHORIZATION);
if (token == null || token.isBlank()) {
return unauthorized(exchange, "token missing");
}
return jwtManager.parse(token)
.flatMap(claims -> {
UserContext userContext =
buildUserContext(claims);
exchange.getAttributes()
.put(
UserContext.KEY,
userContext
);
ServerHttpRequest request =
exchange.getRequest()
.mutate()
.header(
"X-User-Id",
userContext.getUserId()
)
.header(
"X-Username",
userContext.getUsername()
)
.header(
"X-Roles",
String.join(
",",
userContext.getRoles()
)
)
.build();
return chain.filter(
exchange.mutate()
.request(request)
.build()
);
})
.onErrorResume(
e -> unauthorized(
exchange,
"token invalid"
)
);
}
@Override
public int getOrder() {
return -700;
}
private UserContext buildUserContext(
Claims claims) {
return UserContext.builder()
.userId(claims.getSubject())
.username(
claims.get(
"username",
String.class
)
)
.roles(
claims.get(
"roles",
java.util.List.class
)
)
.build();
}
private Mono<Void> unauthorized(
ServerWebExchange exchange,
String msg) {
exchange.getResponse()
.setStatusCode(
HttpStatus.UNAUTHORIZED
);
return exchange.getResponse()
.setComplete();
}
}
SignatureVerifyFilter
职责:
校验:
X-Nonce
X-Timestamp
X-Sign
签名内容:
method
path
query
body
nonce
timestamp
Order:
-800
在 JWT 前执行。
package com.xxx.gateway.filter;
import com.xxx.gateway.component.SignManager;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@RequiredArgsConstructor
public class SignatureVerifyFilter
implements GlobalFilter, Ordered {
private final SignManager signManager;
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
return signManager.verify(exchange)
.flatMap(pass -> {
if (!pass) {
exchange.getResponse()
.setStatusCode(
HttpStatus.FORBIDDEN
);
return exchange
.getResponse()
.setComplete();
}
return chain.filter(exchange);
});
}
@Override
public int getOrder() {
return -800;
}
}
RbacAuthorizationFilter
职责:
RBAC权限控制
例如:
gateway:
auth:
rules:
- path: /admin/**
roles:
- ADMIN
- path: /order/**
roles:
- USER
- ADMIN
package com.xxx.gateway.filter;
import com.xxx.gateway.component.UserContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class RbacAuthorizationFilter
implements GlobalFilter, Ordered {
private final AntPathMatcher matcher =
new AntPathMatcher();
/**
* 后面改成Nacos动态加载
*/
private final Map<String, List<String>> ruleMap =
Map.of(
"/admin/**",
List.of("ADMIN"),
"/order/**",
List.of("USER", "ADMIN")
);
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
String path =
exchange.getRequest()
.getURI()
.getPath();
UserContext userContext =
exchange.getAttribute(
UserContext.KEY
);
if (userContext == null) {
exchange.getResponse()
.setStatusCode(
HttpStatus.UNAUTHORIZED
);
return exchange.getResponse()
.setComplete();
}
for (Map.Entry<String, List<String>> entry
: ruleMap.entrySet()) {
if (matcher.match(
entry.getKey(),
path
)) {
boolean pass =
userContext.getRoles()
.stream()
.anyMatch(
entry.getValue()::contains
);
if (!pass) {
exchange.getResponse()
.setStatusCode(
HttpStatus.FORBIDDEN
);
return exchange.getResponse()
.setComplete();
}
}
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -600;
}
}
AccessLogFilter
职责:
统一访问日志
记录:
traceId
userId
path
method
status
cost
ip
ua
requestSize
Order:
LOWEST_PRECEDENCE
最后执行。
package com.xxx.gateway.filter;
import com.xxx.gateway.component.UserContext;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class AccessLogFilter
implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
long start =
System.currentTimeMillis();
return chain.filter(exchange)
.doFinally(signal -> {
long cost =
System.currentTimeMillis()
- start;
UserContext user =
exchange.getAttribute(
UserContext.KEY
);
Map<String, Object> logMap =
new HashMap<>();
logMap.put(
"traceId",
exchange.getAttribute(
"traceId"
)
);
logMap.put(
"userId",
user == null
? null
: user.getUserId()
);
logMap.put(
"path",
exchange.getRequest()
.getURI()
.getPath()
);
logMap.put(
"method",
exchange.getRequest()
.getMethod()
);
logMap.put(
"status",
exchange.getResponse()
.getStatusCode()
);
logMap.put(
"cost",
cost
);
logMap.put(
"ip",
exchange.getRequest()
.getHeaders()
.getFirst(
"X-Forwarded-For"
)
);
logMap.put(
"ua",
exchange.getRequest()
.getHeaders()
.getFirst(
"User-Agent"
)
);
log.info(
JSONUtil.toJsonStr(
logMap
)
);
});
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
TraceFilter
↓
IpBlacklistFilter
↓
ReplayAttackFilter
↓
SignatureVerifyFilter
↓
JwtAuthenticationFilter
↓
RbacAuthorizationFilter
↓
RateLimitFilter(下一批)
↓
AccessLogFilter
UserContext
package com.xxx.gateway.component;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserContext implements Serializable {
public static final String KEY = "USER_CONTEXT";
/**
* 用户ID
*/
private String userId;
/**
* 用户名
*/
private String username;
/**
* 角色
*/
private List<String> roles;
/**
* 租户ID
*/
private String tenantId;
/**
* 客户端ID
*/
private String clientId;
}
JwtManager
生产版 JWT 管理器。
package com.xxx.gateway.component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import javax.crypto.SecretKey;
@Component
@RequiredArgsConstructor
public class JwtManager {
private final SecretKey jwtSecretKey;
public Mono<Claims> parse(String authorization) {
return Mono.fromCallable(() -> {
if (authorization == null) {
throw new RuntimeException("token missing");
}
if (!authorization.startsWith("Bearer ")) {
throw new RuntimeException("invalid token");
}
String token =
authorization.substring(7);
return Jwts.parserBuilder()
.setSigningKey(jwtSecretKey)
.build()
.parseClaimsJws(token)
.getBody();
});
}
public String getUserId(Claims claims) {
return claims.getSubject();
}
public String getUsername(Claims claims) {
return claims.get(
"username",
String.class
);
}
}
ReplayManager
Redis防重放。
核心:
SETNX
EXPIRE
原子执行。
package com.xxx.gateway.component;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.time.Duration;
@Component
@RequiredArgsConstructor
public class ReplayManager {
private final ReactiveStringRedisTemplate redisTemplate;
private static final String PREFIX =
"gateway:nonce:";
/**
* true:
* 首次请求
*
* false:
* 重放请求
*/
public Mono<Boolean> checkAndSaveNonce(
String nonce,
long ttlMillis) {
String key =
PREFIX + nonce;
return redisTemplate
.opsForValue()
.setIfAbsent(
key,
"1",
Duration.ofMillis(ttlMillis)
);
}
}
SignManager
生产建议:
Header:
X-App-Id
X-Nonce
X-Timestamp
X-Sign
签名算法:
method
+
path
+
query
+
timestamp
+
nonce
+
secret
HmacSHA256
package com.xxx.gateway.component;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
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.nio.charset.StandardCharsets;
import java.util.TreeMap;
@Component
@RequiredArgsConstructor
public class SignManager {
@Value("${gateway.sign.secret}")
private String signSecret;
public Mono<Boolean> verify(
ServerWebExchange exchange) {
ServerHttpRequest request =
exchange.getRequest();
String nonce =
request.getHeaders()
.getFirst("X-Nonce");
String timestamp =
request.getHeaders()
.getFirst("X-Timestamp");
String sign =
request.getHeaders()
.getFirst("X-Sign");
if (nonce == null ||
timestamp == null ||
sign == null) {
return Mono.just(false);
}
String content =
buildContent(
request,
nonce,
timestamp
);
String localSign =
generateSign(content);
return Mono.just(
localSign.equalsIgnoreCase(sign)
);
}
private String buildContent(
ServerHttpRequest request,
String nonce,
String timestamp) {
TreeMap<String, String> params =
new TreeMap<>(
request.getQueryParams()
.toSingleValueMap()
);
StringBuilder sb =
new StringBuilder();
sb.append(
request.getMethod()
.name()
);
sb.append("|");
sb.append(
request.getURI()
.getPath()
);
sb.append("|");
params.forEach((k, v) -> {
sb.append(k)
.append("=")
.append(v)
.append("&");
});
sb.append("|");
sb.append(timestamp);
sb.append("|");
sb.append(nonce);
return sb.toString();
}
public String generateSign(
String content) {
HMac hmac =
new HMac(
HmacAlgorithm.HmacSHA256,
signSecret.getBytes(
StandardCharsets.UTF_8
)
);
return hmac.digestHex(content);
}
}
配套配置
gateway:
sign:
secret: your-sign-secret
jwt:
secret: xxxxxxxxxxxxxxxxxxxxxxxxx
不建议用你前面那套自己拼的 RedisTemplate.execute(script -> script.eval(...)) 方式。
生产上建议:
Redis
+
Lua
+
ReactiveRedisTemplate
+
令牌桶(Token Bucket)
支持:
* 集群部署
* 多实例共享限流状态
* 用户限流
* IP限流
* Redis异常降级
先定义公共返回对象
package com.xxx.gateway.ratelimit;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class RateLimitResult {
/**
* 是否允许
*/
private boolean allowed;
/**
* 剩余令牌
*/
private long remainTokens;
}
公共Lua脚本
建议放:
resources/lua/token_bucket.lua
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local lastTime = tonumber(redis.call('HGET', key, 'lastTime') or now)
local tokens = tonumber(redis.call('HGET', key, 'tokens') or capacity)
local delta = math.max(0, now - lastTime)
local refill = delta * rate / 1000
tokens = math.min(capacity, tokens + refill)
local allowed = 0
if tokens >= 1 then
tokens = tokens - 1
allowed = 1
end
redis.call('HSET', key, 'tokens', tokens)
redis.call('HSET', key, 'lastTime', now)
redis.call('EXPIRE', key, 300)
return {
allowed,
math.floor(tokens)
}
抽象基类
避免代码重复。
package com.xxx.gateway.ratelimit;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.List;
@RequiredArgsConstructor
public abstract class AbstractRateLimiter {
protected final ReactiveStringRedisTemplate redisTemplate;
private RedisScript<List> script;
protected RedisScript<List> getScript() {
if (script != null) {
return script;
}
try {
String lua =
new String(
new ClassPathResource(
"lua/token_bucket.lua"
)
.getInputStream()
.readAllBytes(),
StandardCharsets.UTF_8
);
script =
RedisScript.of(
lua,
List.class
);
return script;
} catch (Exception e) {
throw new RuntimeException(
"load lua fail",
e
);
}
}
protected Mono<RateLimitResult> execute(
String key,
long capacity,
long rate) {
long now =
System.currentTimeMillis();
return redisTemplate.execute(
getScript(),
List.of(key),
String.valueOf(capacity),
String.valueOf(rate),
String.valueOf(now)
)
.next()
.map(result -> {
Long allowed =
Long.valueOf(
result.get(0)
.toString()
);
Long remain =
Long.valueOf(
result.get(1)
.toString()
);
return new RateLimitResult(
allowed == 1,
remain
);
})
.onErrorResume(e -> {
/*
* Redis故障降级
*
* 放行
*/
return Mono.just(
new RateLimitResult(
true,
-1
)
);
});
}
}
UserRateLimiter
按用户限流。
适合:
下单
支付
短信发送
package com.xxx.gateway.ratelimit;
import com.xxx.gateway.component.UserContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Slf4j
@Component
public class UserRateLimiter
extends AbstractRateLimiter {
private static final String PREFIX =
"gateway:user:rate:";
/**
* 每秒20个请求
*/
private static final long RATE = 20;
/**
* 最大容量100
*/
private static final long CAPACITY = 100;
public UserRateLimiter(
ReactiveStringRedisTemplate redisTemplate) {
super(redisTemplate);
}
public Mono<RateLimitResult> isAllowed(
UserContext userContext) {
if (userContext == null) {
return Mono.just(
new RateLimitResult(
true,
-1
)
);
}
String key =
PREFIX
+ userContext.getUserId();
return execute(
key,
CAPACITY,
RATE
);
}
}
IpRateLimiter
按IP限流。
适合:
登录
注册
验证码
package com.xxx.gateway.ratelimit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Slf4j
@Component
public class IpRateLimiter
extends AbstractRateLimiter {
private static final String PREFIX =
"gateway:ip:rate:";
/**
* 每秒10个
*/
private static final long RATE = 10;
/**
* 最大容量50
*/
private static final long CAPACITY = 50;
public IpRateLimiter(
ReactiveStringRedisTemplate redisTemplate) {
super(redisTemplate);
}
public Mono<RateLimitResult> isAllowed(
String ip) {
String key =
PREFIX + ip;
return execute(
key,
CAPACITY,
RATE
);
}
}
推荐增加统一过滤器
后面你应该再加:
RateLimitFilter
顺序:
TraceFilter
↓
IpBlacklistFilter
↓
ReplayAttackFilter
↓
SignatureVerifyFilter
↓
JwtAuthenticationFilter
↓
RbacAuthorizationFilter
↓
RateLimitFilter
↓
AccessLogFilter
逻辑:
IP限流
↓
用户限流
↓
通过
即:
ipLimiter.isAllowed(ip)
↓
userLimiter.isAllowed(user)
↓
chain.filter()
生产环境还建议再升级
目前这一版已经能上线。
但大流量场景(10万+QPS)建议把:
gateway:user:rate:10001
gateway:ip:rate:1.1.1.1
升级为:
gateway:rate:user:{10001}
gateway:rate:ip:{1.1.1.1}
利用 Redis Cluster 的 Hash Tag,保证同一个用户的限流数据始终落在同一个 Slot,避免跨 Slot 问题。
一个关键点:
GlobalExceptionHandler 对 Spring Cloud Gateway 的 GlobalFilter 里的异常并不能完全兜住。
很多异常发生在:
GlobalFilter
↓
Netty
↓
Gateway Routing
这时候应该配合:
ErrorWebExceptionHandler
而不是单纯:
@RestControllerAdvice
GatewayFallbackController
用于:
filters:
- name: CircuitBreaker
args:
name: user-service
fallbackUri: forward:/fallback/user
package com.xxx.gateway.fallback;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/fallback")
public class GatewayFallbackController {
@GetMapping("/{service}")
public Mono<Map<String, Object>> fallback(
@PathVariable String service) {
log.error("service fallback {}", service);
Map<String, Object> result =
new HashMap<>();
result.put("code", 503);
result.put("msg", service + " unavailable");
result.put("time", LocalDateTime.now());
return Mono.just(result);
}
}
GlobalExceptionHandler
生产版。
不要用:
@ControllerAdvice
而是:
ErrorWebExceptionHandler
package com.xxx.gateway.exception;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
@Order(-1)
@RequiredArgsConstructor
public class GlobalExceptionHandler
implements ErrorWebExceptionHandler {
private final ObjectMapper objectMapper;
@Override
public Mono<Void> handle(
ServerWebExchange exchange,
Throwable ex) {
log.error(
"gateway exception",
ex
);
HttpStatus status =
HttpStatus.INTERNAL_SERVER_ERROR;
Map<String, Object> body =
new HashMap<>();
body.put("code", status.value());
body.put("msg", ex.getMessage());
body.put("time", LocalDateTime.now());
try {
byte[] bytes =
objectMapper.writeValueAsBytes(
body
);
exchange.getResponse()
.setStatusCode(status);
exchange.getResponse()
.getHeaders()
.setContentType(
MediaType.APPLICATION_JSON
);
DataBuffer buffer =
exchange.getResponse()
.bufferFactory()
.wrap(bytes);
return exchange
.getResponse()
.writeWith(
Mono.just(buffer)
);
} catch (Exception e) {
return Mono.error(e);
}
}
}
GatewayMetricsCollector
这里直接接 Micrometer。
配合前面你的:
MetricsConfig
package com.xxx.gateway.metrics;
import io.micrometer.core.instrument.*;
import lombok.Getter;
import org.springframework.stereotype.Component;
@Component
@Getter
public class GatewayMetricsCollector {
private final Counter requestCounter;
private final Counter authFailCounter;
private final Counter replayCounter;
private final Counter rateLimitCounter;
private final Counter blacklistCounter;
private final Timer requestTimer;
public GatewayMetricsCollector(
MeterRegistry registry) {
this.requestCounter =
registry.counter(
"gateway_request_total"
);
this.authFailCounter =
registry.counter(
"gateway_auth_fail_total"
);
this.replayCounter =
registry.counter(
"gateway_replay_total"
);
this.rateLimitCounter =
registry.counter(
"gateway_rate_limit_total"
);
this.blacklistCounter =
registry.counter(
"gateway_blacklist_total"
);
this.requestTimer =
registry.timer(
"gateway_request_duration"
);
}
}
使用示例
例如:
private final GatewayMetricsCollector metrics;
认证失败:
metrics.getAuthFailCounter()
.increment();
限流:
metrics.getRateLimitCounter()
.increment();
请求:
metrics.getRequestCounter()
.increment();
GrayRoutePredicateFactory
生产灰度发布。
支持:
X-Version: gray
或者:
X-Version: v2
配置
spring:
cloud:
gateway:
routes:
- id: user-gray
uri: lb://user-service-gray
predicates:
- Gray=v2
实现
package com.xxx.gateway.route;
import lombok.Data;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.function.Predicate;
@Component
public class GrayRoutePredicateFactory
extends AbstractRoutePredicateFactory<
GrayRoutePredicateFactory.Config> {
public GrayRoutePredicateFactory() {
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return List.of("version");
}
@Override
public Predicate<ServerHttpRequest> apply(
Config config) {
return request -> {
String version =
request.getHeaders()
.getFirst(
"X-Version"
);
return config.getVersion()
.equalsIgnoreCase(version);
};
}
@Data
public static class Config {
private String version;
}
}
生产增强版灰度
实际上大厂不会只做:
X-Version=v2
而是:
用户ID
手机号
租户
城市
百分比
白名单
例如:
userId % 100 < 10
实现:
10%
灰度用户
这种才是真正生产级。
更多推荐





所有评论(0)