跳转至

网关、认证过滤器与拦截器

本文讲通用职责、请求处理顺序、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
  1. Path 匹配请求路径,例如 /api/profile;不包含 Host。
  2. StripPrefix=1 去掉第一个路径片段,转发路径变成 /profile,不是删除第一个字符;见 Spring Cloud Gateway 官方示例。
  3. lb:// 表示通过负载均衡机制解析服务实例,需要相应依赖、服务发现与可达实例;它不是普通 DNS 域名。
  4. 若 uri 为 http://...,则是指定目标地址,不应根据文档示例推断已启用服务发现。
  5. 路由能转发不等于可以匿名访问,安全链仍独立执行。

排障顺序:网关是否可达 → 路由是否匹配 → 目标实例是否存在且可达 → 转发路径是否正确 → 认证与业务授权是否通过。不要把 404 一律归结为 Token 错误,也不要用公开所有路径解决 401。

实现或变更安全规则后,应验证公开端点的精确匹配、无效凭据、401/403、浏览器预检与实际跨源请求、伪造代理头和重定向地址;本地测试不能证明配置中心与实际下游环境正确。