一、没有网关的日子我们是怎么过的
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 路由配置
# application.ymlspring:cloud:gateway:routes:# 订单服务-id:order-serviceuri:lb://order-servicepredicates:-Path=/api/orders/**filters:-StripPrefix=1-name:RequestRateLimiterargs:redis-rate-limiter.replenishRate:100redis-rate-limiter.burstCapacity:200key-resolver:"#{@userKeyResolver}"# 商品服务-id:product-serviceuri:lb://product-servicepredicates:-Path=/api/products/**filters:-StripPrefix=1-name:CircuitBreakerargs:name:productCircuitBreakerfallbackUri:forward:/fallback/product# 支付服务(灰度)-id:payment-service-v2uri:lb://payment-service-v2predicates:-Path=/api/payments/**-Header=X-Gray,v2filters:-StripPrefix=13.2 鉴权过滤器
/** * JWT鉴权过滤器 */@Component@Slf4jpublicclassJwtAuthFilterimplementsGlobalFilter,Ordered{@AutowiredprivateJwtTokenProvidertokenProvider;/** 白名单路径 */privatestaticfinalSet<String>WHITE_LIST=Set.of("/api/auth/login","/api/auth/register","/api/auth/refresh","/api/public/**");@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){Stringpath=exchange.getRequest().getPath().value();// 白名单放行if(isWhiteListed(path)){returnchain.filter(exchange);}// 获取TokenStringtoken=extractToken(exchange.getRequest());if(token==null){returnunauthorized(exchange,"缺少认证Token");}try{// 验证TokenJwtClaimsclaims=tokenProvider.validateToken(token);// 将用户信息传递给下游服务ServerHttpRequestrequest=exchange.getRequest().mutate().header("X-User-Id",claims.getUserId()).header("X-User-Role",claims.getRole()).header("X-Trace-Id",generateTraceId()).build();returnchain.filter(exchange.mutate().request(request).build());}catch(JwtTokenExpiredExceptione){returnunauthorized(exchange,"Token已过期");}catch(JwtTokenInvalidExceptione){returnunauthorized(exchange,"Token无效");}}privateMono<Void>unauthorized(ServerWebExchangeexchange,Stringmessage){exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);Stringbody=JSON.toJSONString(Result.fail(401,message));DataBufferbuffer=exchange.getResponse().bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));returnexchange.getResponse().writeWith(Mono.just(buffer));}@OverridepublicintgetOrder(){return-100;// 高优先级}}3.3 限流过滤器
/** * 自定义限流Key解析器 */@ComponentpublicclassUserKeyResolverimplementsKeyResolver{@OverridepublicMono<String>resolve(ServerWebExchangeexchange){StringuserId=exchange.getRequest().getHeaders().getFirst("X-User-Id");Stringpath=exchange.getRequest().getPath().value();if(userId!=null){returnMono.just("rate_limit:"+userId+":"+path);}// 未登录用户按IP限流Stringip=exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();returnMono.just("rate_limit:ip:"+ip+":"+path);}}3.4 灰度路由
/** * 灰度路由过滤器 */@Component@Slf4jpublicclassGrayRouteFilterimplementsGlobalFilter,Ordered{@AutowiredprivateGrayConfigServicegrayConfigService;@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){StringuserId=exchange.getRequest().getHeaders().getFirst("X-User-Id");StringserviceId=getServiceId(exchange);// 检查灰度规则GrayRulerule=grayConfigService.getGrayRule(serviceId);if(rule!=null&&rule.isEnabled()){booleanisGrayUser=isGrayUser(userId,rule);if(isGrayUser){// 灰度用户路由到V2版本StringnewUri=rewriteUri(exchange.getRequest().getURI(),rule.getGrayVersion());ServerHttpRequestrequest=exchange.getRequest().mutate().uri(URI.create(newUri)).header("X-Gray","true").build();log.info("灰度路由: userId={}, service={}, version={}",userId,serviceId,rule.getGrayVersion());returnchain.filter(exchange.mutate().request(request).build());}}returnchain.filter(exchange);}privatebooleanisGrayUser(StringuserId,GrayRulerule){// 按用户ID范围灰度if(rule.getUserIdRange()!=null){returnrule.getUserIdRange().contains(Long.parseLong(userId));}// 按百分比灰度if(rule.getPercentage()>0){inthash=Math.abs(userId.hashCode());returnhash%100<rule.getPercentage();}returnfalse;}@OverridepublicintgetOrder(){return0;}}四、踩坑实录
坑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 |
最佳实践:
- 网关多实例部署
- 只做轻量操作
- 统一鉴权和限流
- 合理的超时配置
- 完善的监控告警
血的教训:
网关是微服务的大门。门没守好,再多的内部安全措施也白搭。
思考题:你的系统用了什么网关方案?有没有踩过坑?
个人观点,仅供参考