生产级 Spring Boot 网关完整实现方案

生产级 Spring Boot 网关简单实现方案-CSDN博客

文章中存在不足。

如果这是Demo项目,我会给 85分。

如果这是准备上线承载真实流量的生产网关,我会给 65分,因为核心骨架有了,但还有不少生产级缺失点。

我按照真实互联网网关(阿里、字节、美团常见实践)帮你挑一下问题。

一、最大的问题:JWT校验位置不对

你现在:

validateToken()

validateSign()

转发

实际上应该:

限流

签名校验

JWT校验

权限校验

转发

原因:

JWT解析是CPU操作。

如果攻击者疯狂发垃圾请求:

Authorization: xxxxxxx

你的网关会先解析JWT。

CPU直接被打爆。

签名校验成本更低。

应该先挡掉非法请求。

二、缺少权限控制(RBAC)

你目前只做了:

header("X-User-Roles", roles)

但没校验。

例如:

java 复制代码
/admin/**

应该:

java 复制代码
ADMIN

才能访问。

生产环境一般增加:

java 复制代码
RoleAuthorizationFilter

例如:

java 复制代码
/api/admin/**

需要:

java 复制代码
ADMIN

三、缺少IP黑名单

现在只有:

java 复制代码
RateLimiter

但没有:

java 复制代码
BlackList

生产上通常:

Redis

java 复制代码
ip:blacklist:1.1.1.1

过滤器最前面:

java 复制代码
if (blacklist.contains(ip))

直接:

java 复制代码
403

四、缺少动态限流

现在:

java 复制代码
20
10

写死。

生产一定会动态配置。

例如:

XML 复制代码
/api/login/**
100/s
/api/order/**
20/s
/api/pay/**
5/s

一般放:

* Redis

* Nacos

* Apollo

动态下发。

五、缺少用户维度限流

你只有:

java 复制代码
IP限流

问题:

公司出口IP

java 复制代码
1000个人
同一个IP

直接被限死。

通常:

java 复制代码
userId + ip

双限流。

例如:

java 复制代码
rate:user:10001
rate:ip:1.1.1.1

同时校验。

六、防重放基本没实现

这里:

java 复制代码
checkReplay()

是假的。

生产真正实现:

Header:

java 复制代码
X-Nonce
X-Timestamp
X-Sign

流程:

java 复制代码
1 校验时间窗口
当前时间 - timestamp < 300s
2 查询Redis
nonce存在
直接拒绝
3 不存在
SETNX
过期5分钟

Redis:

java 复制代码
nonce:xxxxxx

七、签名校验不完整

你自己也注释了:

java 复制代码
只校验Query参数

问题很严重。

POST:

java 复制代码
{
  "amount":100
}

攻击者改成:

java 复制代码
{
  "amount":1
}

签名还能通过。

生产:

java 复制代码
query
+
body
+
timestamp
+
nonce

一起参与签名。

八、Body缓存缺失

WebFlux读取Body:

java 复制代码
request.getBody()

只能读一次。

你的签名要读取Body:

java 复制代码
validateSign()

下游还要读Body。

必须:

java 复制代码
CachedBodyOutputMessage
ServerHttpRequestDecorator

缓存请求体。

这是Spring Gateway生产开发最容易踩坑的地方之一。

九、TraceId生成方式不推荐

现在:

java 复制代码
UUID.randomUUID()

生产一般:

java 复制代码
MDC

结合:

java 复制代码
SkyWalking
Zipkin
OpenTelemetry

例如:

java 复制代码
traceId
spanId

形成完整调用链。

推荐:

OpenTelemetry

十、日志不够

现在:

java 复制代码
path
status
cost
ip

生产还需要:

java 复制代码
userId
clientId
traceId
requestSize
responseSize
userAgent
referer

特别是:

java 复制代码
requestSize
responseSize

排查问题非常重要。

十一、缺少熔断降级

真实生产一定有:

java 复制代码
CircuitBreaker

例如:

下游:

java 复制代码
user-service

挂了。

应该:

java 复制代码
fallback

而不是一直超时。

Spring Gateway直接支持:

Resilience4j

例如:

java 复制代码
filters:
  - name: CircuitBreaker
    args:
      name: user-service
      fallbackUri: forward:/fallback

十二、缺少超时控制

生产必须配置:

java 复制代码
spring:
  cloud:
    gateway:
      httpclient:
        connect-timeout: 3000
        response-timeout: 5s

否则:

下游卡死。

网关线程全部堆积。

十三、JWT密钥管理有问题

现在:

java 复制代码
private static final String SECRET

生产绝对不能这样。

应该:

java 复制代码
KMS

或者:

java 复制代码
环境变量

或者:

java 复制代码
Nacos加密配置

否则:

代码泄露=全站失陷。

十四、缺少接口灰度能力

生产网关很重要的一项:

java 复制代码
Canary

例如:

java 复制代码
X-Version=v2

路由到:

java 复制代码
user-service-v2

普通用户:

java 复制代码
user-service-v1

Spring Gateway非常适合做这个。

十五、缺少网关监控指标

你引入了:

java 复制代码
Actuator

但没真正统计:

java 复制代码
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过滤

java 复制代码
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

java 复制代码
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。

java 复制代码
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)
        );
    }
}

对应配置:

java 复制代码
gateway:
  jwt:
    secret: >
      dGhpc19pc19hX3Byb2R1Y3Rpb25fan
      d0X3NlY3JldF9rZXlfZm9yX2dhdGV3
      YXlfMjAyNl8wNl8wOA==

不要写:

java 复制代码
secret: abc123

生产禁止。

MetricsConfig.java

生产必须接Prometheus。

Maven:

XML 复制代码
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

配置类:

java 复制代码
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(配套生产配置)

java 复制代码
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

最先执行。

java 复制代码
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结构:

java 复制代码
gateway:blacklist:ip:1.1.1.1

value:

java 复制代码
1
java 复制代码
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 失败拒绝

java 复制代码
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

前面那版代码里有个生产隐患:

java 复制代码
MDC.put(...)

在 WebFlux 里并不可靠。

后面我给你的 AccessLogFilter 会优先从 exchange.getAttribute() 获取 TraceId,而不是直接依赖 MDC。

JwtAuthenticationFilter

职责:

* 校验JWT

* 解析用户信息

* 写入UserContext

* Header透传下游

Order:

java 复制代码
-700
java 复制代码
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:

java 复制代码
-800

在 JWT 前执行。

java 复制代码
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权限控制

例如:

java 复制代码
gateway:
  auth:
    rules:
      - path: /admin/**
        roles:
          - ADMIN
      - path: /order/**
        roles:
          - USER
          - ADMIN
java 复制代码
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

最后执行。

java 复制代码
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

java 复制代码
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 管理器。

java 复制代码
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

原子执行。

java 复制代码
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

java 复制代码
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);
    }
}

配套配置

java 复制代码
gateway:
  sign:
    secret: your-sign-secret
  jwt:
    secret: xxxxxxxxxxxxxxxxxxxxxxxxx

不建议用你前面那套自己拼的 RedisTemplate.execute(script -> script.eval(...)) 方式。

生产上建议:

Redis

Lua

ReactiveRedisTemplate

令牌桶(Token Bucket)

支持:

* 集群部署

* 多实例共享限流状态

* 用户限流

* IP限流

* Redis异常降级

先定义公共返回对象

java 复制代码
package com.xxx.gateway.ratelimit;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class RateLimitResult {
    /**
     * 是否允许
     */
    private boolean allowed;
    /**
     * 剩余令牌
     */
    private long remainTokens;
}

