先说个我最近面试遇到的真实场景。候选人简历上写着“熟悉微服务架构,做过网关层开发”,我问了一个很基础的问题:“网关里的过滤器和微服务里的 Interceptor,你平时是怎么区分使用的?” 对方沉默了几秒,然后说:“其实我觉得它们差不多,都是为了做鉴权。” 我追问:“那如果把登录校验写在网关里,跟写在服务内的 Interceptor 里,效果有什么不一样?” 他还是支支吾吾。
这不是个别现象。Java 后端做到微服务阶段,网关和 Interceptor 是两件绕不开的事,也是最容易被混淆的两个概念。先提醒一句:网上很多资料里说的“网关”可能是家里用的智能网关、边缘网关设备,但本文语境下的网关,特指微服务架构里的 API 网关——所有请求抵达后端服务集群之前的那道“总闸门”。
很多人项目跑得起来,代码能上线,但真把“网关管理原理”和“微服务 Interceptor 区分”拿出来讲,就露馅了。这篇文章我准备用一整篇的篇幅,把两件事彻底讲透:第一,网关到底管什么、怎么管;第二,微服务内部到底有哪几种 Interceptor,各自分工是什么。最后给一张清晰的对照表,再加上我在实际项目中总结的使用边界和排障经验。适合准备跳槽的 Java 开发,也适合正在搭微服务架构、想理顺流量入口和业务拦截层级的团队参考。
1. 先理清一个常见误区:网关和拦截器到底是不是一回事
1.1 一句话定位两者的边界
先给一个粗暴但准确的结论:网关是全局性的流量总闸,Interceptor 是局域性的业务关卡。
网关管的是“请求进不进入整个微服务集群”,Interceptor 管的是“请求到了某个服务内部之后,要不要继续往下走”。一个在系统最外层,一个在业务代码附近,两者的网络位置、代码层级、生命周期都不一样,只是“都能拦截请求”这个表象让很多人混为一谈。
我在技术群里见过不少类似的讨论,有人说“我直接用 Interceptor 实现登录校验就行了,还要网关干嘛”,也有人说“既然有网关统一鉴权,服务内部就完全不用写拦截器了”。这两种说法都只对了一半。网关和服务内拦截器解决的是不同粒度的问题,后面我会详细展开。
1.2 为什么这个点在面试中高频出现
说白了,面试官不是在考你背没背过概念,而是在考你有没有真正从“全局”和“局部”两个视角看一个请求的完整生命周期。
如果你能清楚说出:请求先经过网关层的 GlobalFilter/GatewayFilter 做路由分发和横切处理,再通过负载均衡到达某个微服务实例,接着经过 Servlet Filter、Spring MVC 的 HandlerInterceptor,最后才到 Controller 和 AOP 切面——这就说明你确实理解微服务调用链。反之,如果只会说“反正都能拦截请求”,面试官基本就给你定性为“项目是照着文档搭的,没有深入思考”。
热门面试题里“微服务网关的作用”“Spring 拦截器和过滤器区别”“AOP 和 Interceptor 区别”基本都是从这个链路里拆出来的。把这一条链搞明白,等于一次准备了三道题。
1.3 微服务架构图里网关的实际物理位置
打开任何一张微服务架构图——不管是 Nacos 官网的示例还是 Spring Cloud Alibaba 的典型架构——网关永远画在最上方,下面是注册中心、配置中心,再下面才是各个微服务实例。
网关和微服务之间是“上游”与“下游”的关系。客户端请求只认网关的地址,通过 HTTP/HTTPS 把请求打到网关上,网关拿到请求后做三件事:一是判断要不要放行,二是根据路由规则找到下游哪个服务来处理,三是把请求转发过去并把响应带回来。整个过程中,微服务本身并不知道客户端长什么样,它只认识网关转过来的请求。
这个位置决定了网关天然拥有全局视野:所有流量都经过它,它就能做全局限流、全局鉴权、全局开关、灰度路由。而服务内的 Interceptor 没有这个视野,它只能看到“到达我这个服务的请求”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 网关的管理原理拆解:路由、过滤与横切关注点
2.1 网关为什么必须存在:没有网关的微服务什么样
先想象一下没有网关的微服务集群是什么样子。假设你有订单服务、用户服务、支付服务三个独立部署的应用,每个服务都暴露自己的端口和地址。客户端在页面请求订单数据,就得知道订单服务的 IP 和端口;请求用户信息,就得知道用户服务的 IP 和端口。业务一多,客户端要把所有服务地址硬编码在自己的代码里,任何一个服务扩容、缩容或者换机器,客户端都要跟着改。
更要命的是业务逻辑的重复。登录校验、参数校验、全局限流、统一日志、跨域处理,这些逻辑如果放在每个服务里各写一遍,代码冗余不说,还容易出现“这个服务校验了、那个服务忘了校验”的漏洞。
网关就是把这一堆问题集中收纳的组件。它作为唯一的入口,把“服务寻址”和“横切逻辑”从业务服务里抽离出来,所有客户端请求先到网关,由网关做统一处理以后再分配到具体的服务。这么一来,客户端只需要认识一个域名,服务内部可以随便扩容和调整,横切逻辑也只维护一份。
2.2 路由管理:请求怎么被精准送到目标服务
网关最核心也是最早期的能力,就是路由转发。以 Spring Cloud Gateway 为例,路由(Route)由三个基本要素组成:ID、匹配条件(Predicate)和目标地址(URI)。
Predicate 用来定义“什么样的请求走这条路由”。常见的有按路径匹配(Path=/order/**)、按请求头匹配(Header=X-Tenant-ID, 1001)、按查询参数匹配、按请求方法匹配(Method=GET)、按时间窗口匹配等等。Gateway 内部内置了十几个内置 Predicate,基本都是围绕“请求的哪一部分信息”来设计。
URI 则是这条路由最终要转发到的地点。在配合 Nacos 注册中心的场景下,URI 通常写成 lb://order-service 这种形式。lb 是 Load Balancer 的缩写,意思是用负载均衡的方式,从注册中心拉取 order-service 的所有实例列表,再按负载均衡策略选一个实例转发。这里有个点值得展开:lb 前缀必须配合 spring-cloud-starter-loadbalancer 使用,如果不加负载均衡依赖,Gateway 启动后根本解析不了 lb:// 这种地址。
转发过程中,Gateway 会把客户端的原始请求进行改写——包括替换目标 Host、拼接服务实例的具体 IP 和端口,再把请求发出去。这时候下游服务收到的请求,在路径前缀、请求头上都可能和原始请求不一致。所以设计路由时通常会在服务端加上 StripPrefix 这类过滤器,把网关层添加的前缀去掉,让 Controller 的 RequestMapping 保持简洁。
2.3 横切管理:鉴权、限流、灰度、日志一个都不能少
现代网关早就不是单纯的“路由器”了,它更像一个流量治理平台。我把常用能力列一下:
鉴权认证:网关在把请求转发给下游之前,先校验 token、签名或白名单。比如解析 JWT,校验有效期,把用户 ID 放进请求头传到下游。这样下游服务就无需重复解析 token,只需要信任网关传过来的 X-User-Id 头。
限流降级:比如使用 Sentinel 或网关自带的 RequestRateLimiter,为每个接口、每个用户、每个 IP 配置 QPS 上限。流量一旦超过阈值,网关直接返回 429 或降级响应,而不是把流量压到后端。这块网上关于 Sentinel 做微服务高并发流量治理的实战文章已经很多了,但核心就是一句话:限流要在入口做,才能保护全局。
灰度发布:网关注册到 Nacos 以后,可以根据请求头里的版本号或者用户标签,把请求转发到不同版本的服务实例。这个在 Spring Cloud Gateway 里通过自定义路由谓词或过滤器实现,是上线新版本时的常用手法。
统一日志与监控:所有请求都经过网关,网关自然是最理想的日志采集点。记录请求来源 IP、请求路径、响应耗时,再配合链路追踪把 traceId 下传到微服务,排障效率能提升一个量级。
跨域与协议转换:前端部署在别的域名,网关直接处理 CORS;需要把 HTTP 转 WS 或转 Dubbo 协议,也都在网关层完成。
这些能力有一个共同特点:它们和具体业务无关,属于“横切关注点”。放在网关统一处理,比放在每个服务里分别实现要高效得多,这也是网关存在的根本原因。
2.4 主流网关选型对比
面试和实战中,网关选型也是个高频话题。我整理一个常用对比表:
| 网关名称 | 编程语言 | 定位 | 核心优势 | 适用场景 |
|---|---|---|---|---|
| Spring Cloud Gateway | Java | 基于 WebFlux 的响应式网关 | 与 Spring Cloud 生态无缝集成,内置路由、过滤、限流,支持 lb:// 注册中心联动 | Spring Cloud 微服务体系的标准选择 |
| Nginx | C | 反向代理/负载均衡/静态资源网关 | 高性能、稳定、配置灵活,处理千万级并发没问题 | 作为系统最前端的流量入口,常用于网关集群前方的统一接入层 |
| 神禹网关(ShenYu) | Java | Apache 开源的高性能网关 | 支持 Dubbo、Spring Cloud、HTTP 等多种协议,插件化架构,管理后台可视化 | 多协议接入、需要控制台管理插件的场景 |
| Zuul 1.x | Java | 同步 Servlet 网关 | 处理简单、社区老牌,但性能和并发能力弱于 WebFlux 网络模型 | 遗留旧项目或同步编程偏好的场景 |
选型有一条核心原则:和你的技术生态一致,维护成本最低的那个就是最好的。如果你整个微服务用的是 Spring Cloud Alibaba 栈,那 Spring Cloud Gateway 基本是默认答案,不需要为了“新”去引入其他网关给自己增加维护负担。如果系统最前面还需要一层入口负载均衡,Nginx + Spring Cloud Gateway 的多层网关架构在大型系统里非常常见。ShenYu 的优势主要在多协议和高性能场景,不是大多数项目的主题。
2.5 网关与注册中心联动的工作原理
网关要做“根据服务名找到可用实例”这件事,依赖注册中心。以 Nacos 为例,流程大致是:
- 网关启动时读取 Nacos 配置,获取服务列表并订阅变更;
- 客户端请求到达网关,网关通过
lb://order-service解析服务名,向负载均衡器要一个实例; - 负载均衡器从本地缓存的服务实例列表里挑选一个实例,调用转发;
- 如果实例下线或扩容,Nacos 推送变更给网关,网关的本地服务列表自动更新。
这里有一个关键点:网关并不在每次请求时都去查 Nacos,而是把服务列表缓存在本地,靠长轮询监听变更。这样保证转发性能不会因为动态服务发现而明显下降。理解了这一点,你就能排查一类典型的运维问题——“服务已经下线了,为什么网关还在往里转发?” 多半是 Nacos 的变更推送延迟,或者实例注册信息没有摘除。通常等待心跳超时或手动下线实例就能解决。
3. 微服务内部的三类拦截器:Filter、Interceptor、AOP 到底谁是谁
3.1 Servlet Filter:离容器最近的通用拦截
进入微服务内部的第一个拦截点,是 Servlet 容器层面的 Filter。它不归 Spring 管,是 Servlet 规范里的组件,由 Tomcat、Jetty 这类容器直接管理。
Filter 拦截的是“进入 Web 应用的所有请求”,范围最广,几乎包罗万象。它的生命周期由容器管理,可以注册多个 Filter 形成过滤器链,每个 Filter 在 doFilter 方法里决定放行还是终止。它能看到 request 和 response 的原始形态,但拿到的主要是 HTTP 层面的信息,比如 URL、请求头、参数、cookie。
实际开发中,Filter 适合做几类事:字符集编码、跨域处理、请求日志、防止 XSS、参数校验。因为它不依赖 Spring MVC,所以即使在不用 Spring MVC 的纯 Servlet 项目里也能用。一个常被忽略的细节是:Filter 在 Spring Boot 里的注册方式会影响执行顺序,用 @WebFilter + @ServletComponentScan 注册的 Filter 顺序不好控制,最佳实践是把它写成 Spring Bean 并用 FilterRegistrationBean 显式指定 order。
总之,Filter 是进入 Spring 容器前的最外层防线,所有基于 Servlet 容器的请求都会先经过它。
3.2 Spring MVC HandlerInterceptor:控制器专属守门员
HandlerInterceptor 是 Spring MVC 框架提供的拦截器,它只在 Spring MVC 的 DispatcherServlet 分发请求过程中生效,拦截目标是“即将进入 Controller 的方法调用”。
它有三个核心方法:
- preHandle:Controller 方法执行之前调用,返回 true 继续,返回 false 中断;
- postHandle:Controller 方法执行之后、视图渲染之前调用;
- afterCompletion:整个请求完成后调用,适合做资源清理和统计。
它比 Filter 高明的一点是,能拿到 HandlerMethod,从而精确知道请求最终要调用的是哪个 Controller 的哪个方法。这意味着它可以做“方法级”的精细化控制,比如:某个接口需要管理员权限、某个接口需要租户参数、某个接口要做操作审计。
实际项目中,HandlerInterceptor 最典型的应用场景是登录校验。流程一般是:在 preHandle 里取请求头里的 token,校验合法性,把用户信息塞进 ThreadLocal 供 Controller 使用,返回 false 或者抛异常时返回统一的 401。我见过不少团队把这种校验写在每个 Controller 第一行,后来被迫统一抽成 Interceptor,代码量一下子少了三分之一还多。
3.3 Spring AOP 拦截器:业务方法级切面
第三种拦截器实际上是 AOP,基于动态代理实现,可以通过自定义注解+切面在任意 Spring Bean 方法执行前后插入逻辑。它和 HandlerInterceptor 的本质区别在于:HandlerInterceptor 只管 Controller 这一层,AOP 可以管任何 Bean 的任何方法,包括 Service、Mapper、内部工具类。
最常见的设计模式是自定义注解。比如定义一个 @OperLog 注解,加在需要审计的 Service 方法上,再写一个切面类,用 @Around 包裹目标方法:记录入参、调用时间、返回值、异常信息,做成操作日志。再比如分布式锁注解 @DistributedLock,切面里先尝试获取锁,拿到锁才执行目标方法,拿不到就快速失败或等待。
AOP 还有个优势是可以拿到方法的完整签名和注解信息,实现“声明式”编码——方法上挂个注解,能力就自动生效,不用在业务代码里写一堆 if else。这也是很多框架,比如 Spring 事务、Sentinel、Shiro 注解式权限底层的实现机制。
网上很多文章把 AOP 和 Interceptor 混着写,其实在 Spring 里这两个概念是不同层级的。AOP 通过代理在方法调用前后织入逻辑,HandlerInterceptor 通过处理器映射在请求分发时介入,两者都能拦,但拦的“对象”完全不是一回事。
3.4 三类拦截时机对比与选型建议
我把三种机制在 Spring Boot 项目里的调用顺序和适用场景整理成一张表:
| 类型 | 执行位置 | 拦截对象 | 典型场景 | 能否拿到 Spring Bean |
|---|---|---|---|---|
| Servlet Filter | Servlet 容器内,DispatcherServlet 之前 | 所有 HTTP 请求 | 编码、CORS、全局限流、安全过滤 | 可以,但需注册为 Bean |
| HandlerInterceptor | DispatcherServlet 分发后,Controller 方法前 | 处理器方法(Controller 方法) | 登录校验、权限校验、租户识别、接口幂等 | 可以 |
| Spring AOP | 目标 Bean 方法调用时 | 任意 Spring Bean 方法 | 日志、事务、分布式锁、缓存、Sentinel 降级 | 可以 |
选型经验就一句口诀:能用 HandlerInterceptor 解决的就别用 AOP,需要在 Controller 之前更早介入的就用 Filter,需要管业务方法内部逻辑的才上 AOP。Filter 离业务最远,AOP 离业务最近,中间地带是 HandlerInterceptor。
4. 网关过滤器与微服务 Interceptor 的核心区别
4.1 一张表看清五个关键维度的差异
现在我们回到本文的核心问题:网关里的过滤器和微服务里的 Interceptor 到底差在哪。
| 对比维度 | 网关过滤器(GlobalFilter / GatewayFilter) | 微服务 Interceptor(Filter / HandlerInterceptor) |
|---|---|---|
| 执行位置 | 独立网关进程内,所有微服务之前 | 某个微服务进程内部,Controller 或方法之前 |
| 作用范围 | 全局,所有经过网关的服务都受影响 | 局部,只影响所在服务的请求 |
| 所属生态 | Spring Cloud Gateway / ShenYu / Nginx 等网关层 | Servlet / Spring MVC / Spring AOP 本身 |
| 可见数据 | 请求头、URL、路由信息,看不到下游服务内部状态 | 能拿到 HandlerMethod、请求参数,如果有 AOP 还能拿到方法入参 |
| 典型职责 | 路由、全局限流、统一鉴权、灰度、跨域 | 业务参数校验、用户明细组装、操作日志、租户数据隔离 |
| 故障影响面 | 网关挂了,所有服务不可用(所以必须高可用部署) | 单个服务内部拦截出错,不影响其他服务 |
4.2 从调用链路看两者的本质区别
用一个具体请求的完整链路来理解更直观。
客户端带着 JWT 访问 /order/detail,链路大概是这样的:
- 请求先到 Nginx 或者 SLB;
- 再到 Spring Cloud Gateway;
- 网关的 GlobalFilter 拿到 JWT 做解析,校验通过后在请求头上附加
X-User-Id: 10086; - 路由过滤器根据
/order/**匹配到 order-service,通过lb://负载均衡选出一个实例; - 请求被转发到 order-service 实例;
- order-service 内部先经过 Servlet Filter,做编码、打日志;
- 再经过 HandlerInterceptor 的 preHandle,从
X-User-Id头读取用户 ID,塞到 ThreadLocal; - 进入 Controller,拿到用户 ID 查询数据库;
- 如果 Controller 调用的 Service 方法上有 AOP 注解,再经过 AOP 切面;
- 返回结果逐层回传。
从第 3 步和第 7 步就能看出,网关过滤器主要负责“进不进得来”和“往哪走”,服务的 Interceptor 负责“进来了以后能不能继续往下跑业务”。网关在验证完用户身份后,把用户信息以请求头的形式传给下游;下游的 Interceptor 再去解释这个头并做业务侧的准备。这是一条流水线,不是二选一的关系。
4.3 有些事只能网关做,有些事 Interceptor 做更合适
先说什么只能网关做:
- 全局限流:你得在网关看到了全部流量,才能做整体的 QPS 控制。如果放在单个服务里,每个服务只看到自己那部分流量,很容易被流量洪峰打爆。
- 跨服务路由和灰度:只有网关知道所有服务实例的存在和版本,才能按规则分配流量。
- 全局性的安全防御:比如 WAF 级别的过滤、IP 黑名单、防重放攻击,网关在前面挡一道,后端会轻松很多。
什么在服务内 Interceptor 做更合适:
- 和业务强相关的参数校验:比如这个订单状态能不能取消、这个商品库存够不够,这依赖数据库和服务内部状态,网关根本拿不到。
- 租户数据隔离:多租户系统需要从请求头解析租户 ID 并设置数据范围,HandlerInterceptor 在这里可以顺手把租户上下文放到 ThreadLocal。
- 操作审计日志:记录“谁在什么时间调用了哪个接口、改了哪些数据”,需要结合 HandlerMethod 的注解信息,服务内实现最方便。
- 业务幂等:同一个请求不能重复创建订单,这种校验必须放在业务事务边界内,网关做不了。
简而言之:和全局流量相关的、和业务无关的,放网关;和具体业务状态相关的、需要深入方法内部的,放服务内拦截器。这条原则想清楚了,架构设计基本不会跑偏。
5. 实操落地:典型微服务体系里的正确配合姿势
5.1 场景设定:基于 Nacos + Spring Cloud Gateway 的微服务项目
我用一个非常接近生产环境的例子来说明。假设我们要搭建类似若依微服务 plus 那种结构的系统:一个 Gateway 网关服务、一个 auth-service 负责登录认证、一个 order-service 负责订单、一个 system-service 负责用户和字典,所有服务注册到 Nacos,接口文档用 Knife4j 聚合。
这个架构下,有两个核心拦截点必须设计好:
- 网关层:统一处理所有客户端的 JWT 鉴权、全局限流、跨域;
- 服务内:system-service 和 order-service 各自实现用户上下文解析、租户过滤、操作日志。
如果这两层的职责没有划清楚,项目就会出现“网关里解析了 token 但服务又在解析一次”“服务接口直接暴露没经过鉴权”“改了网关规则结果服务没生效”这类问题。
5.2 网关侧实现统一鉴权、限流与灰度
网关侧统一鉴权,在 Spring Cloud Gateway 里最优雅的实现方式是写一个 GlobalFilter。核心思路:先判断请求路径是否在白名单里(比如 /auth/login、/auth/register、Knife4j 的 /doc.html),白名单直接放行;非白名单请求校验 Authorization 头里的 JWT,校验通过就把 userId 解析出来放进请求头,转运到下游;校验失败返回 401。
代码示意如下:
java复制@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().toString();
// 白名单放行
if (whiteList.contains(path)) {
return chain.filter(exchange);
}
String token = request.getHeaders().getFirst("Authorization");
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
return unauthorized(exchange);
}
try {
Claims claims = JwtUtil.parseToken(token.replace("Bearer ", ""));
// 把用户信息透传给下游服务
ServerHttpRequest mutated = request.mutate()
.header("X-User-Id", claims.get("userId").toString())
.build();
return chain.filter(exchange.mutate().request(mutated).build());
} catch (Exception e) {
return unauthorized(exchange);
}
}
@Override
public int getOrder() {
return -100; // 数值越小越先执行
}
}
限流方面,可以引入 Sentinel 的网关适配器。在 Nacos 或 Sentinel Dashboard 上给 order-service 的 /order/** 路由配置 QPS 阈值,一旦超过直接返回限流响应,不再转发到下游。灰度发布则可以在网关里加一个自定义路由谓词,比如读取 X-Gray-Version 请求头,匹配到对应版本的实例。
5.3 服务内 Interceptor 实现租户隔离与审计日志
服务内部,我强烈建议把“用户上下文”这一层放在 HandlerInterceptor 里实现,而不是在 Controller 里一遍遍写重复代码。
实现思路:在 preHandle 里取网关透传过来的 X-User-Id 头,查出用户的租户 ID、角色码,封装成 UserContext 对象存储到 ThreadLocal;afterCompletion 里清理 ThreadLocal,防止内存泄漏。同时写一个权限拦截器,只放行有 @RequireRole("admin") 注解的方法做权限校验,校验失败抛 403。
代码示意如下:
java复制public class UserContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (handler instanceof HandlerMethod) {
String userId = request.getHeader("X-User-Id");
UserContext.set(userId);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
UserContext.clear();
}
}
配合注册配置:
java复制@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserContextInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/actuator/**", "/v3/api-docs/**");
}
}
至于操作日志,用 AOP 最合适。自定义一个 @OperLog 注解,标注在需要审计的 Service 或 Controller 方法上,用一个 @Around 切面记录方法入参、执行时间、操作人,异步落库。这样代码侵入最小,路径清晰。
5.4 接入 Knife4j 时网关与服务内文档的差异
热词里出现了“微服务整合 Knife4j Nacos”,这里踩坑的人不少。Knife4j 服务端会生成 /v3/api-docs 接口和 /doc.html 页面,问题在于:如果文档从网关访问,网关的鉴权过滤器会拦截掉这些路径,导致开发人员访问不了文档。
解决方案有两个方向。一是网关把文档相关路径加入白名单,比如 /doc.html、/webjars/**、/v3/api-docs/**、/swagger-resources/** 全部放行;二是不从网关访问,直接在浏览器访问具体服务的 IP:端口/doc.html,仅在内网开放。生产环境强烈建议采用第二种思路,别把接口文档暴露在公网,内部使用就足够了。这个细节看似小,但在真实项目里经常被忽视,导致文档“时好时坏”,排查半天才发现是网关拦截导致的。
6. 常见问题与排查技巧实录
6.1 网关返回了 401,但服务内部又抛了一次未登录异常
这是我见过最多的问题之一。团队在网关里解析了 JWT 并把用户 ID 放进请求头,但服务内的 HandlerInterceptor 仍然要求请求带 Authorization 头,两边校验逻辑重复,一旦网关放行的请求头和服务内期望的头不一致,后端的拦截器就会报错。
排查思路很简单:先看服务内拦截器的白名单和网关的白名单是否一致;再看请求头传递的字段名是否对得上。如果网关已经把 JWT 转换成 X-User-Id,服务内就不要再依赖 Authorization,直接信任网关透传的头即可。这种“两层校验”并没有问题,但规则必须统一,否则就是给自己埋坑。
6.2 Interceptor 里读不到 Request Body
HandlerInterceptor 的 preHandle 可以直接 getParameter 但读不到 body,因为 body 是输入流,读过一次就会被消耗掉。我见过同事在拦截器里用 getBody 读取 JSON 做参数校验,结果到 Controller 时 body 已经空了,接口拿到 null,苦恼很久。
解决办法有几个:第一,改用 AOP 切面,在方法入参阶段处理参数;第二,用 Spring 提供的 ContentCachingRequestWrapper 包装请求,让 body 可以被重复读取;第三,把“参数校验”这种需求下沉到 Controller 方法参数上,用 Jakarta Validation 的注解做,根本不需要手动读 body。一般我推荐第三种方案,代码干净还少坑。
6.3 过滤器执行顺序错乱导致鉴权失效
Spring Cloud Gateway 里如果有多个 GlobalFilter 和 GatewayFilter,顺序由 getOrder() 方法控制,数值越小越先执行。很多同学自定义鉴权过滤器没实现 Ordered,或者给了一个正数,结果顺序排在路由转发之后,鉴权根本来不及生效,请求已经发出去了。
建议:统一约定,鉴权过滤器 order 设为 -100,限流过滤器设为 -200,日志过滤器设为 -50,后续再新增过滤器时按这个“优先级预算”来定。同时,网关里优先使用 GlobalFilter 而不是局部 GatewayFilter + 每个路由都配置一遍,维护成本天差地别。
6.4 用日志链路把网关到服务的调用串起来
排查网关和拦截器问题,最忌讳的是网关日志一份、服务日志一份,出问题时两头对不上。我现在的习惯是:网关生成一个 traceId,放进请求头(比如 X-Trace-Id)透传到下游;所有服务在日志打印时带上 traceId;排障时直接 grep 同一个 traceId,一键串联整条调用链。
配合参数在日志格式里加 %X{traceId},或者干脆接上 SkyWalking / OpenTelemetry 这类链路追踪工具,效果更好。实现这个链路,很多“网关和服务到底谁拦了请求”的争论,看一眼日志就真相大白。
最后说点个人的真实体会。网关和 Interceptor 的区别,本质上不是“用哪个更好”,而是“各自管哪一段”。我见过很多团队为了省事把鉴权全写在网关里,结果服务内部完全没有用户上下文,业务代码一团乱;也见过把跨域、全局限流全塞进服务内拦截器的,结果每个服务重复实现一遍,改一处漏一处。最舒服的状态是:网关只管“大门口的事”,服务内拦截器只管“自己家门内的事”,中间用一套请求头约定对接起来。把这条边界划清楚,微服务架构才算真正立住了。
如果你正在搭微服务,或者面试前想把这几个概念理一遍,不妨拿这个思路去复盘你自己项目的调用链——从网关到 Filter、HandlerInterceptor、AOP,每一步问一句“这层在管什么,为什么放在这层”,你会有一种豁然开朗的感觉。
