一、没有网关的日子我们是怎么过的
2018年,我们的微服务直接暴露给前端。前端要记10个不同的域名和端口。
更痛苦的是,每个服务各自实现鉴权、限流、日志,代码重复度超过60%。
有一次安全审计,发现3个服务没有做鉴权,2个服务没有做限流。
后来我们上了网关,统一入口、统一鉴权、统一限流,世界瞬间清净了。
二、网关核心功能
2.1 功能清单
┌─────────────────────────────────────────────────────────────────┐
│ 网关核心功能 │
│ │
│ 1. 路由转发 │
│ - 根据路径/域名/Header转发到后端服务 │
│ │
│ 2. 鉴权认证 │
│ - JWT验证、OAuth2认证 │
│ - 统一登录、单点登录 │
│ │
│ 3. 限流熔断 │
│ - 全局限流、按用户限流 │
│ - 服务熔断、降级 │
│ │
│ 4. 协议转换 │
│ - HTTP → gRPC │
│ - HTTP → WebSocket │
│ │
│ 5. 日志监控 │
│ - 请求日志、响应日志 │
│ - 链路追踪 │
│ │
│ 6. 灰度发布 │
│ - 按比例/按用户灰度路由 │
│ │
└──────────────────────────────────────────────────────────────────┘
三、Spring Cloud Gateway实现
3.1 路由配置
yaml
# application.yml
spring:
cloud:
gateway:
routes:
# 订单服务
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
key-resolver: "#{@userKeyResolver}"
# 商品服务
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/products/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
name: productCircuitBreaker
fallbackUri: forward:/fallback/product
# 支付服务(灰度)
- id: payment-service-v2
uri: lb://payment-service-v2
predicates:
- Path=/api/payments/**
- Header=X-Gray, v2
filters:
- StripPrefix=1
3.2 鉴权过滤器
java
/**
* JWT鉴权过滤器
*/
@Component
@Slf4j
public class JwtAuthFilter implements GlobalFilter, Ordered {
@Autowired
private JwtTokenProvider tokenProvider;
/** 白名单路径 */
private static final Set<String> WHITE_LIST = Set.of(
"/api/auth/login",
"/api/auth/register",
"/api/auth/refresh",
"/api/public/**"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
// 白名单放行
if (isWhiteListed(path)) {
return chain.filter(exchange);
}
// 获取Token
String token = extractToken(exchange.getRequest());
if (token == null) {
return unauthorized(exchange, "缺少认证Token");
}
try {
// 验证Token
JwtClaims claims = tokenProvider.validateToken(token);
// 将用户信息传递给下游服务
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-User-Id", claims.getUserId())
.header("X-User-Role", claims.getRole())
.header("X-Trace-Id", generateTraceId())
.build();
return chain.filter(exchange.mutate().request(request).build());
} catch (JwtTokenExpiredException e) {
return unauthorized(exchange, "Token已过期");
} catch (JwtTokenInvalidException e) {
return unauthorized(exchange, "Token无效");
}
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = JSON.toJSONString(Result.fail(401, message));
DataBuffer buffer = exchange.getResponse().bufferFactory()
.wrap(body.getBytes(StandardCharsets.UTF_8));
return exchange.getResponse().writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return -100; // 高优先级
}
}
3.3 限流过滤器
java
/**
* 自定义限流Key解析器
*/
@Component
public class UserKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
String path = exchange.getRequest().getPath().value();
if (userId != null) {
return Mono.just("rate_limit:" + userId + ":" + path);
}
// 未登录用户按IP限流
String ip = exchange.getRequest().getRemoteAddress()
.getAddress().getHostAddress();
return Mono.just("rate_limit:ip:" + ip + ":" + path);
}
}
3.4 灰度路由
java
/**
* 灰度路由过滤器
*/
@Component
@Slf4j
public class GrayRouteFilter implements GlobalFilter, Ordered {
@Autowired
private GrayConfigService grayConfigService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
String serviceId = getServiceId(exchange);
// 检查灰度规则
GrayRule rule = grayConfigService.getGrayRule(serviceId);
if (rule != null && rule.isEnabled()) {
boolean isGrayUser = isGrayUser(userId, rule);
if (isGrayUser) {
// 灰度用户路由到V2版本
String newUri = rewriteUri(exchange.getRequest().getURI(),
rule.getGrayVersion());
ServerHttpRequest request = exchange.getRequest().mutate()
.uri(URI.create(newUri))
.header("X-Gray", "true")
.build();
log.info("灰度路由: userId={}, service={}, version={}",
userId, serviceId, rule.getGrayVersion());
return chain.filter(exchange.mutate().request(request).build());
}
}
return chain.filter(exchange);
}
private boolean isGrayUser(String userId, GrayRule rule) {
// 按用户ID范围灰度
if (rule.getUserIdRange() != null) {
return rule.getUserIdRange().contains(Long.parseLong(userId));
}
// 按百分比灰度
if (rule.getPercentage() > 0) {
int hash = Math.abs(userId.hashCode());
return hash % 100 < rule.getPercentage();
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
四、踩坑实录
坑1:网关成为单点
网关挂了,所有服务都不可用。
解决:网关多实例部署 + 健康检查 + 自动扩容。
坑2:网关性能瓶颈
所有请求经过网关,QPS高时网关响应慢。
解决:网关只做轻量操作(路由、鉴权),重逻辑放在业务服务。
坑3:网关超时设置不合理
网关超时30秒,但有些导出接口需要60秒。
解决:不同路由设置不同超时,长连接接口特殊处理。
坑4:跨域配置遗漏
前端请求被CORS策略拦截。
解决:在网关统一配置CORS。
坑5:请求体大小限制
文件上传请求被网关拒绝,因为超过了默认请求体大小限制。
解决 :调整spring.codec.max-in-memory-size配置。
五、总结
网关设计要点:
| 功能 | 方案 |
|---|---|
| 路由 | Spring Cloud Gateway |
| 鉴权 | JWT + GlobalFilter |
| 限流 | Redis + RequestRateLimiter |
| 熔断 | Resilience4J |
| 灰度 | 自定义路由规则 |
| 监控 | Actuator + Prometheus |
最佳实践:
- 网关多实例部署
- 只做轻量操作
- 统一鉴权和限流
- 合理的超时配置
- 完善的监控告警
血的教训:
网关是微服务的大门。门没守好,再多的内部安全措施也白搭。
思考题: 你的系统用了什么网关方案?有没有踩过坑?
个人观点,仅供参考