公共Lua脚本

建议放:

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)
}

抽象基类

避免代码重复。

java 复制代码
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

按用户限流。

适合:

下单

支付

短信发送

java 复制代码
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限流。

适合:

登录

注册

验证码

java 复制代码
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

逻辑:

java 复制代码
IP限流
↓
用户限流
↓
通过

即:

java 复制代码
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

这时候应该配合:

java 复制代码
ErrorWebExceptionHandler

而不是单纯:

java 复制代码
@RestControllerAdvice

GatewayFallbackController

用于:

java 复制代码
filters:
  - name: CircuitBreaker
    args:
      name: user-service
      fallbackUri: forward:/fallback/user
java 复制代码
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

生产版。

不要用:

java 复制代码
@ControllerAdvice

而是:

java 复制代码
ErrorWebExceptionHandler
java 复制代码
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。

配合前面你的:

java 复制代码
MetricsConfig
java 复制代码
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"
                );
    }
}

使用示例

例如:

java 复制代码
private final GatewayMetricsCollector metrics;

认证失败:

java 复制代码
metrics.getAuthFailCounter()
       .increment();

限流:

java 复制代码
metrics.getRateLimitCounter()
       .increment();

请求:

java 复制代码
metrics.getRequestCounter()
       .increment();

GrayRoutePredicateFactory

生产灰度发布。

支持:

java 复制代码
X-Version: gray

或者:

java 复制代码
X-Version: v2

配置

java 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: user-gray
          uri: lb://user-service-gray
          predicates:
            - Gray=v2

实现

java 复制代码
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;
    }
}

生产增强版灰度

实际上大厂不会只做:

java 复制代码
X-Version=v2

而是:

用户ID

手机号

租户

城市

百分比

白名单

例如:

java 复制代码
userId % 100 < 10

实现:

10%

灰度用户

这种才是真正生产级。

相关推荐
JAVA面经实录9171 小时前
Spring Cloud Alibaba 微服务企业实战完整文档(架构+规范+调优+故障+源码)
java·运维·spring cloud·微服务
布局呆星1 小时前
Spring Boot + JWT + Spring Security 认证授权实战:双角色、双 Token、方法级权限,一次讲透
java·开发语言
LucianaiB1 小时前
Swarm管理面板的多项目配置策略与模型别名机制的效率分析
java·服务器·前端
qq_2518364571 小时前
基于Spring Boot的数据标注与质检系统设计与实现
java·spring boot·后端
總鑽風1 小时前
Spring AI实战:快速集成阿里通义千问
java·后端·spring·ai编程
searchforAI1 小时前
利用AI翻译视频做双语笔记,一套视频翻译到知识库沉淀的完整方案
人工智能·笔记·gpt·音视频·语音识别·知识图谱·机器翻译
一条泥憨鱼1 小时前
苍穹外卖【day3|菜品管理】
java·数据库·sql·mysql·mybatis
Wenzar_1 小时前
Playwright 实战:高可信 UI 回归验证流水线
java·ui
livemetee2 小时前
Java 25虚拟线程使用实例
java