网关、认证过滤器与拦截器¶
本文讲通用职责、请求处理顺序、CORS 和可信代理,并保留独立 MVC 入门实验;
如果还没写过 HTTP 接口,先完成 Spring Boot 入门练习。密码认证原理见 认证和授权,协议原理见 OAuth2 与 OIDC,验签细节见 JWT 与 JWKS。
先认识网络和框架词语¶
| 词语 | 含义及作用 |
|---|---|
| 反向代理 | 客户端请求一个统一地址,代理替客户端访问后端并转回响应;网关是承担更多 API 治理职责的入口 |
| 路由(route) | “符合什么条件的请求转发到哪里”的规则,通常包含标识、目标地址、匹配条件及过滤器 |
| Predicate(断言) | 判断请求是否符合路由,例如 Path 根据 URL 路径匹配;不是修改请求的代码 |
| Filter(过滤器) | 处理请求或响应的一环,例如去前缀、写头、限流;不同框架的 Filter 接口不能混用 |
| 负载均衡(load balancing) | 多个服务实例可用时选择其中一个处理请求;不等于一个服务启动了多个 Java 类 |
| 服务发现 / Nacos | 服务上报实例地址,调用方按服务名发现它们;Nacos 还可提供配置管理,两种能力不能混为一谈 |
| Servlet | Java Web 请求与响应的标准 API;Spring MVC 建立在这一体系上,入口调度器称 DispatcherServlet |
| WebFlux / Reactor / Mono | WebFlux 是响应式 Web 框架;Reactor 提供其常用响应式类型;Mono 表示最多一个结果或完成/错误的异步序列 |
1. 网关在系统中的位置¶
网关是客户端访问后端服务的统一入口,可负责路由、跨域、入口认证和请求治理。它不承担用户注册等领域业务,也不替代下游服务的授权检查。
App / 浏览器
↓
网关:路由、入口安全、请求治理
├── 认证服务:核验凭据、维护认证协议与会话
└── 业务服务:处理业务、检查接口权限和对象归属
这是职责示意,不规定每个项目都必须拆成三个服务,也不证明某个远端路由已经部署。安全白名单决定是否允许请求,路由规则决定转发到哪里,二者不能互相替代。
2. Filter、WebFilter、Interceptor 不要混用¶
| 机制 | 所属体系 | 常见用途 |
|---|---|---|
Servlet Filter |
Servlet / Spring MVC | 请求进入 MVC 前后的过滤,Servlet Spring Security 过滤链基于此体系 |
MVC HandlerInterceptor |
Spring MVC Handler 执行链 | 围绕 Controller 执行做框架层处理,不代替完整安全过滤链 |
WebFlux WebFilter |
响应式 Web 应用 | 在响应式请求链中处理请求,可用于响应式网关的入口治理 |
Gateway GlobalFilter / GatewayFilter |
Spring Cloud Gateway 路由过滤链 | 全局或特定路由的转发处理 |
基于 WebFlux 的网关使用 SecurityWebFilterChain;基于 Servlet 的业务服务使用 SecurityFilterChain。它们不属于同一套请求处理模型,不能直接复制 Servlet 的线程绑定用户方案。
请求进去、响应回来时的顺序¶
下面是同步 MVC 请求的简化顺序,异常和异步处理还存在各自分支:
Servlet Filter 前置
→ DispatcherServlet 找到 Controller
→ Interceptor.preHandle
→ Controller → Service
← Interceptor.postHandle(正常处理返回后)
← Interceptor.afterCompletion(MVC 本次处理收尾)
← Servlet Filter 中 chain.doFilter 返回后的逻辑
preHandle 返回 true 才继续;返回 false 表示拦截,应明确处理响应。只有成功完成前置阶段的拦截器才有相应收尾调用;不要假定失败、异步等情况始终等同于同步成功路径。对于直接写 JSON 的 Controller,postHandle 时响应可能已提交,不适合再修改响应头。Spring MVC 拦截器说明
响应式链的 chain.filter(exchange) 返回的是 Mono,不是已经完成的远程响应。需要用 then、doFinally 等响应式组合表达后置处理;直接写在方法调用下一行,不代表等请求完成后再执行。GlobalFilter 的顺序由路由链中的排序决定;它与 WebFilter 是不同链,不能只比较两个 @Order 数字就断言跨链先后。
ThreadLocal 是线程局部存储,值绑定线程而不是某个用户。线程池复用时不清理会污染后续请求;响应式请求可能跨线程执行,应使用响应式安全上下文,不能依赖手写 ThreadLocal 传用户。
3. 资源服务器与公开入口¶
采用 JWT 时,应配置框架的 OAuth2 Resource Server 支持,由受信解码器完成验证;仅拆开 Base64 内容不能证明身份。配置与验证步骤见 JWT 验证流程,框架接入见 Spring Security 响应式资源服务器。
不能只让网关写一个 username Header,就让下游无条件相信它。网关认证不能代替业务对象授权;分层信任、内网隔离与服务间验证应由架构明确设计。
公开入口应同时限制 HTTP 方法和路径。permitAll() 只表示该路径不要求先满足访问授权条件,不等于下游无需验证密码、授权码或刷新凭据。不要用过宽通配符顺带公开未来接口。
调试公开端点时不要自动附带无效 Bearer Token,安全过滤器可能在路径授权之前就拒绝它。登录请求是否无需既有 Token,应以接口契约为准。
4. 认证错误与业务响应的边界¶
响应式 Spring Security 中:
ServerAuthenticationEntryPoint用于发起认证或处理缺少有效认证的情况;Bearer API 通常返回 HTTP 401。ServerAccessDeniedHandler处理访问被拒绝;已认证但权限不足通常返回 HTTP 403。- 验证成功后继续请求链,由目标业务处理请求;不是由网关提前返回“登录成功”。
浏览器页面可能需要重定向;OAuth2/OIDC 协议端点应保留对应协议的错误格式,不能把它们都改为自定义业务 JSON。Servlet 异常与响应式过滤链也各有处理入口,见 异常处理。
响应式响应通常通过 Mono 表达完成或错误,DataBuffer 承载字节。序列化后再写入响应,不能吞掉失败或在过滤器中加入阻塞业务查询;JSON 相关基础见 序列化。
5. 转发头为什么需要可信边界¶
反向代理可能改变请求实际经过的地址,而下游又需要知道用户看到的外部地址,才能生成正确的登录回调或重定向。Forwarded、X-Forwarded-Host、X-Forwarded-Proto 等头可以传递相关信息,但公网客户端也能伪造它们。
可靠设计需要明确哪一层代理可信、下游允许哪些网络来源,并由可信边界清理或重建信息。增加负载均衡器或 TLS 终止代理后,必须重新检查这条信任链;不能仅打开“使用转发头”的开关就认为安全。Spring 转发头说明
6. CORS:浏览器怎样决定跨源访问¶
CORS 是 Cross-Origin Resource Sharing(跨源资源共享)。Origin(源)由协议、主机和端口组成,不包含路径。例如两个地址端口不同,也属于不同源。
浏览器脚本跨源读取响应受同源策略限制;服务端用 CORS 响应头声明允许的源、方法和请求头。某些请求先发送 OPTIONS 预检,预检通过后才发送实际请求;不能假定所有跨源请求都会预检。
配置时明确:
- 允许哪些 Origin;涉及 Cookie 等凭据时尤其要限制可信源。
- 允许哪些 HTTP 方法和请求头。
- 是否允许携带凭据,以及哪些响应头允许脚本读取。
- CORS 应在哪层处理,避免网关和下游产生冲突或重复响应头。
允许携带凭据时,不能把 Access-Control-Allow-Origin 返回为 *。CORS 是浏览器执行的跨源策略,不是 App、脚本或命令行的身份验证机制,也不能替代权限检查。Spring CORS 文档
CSRF处理的是另一类风险。网关与下游是否需要 CSRF 防护,要分别根据凭据是否自动携带、登录协议与会话机制判断;不能因为一层禁用,另一层也照抄禁用。
7. 动手:用响应头验证 MVC 拦截器确实执行了¶
这是普通 MVC 学习实验,不是网关认证方案。前置:完成 Spring Boot 第 9 节,操作在 Mac 的独立 hhjava-learning-basics;无需新增依赖,不添加 Spring Security Starter。
7.1 写拦截器¶
文件:src/main/java/com/example/learning/web/LearningInterceptor.java
package com.example.learning.web;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
public class LearningInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
// 只证明这个教学拦截器执行过,不能把此响应头当作身份或授权证明。
response.setHeader("X-Learning-Interceptor", "visited");
return true;
}
}
不用为本练习读取 Authorization、比较密码或自行解析 JWT。handler 是被 MVC 选中的处理器;request/response 是本次请求响应,不是全局共享的用户对象。
7.2 注册到 MVC¶
文件:src/main/java/com/example/learning/web/LearningWebConfiguration.java
package com.example.learning.web;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class LearningWebConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LearningInterceptor())
.addPathPatterns("/hello", "/books");
}
}
WebMvcConfigurer 用于补充 MVC 配置,注册后仅对这两条路径生效。不需要 @EnableWebMvc 接管整套 Boot MVC 配置,也不需要另外给拦截器加一个重复的 Bean 注册入口。
7.3 启动和验证¶
停止原学习进程,在学习工程根目录运行 mvn spring-boot:run,另开 Mac 终端:
curl -i 'http://127.0.0.1:18080/hello'
预期仍是 HTTP 200 与问候 JSON,额外出现 X-Learning-Interceptor: visited 响应头;HTTP 头名称大小写不敏感。Reqable 在响应 Headers 中查看,而不是在 JSON 中找这个字段。
7.4 用测试锁定结果¶
文件:src/test/java/com/example/learning/LearningInterceptorTest.java
package com.example.learning;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class LearningInterceptorTest {
@Autowired
private MockMvc mvc;
@Test
void shouldPassRequestAndAddLearningHeader() throws Exception {
mvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(header().string("X-Learning-Interceptor", "visited"));
}
}
运行 mvn -Dtest=LearningInterceptorTest test,预期通过。头不存在时先查注册类是否被扫描、请求路径是否匹配、是否已重启;请求被截断时检查 preHandle 是否错误返回 false。
8. 怎样读懂路由,避免把教学规则当成部署事实¶
先确认配置来自本地文件还是配置中心,再检查实际生效的匹配条件、目标地址与路径变换。以下是虚构教学示例,不是要求直接写入 Nacos:
Path=/api/**
StripPrefix=1
uri=lb://account-service
- Path 匹配请求路径,例如
/api/profile;不包含 Host。 - StripPrefix=1 去掉第一个路径片段,转发路径变成
/profile,不是删除第一个字符;见 Spring Cloud Gateway 官方示例。 lb://表示通过负载均衡机制解析服务实例,需要相应依赖、服务发现与可达实例;它不是普通 DNS 域名。- 若 uri 为
http://...,则是指定目标地址,不应根据文档示例推断已启用服务发现。 - 路由能转发不等于可以匿名访问,安全链仍独立执行。
排障顺序:网关是否可达 → 路由是否匹配 → 目标实例是否存在且可达 → 转发路径是否正确 → 认证与业务授权是否通过。不要把 404 一律归结为 Token 错误,也不要用公开所有路径解决 401。
实现或变更安全规则后,应验证公开端点的精确匹配、无效凭据、401/403、浏览器预检与实际跨源请求、伪造代理头和重定向地址;本地测试不能证明配置中心与实际下游环境正确。