校门口文具店无限钞票
63.35MB · 2025-12-12
别被 “架构名词” 吓到,网关其实就是个 “中间商”,干 3 件接地气的事:
简单说:网关 = 小区保安 + 快递中转站 + 公司行政,让微服务们专心 “搞业务”。
市面上的网关不少,别瞎跟风,看场景选:
| 方案 | 特点 | 适合场景 |
|---|---|---|
| Kong/APISIX | 基于 Nginx,性能强,运维友好 | 多语言架构、高并发场景 |
| Spring Cloud Gateway | 纯 Java,和 Spring 生态无缝衔接 | Java 后端主导的微服务 |
| Zuul | 老牌网关,性能一般 | 老项目维护(新项目别用) |
咱们后端搬砖人,大多是 Spring 生态,所以重点聊 Spring Cloud Gateway(以下简称 Gateway)。
老玩家可能记得 Zuul,但现在基本被 Gateway 取代,原因很真实:
总结:选 Gateway,就是选 “不加班”—— 毕竟谁也不想因为 Zuul 的性能问题,半夜起来改代码。
光说不练假把式,咱们用 Spring Boot + Gateway + Nacos(注册中心)搭一个,步骤超简单:
注意:Gateway 不能加spring-boot-starter-web依赖,会冲突!
<dependencies>
<!-- Gateway核心依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 注册中心(Nacos为例,Eureka/Consul也一样) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
核心是 “路由规则”:用户访问啥路径,转发到哪个服务?
spring:
application:
name: gateway-service # 网关服务名
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # Nacos地址
gateway:
routes:
# 路由1:转发用户服务
- id: user-service-route # 唯一标识(随便起)
uri: lb://user-service # 转发到Nacos中的user-service(lb=负载均衡)
predicates: # 匹配规则(断言)
- Path=/api/user/** # 只要访问/api/user/开头的路径,就走这个路由
filters: # 过滤规则
- RewritePath=/api/user/(?<segment>.*), /user/${segment} # 路径重写:/api/user/1 → /user/1
# 路由2:转发订单服务(同理)
- id: order-service-route
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- RewritePath=/api/order/(?<segment>.*), /order/${segment}
就加个@EnableDiscoveryClient,没啥花活:
@SpringBootApplication
@EnableDiscoveryClient // 注册到Nacos
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
启动后,访问http://lo***calhost:8080/api/user/1,会自动转发到user-service的/user/1接口 —— 成了!
刚才配置里的predicates就是 “断言”,简单说:满足条件的请求,才走这个路由。
常用断言整理好了,直接抄:
| 断言类型 | 例子 | 意思 |
|---|---|---|
| Path(路径) | Path=/api/user/** | 匹配 /api/user/ 开头的路径 |
| After(时间) | After=2024-05-01T00:00:00+08:00[Asia/Shanghai] | 2024 年 5 月 1 日后才生效 |
| Query(参数) | Query=token | 请求必须带 token 参数(如?token=xxx) |
| RemoteAddr(IP) | RemoteAddr=192.168.1.0/24 | 只允许 192.168.1.x 网段的请求 |
可以多个断言组合,比如:Path=/api/user/** && Query=token—— 既匹配路径,又得带 token 参数。
过滤分两种:内置过滤器(现成的)和自定义过滤器(自己写),都是为了处理请求。
Gateway 自带很多过滤器,不用写代码,配置就行:
| 过滤器名 | 例子 | 作用 |
|---|---|---|
| AddRequestHeader | AddRequestHeader=X-User-Id, 123 | 给请求加个 X-User-Id 头,值为 123 |
| SetStatus | SetStatus=404 | 把响应状态码改成 404 |
| RewritePath | 前面实战用过,路径重写 | 隐藏真实接口路径 |
| RequestRateLimiter | 限流用(后续单独讲) | 防止接口被刷爆 |
如图所示:
HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为**Filter**)。Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为pre和post两部分,分别会在请求路由到微服务之前和之后被执行。Filter的pre逻辑都依次顺序执行通过后,请求才会被路由到微服务。Filter的post逻辑。在网关过滤器链中,有两种过滤器:
GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route.GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。通常用于自定义过滤器实现拦截功能的过滤器分两类,别搞混:
比如给 “用户服务路由” 加个过滤器,打印请求日志:
// 1. 写过滤器
@Component
public class LogGatewayFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 打印请求路径
System.out.println("请求路径:" + exchange.getRequest().getPath());
// 继续往下走(不放行就卡这了)
return chain.filter(exchange);
}
// 过滤器顺序(数字越小越先执行)
@Override
public int getOrder() {
return 0;
}
}
// 2. 配置路由时引用
spring:
cloud:
gateway:
routes:
- id: user-service-route
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- name: LogGatewayFilter # 引用自定义的过滤器
比如全局打印请求时间,不用在每个路由配置:
@Component
public class GlobalLogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
long start = System.currentTimeMillis();
// 执行完后续逻辑后,打印耗时
return chain.filter(exchange)
.doFinally(signal -> {
long end = System.currentTimeMillis();
System.out.println("请求耗时:" + (end - start) + "ms");
});
}
@Override
public int getOrder() {
return -1; // 全局过滤器,先执行
}
}
最核心的场景来了:所有请求必须带合法 token,否则不让进。
为啥在网关鉴权?—— 总不能每个服务都写一遍 token 验证吧?网关统一拦,效率高!
用 JWT 为例(无状态,适合微服务):
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
// 假设这是JWT工具类(实际项目用现成的,比如jjwt)
private final JwtUtil jwtUtil;
@Autowired
public AuthGlobalFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 跳过白名单(比如登录接口,不用鉴权)
String path = exchange.getRequest().getPath().value();
if ("/api/auth/login".equals(path)) {
return chain.filter(exchange);
}
// 2. 拿token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
return handleUnauthorized(exchange); // 没token,返回401
}
token = token.substring(7); // 去掉"Bearer "前缀
// 3. 验证token
try {
Claims claims = jwtUtil.parseToken(token); // JWT解密
// 把用户信息放入上下文,后续服务可以直接拿
exchange.getAttributes().put("userId", claims.get("userId"));
} catch (Exception e) {
return handleUnauthorized(exchange); // token无效,返回401
}
// 4. 放行
return chain.filter(exchange);
}
// 处理未授权:返回401
private Mono<Void> handleUnauthorized(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String json = "{"code":401,"msg":"未授权,请先登录"}";
DataBuffer buffer = response.bufferFactory().wrap(json.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return -2; // 比全局日志过滤器先执行
}
}
这样一来,所有非白名单请求,都会先过鉴权 —— 搞定!
最后问一句:你在网关实战中遇到过啥奇葩问题?比如动态路由不生效、鉴权丢头?评论区聊聊,一起避坑~