概述
把 mf-gateway 网关搭起来之后,要解决的问题是:原来散落在每个服务里的 token 校验代码,怎么收拢到网关统一做。这里讲清「鉴权放哪、怎么放行白名单、失败怎么响应」这套思路,完整代码实现见后续的网关统一鉴权代码实现。
纲要
- 为什么要统一鉴权:N 个服务 N 套拦截器的痛
- 早期做法:每个 MVC 服务配 TokenInterceptor
- 演进方向:鉴权上移到网关,后端「信任网关」
- 网关过滤器的工作原理
- GlobalFilter:全局过滤器,所有路由的请求都经过
- 处理流程:读 Authorization 头 → 剥 Bearer 前缀 → JwtUtils.verifyToken → 失败 401 + JSON
- 白名单:
gateway.excludedUrls配在 yml,@Value注入 List - 响应式编程模型的特殊处理:GlobalFilter 里不能直接 return,要用 ServerHttpResponse + Mono 写响应
- 保留还是移除后端拦截器:两个选项的取舍(附工程实测结论)
- 双体系鉴权:一个过滤器管 app 端和 admin 端两套 token
回顾早期的做法:mf-app-server 里写了一个 TokenInterceptor(MVC 拦截器),在 preHandle 里校验 token,失效就返回 401;再写一个 WebConfig 把它注册进 Spring MVC,还要配 excludePathPatterns 放行登录接口。当时只有一个服务,这套写法没什么问题。
但现在服务多起来了。app 端服务要校验用户 token,后台管理服务要校验管理员 token,将来再加一个新服务,就得再来一遍:引 mf-commons 拿到 JwtUtils、写拦截器、写注册配置、维护一份白名单。每个项目都复制一套校验代码,改一次规则就要改 N 个地方。
所以把权限校验的代码抽取出来,放到网关去做:客户端的请求先到网关,网关先做权限校验,校验通过再路由转发。这就是统一鉴权。
用图对比改造前后:
一套过滤器代码,管住所有进来的请求,这就是收益。
网关过滤器的工作原理
用 GlobalFilter 而不是 GatewayFilter
Spring Cloud Gateway 里定义过滤器有两条路:GatewayFilter 作用于某一条路由,要在路由配置里逐条指定;GlobalFilter 是全局过滤器,所有路由的请求都会经过。统一鉴权显然是全局需求,不管请求是 /app/** 还是 /admin/**,都要过同一道校验,所以实现 GlobalFilter 接口。
同时实现 Ordered 接口,通过 getOrder() 指定执行顺序。网关里可以配多个过滤器(跨域的 CorsWebFilter 就是一个),getOrder() 的返回值决定谁先执行。
处理流程
请求进入网关后,过滤器按四步走:
- 取当前请求路径,判断是否在白名单里,在就直接放行转发(登录、验证码接口不需要鉴权);
- 从请求头取 token:读
Authorization头。后台管理页面(vue 工程)发请求时 token 带Bearer前缀,需要剥掉; - 校验 token:调用
mf-commons里的JwtUtils.verifyToken(token); - 失效就拦截:返回 401 状态码 + JSON 错误信息,请求不再往后走;有效则
chain.filter(exchange)放行。
白名单配置化
白名单不是硬编码在过滤器里,而是配在 yml 中,过滤器里用 @Value 注入成 List<String>:
# 自定义配置,定义不需要校验 token 的连接
gateway:
excludedUrls: /user/login,/user/loginVerification,/system/users/verification,/system/users/login
@Value("${gateway.excludedUrls}")
private List<String> excludedUrls; //需要配置不校验的连接
逗号分隔的字符串会被 Spring 自动绑定成 List<String>。过滤器里一行 excludedUrls.contains(path) 就完成判断。配置化带来的好处很直接:以后新增一个公开接口,改一行配置就行,不用动代码、不用重新编译。
注意白名单的精确匹配:contains(path) 要求路径完全相等,/user/login 能放行,但 /user/login/xxx 或带查询参数的路径放不了行。这是课程方案的简化,够用,但要知道边界在哪。
这份配置在工程里并不在本地 bootstrap.yml,而是放到了 Nacos 配置中心(mf-gateway-prod.yml),路由规则也在那里:
spring:
cloud:
gateway:
routes:
# 手机端访问
- id: mf-app-server
uri: lb://mf-app-server
predicates:
- Path=/app/**
filters:
- StripPrefix= 1
# 管理后台
- id: mf-admin
uri: lb://mf-admin
predicates:
- Path=/admin/**
filters:
- StripPrefix= 1
gateway:
excludedUrls: /user/login,/user/loginVerification,/system/users/verification,/system/users/login
路由按路径前缀分成两条:/app/** 转发到 mf-app-server,/admin/** 转发到 mf-admin,StripPrefix=1 转发前剥掉第一级前缀。
响应式编程模型:为什么不能像拦截器那样写
这是从 MVC 迁移到网关最容易踩的坑。Gateway 基于 WebFlux,是响应式的,编程模型和 MVC 完全不同。
MVC 拦截器的 preHandle 返回 boolean:true 放行,false 拦截,同步阻塞式,想写响应直接操作 response 对象就行。而 GlobalFilter.filter() 返回 Mono<Void>,一个异步的响应式流,没有 boolean 可返回,也没有「同步写完响应再返回」的机会。放行是 return chain.filter(exchange)(把请求交给过滤器链继续传);拦截则要自己往 ServerHttpResponse 里写数据:
ServerHttpResponse response = exchange.getResponse();
// 用 ObjectMapper 把 Map 转成 JSON 字节数组
byte[] data = objectMapper.writeValueAsBytes(responseData);
// 包装成响应式世界的 DataBuffer
DataBuffer buffer = response.bufferFactory().wrap(data);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
// 用 Mono 把 buffer 写进响应体
return response.writeWith(Mono.just(buffer));
两套编程模型的对照:
| 对比项 | MVC 拦截器 | 网关 GlobalFilter |
|---|---|---|
| 接口 | HandlerInterceptor | GlobalFilter + Ordered |
| 所在技术栈 | Servlet / Spring MVC(阻塞式) | WebFlux / Reactor(响应式) |
| 核心方法 | preHandle | filter |
| 返回值 | boolean:true 放行 / false 拦截 | Mono<Void>:放行返回 chain.filter(exchange) |
| 写响应 | 直接操作 HttpServletResponse | ServerHttpResponse + DataBuffer + writeWith(Mono) |
| 响应体 | response.getWriter().write(...) | 字节转 DataBuffer,包在 Mono.just() 里 |
| 拦截语义 | 返回 false 即终止 | 不调用 chain.filter 即终止 |
| 理解了「响应式世界没有同步 return」这一点,代码实现就只剩照着流程填空了。 |
保留还是移除后端拦截器
网关做了校验之后,后端服务里原有的 TokenInterceptor 怎么办?两个选项:
| 方案 | 做法 | 优点 | 代价 |
|---|---|---|---|
| 移除后端拦截器 | 校验完全信任网关,服务里不留校验代码 | 最简洁,服务零鉴权代码 | 有服务被绕过网关直连,就完全不设防;服务内部也无法拿到当前用户信息 |
| 保留双层 | 网关拦一道,服务内拦截器留一道 | 纵深防御:网关被绕过仍有兜底 | 公共模块和拦截器配置还是得引,代码没有真正减掉 |
看工程快照的实际做法,后端拦截器保留了,但瘦了身。mf-app-server 的 TokenInterceptor 里,校验 token 并返回 401 的那段代码被注释掉了,只剩下解析 token、把用户信息放进 ThreadLocal 的部分:
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1、获取请求头
String token = request.getHeader("Authorization");
//(校验逻辑已上移到网关:JwtUtils.verifyToken + 401 拦截被注释)
//解析token,获取id和手机号码,构造User对象,存入Threadlocal
Claims claims = JwtUtils.getClaims(token);
String mobile = (String) claims.get("mobile");
// 数值字段按大小选型:小值反序列化为 Integer,真实雪花 ID(远超 2^31)为 Long,统一按 Object 读取再 toString
Object idObj = claims.get("id");
Long id = Long.valueOf(idObj.toString());
User user = new User();
user.setId(id);
user.setMobile(mobile);
UserHolder.set(user);
return true;
}
}
WebConfig 也还在,excludePathPatterns 里的登录接口白名单也还在,它和网关的 excludedUrls 是两份配置,要手动保持一致。
也就是说课程实际采用的是**「校验上移 + 解析下沉」的分工**:
| 职责 | 执行者 |
|---|---|
| token 是否有效(验签、过期判断) | 网关 AuthFilter,失败 401 |
| token 里的用户信息解析出来放进 ThreadLocal | 服务内 TokenInterceptor(降级为解析器) |
这个分工是课程的教学简化:业务代码里大量使用 UserHolder.getUserId(),解析逻辑必须留在服务里;而校验逻辑挪到网关后,每个服务少维护一份 401 拦截。但要清楚,严格说这不是完整意义上的纵深防御,后端拦截器里 JwtUtils.getClaims(token) 解析一个无效 token 仍可能抛异常,真正的双层校验应该在服务内也重新验签。生产环境的常见选择是保留双层完整校验,或者走网关透传用户 ID + 服务内网隔离的路线。课程方案理解了取舍,怎么选就是需求问题。
双体系鉴权:一个过滤器管两套 token
系统里有两类身份:app 端用户(手机号登录)和后台管理员(账号密码登录),token 的 claims 内容不同(用户放 id/mobile,管理员放 id/username)。网关是同一个 AuthFilter 统一校验,没有按身份拆过滤器。
隔离靠的是路由前缀 + 白名单:
- app 端请求走
/app/**路由,公开接口是/user/login、/user/loginVerification; - admin 端请求走
/admin/**路由(网关剥前缀后实际路径是/system/users/...),公开接口是/system/users/verification、/system/users/login。
四条白名单放两个体系的登录入口,其余请求不分身份一律验签。过滤器只回答「token 有没有效」,不关心「是用户还是管理员」,身份语义由后端各自的拦截器解析。
异常与边界
| 场景 | 现象 | 处理 |
|---|---|---|
| 请求不带 Authorization 头 | token 为 null | verifyToken(null) 返回 false,401 拦截 |
| token 过期或被篡改 | 验签失败 | 401 + {"errCode":401,"errMessage":"用户未登录"} |
| 白名单漏配登录接口 | 登录请求也要求 token,登录都进不去,系统直接不可用 | 检查 Nacos 里 excludedUrls 是否包含该路径;这是排障第一入口 |
| 白名单多配了路径 | 本该保护的接口裸奔 | 上线前核对白名单清单,白名单只放公开接口 |
| 剥 Bearer 前缀遗漏 | app 端 token 无前缀没问题,vue 后台带 Bearer 的 token 验签必失败 | 统一在过滤器里 replace("Bearer ", "") |
| 响应式写响应编码问题 | 中文错误信息乱码 | 显式设置 Content-Type: application/json;charset=UTF-8,JSON 按 UTF-8 字节写入 |
| 直接绕过网关访问后端服务 | 后端只剩解析逻辑,无效 token 解析时抛异常 | 生产环境应限制后端服务仅内网可达,或恢复服务内校验 |
验证思路
网关起起来、Nacos 配置加载好之后,可以按预期结果逐条验证:
| 用例 | 操作 | 预期 |
|---|---|---|
| 白名单放行 | 不带 token POST /system/users/login | 200,正常登录 |
| 未登录拦截 | 不带 token GET /app/users/findUserById/1 | 401,响应体含 errCode 401 |
| 带有效 token | 带 Authorization 头访问业务接口 | 200,正常返回数据 |
| 带失效 token | 携带过期或伪造的 token | 401 |
注意请求地址要用网关端口(讲义示例 8888,工程 prod 配置为 9997),且路径带 /app、/admin 前缀,所有请求统一从网关进出。具体代码实现与实测过程,见下一篇。
常见误解澄清
| 误解 | 事实 |
|---|---|
| 「GlobalFilter 就是 GatewayFilter 的别名」 | 两者不同:GatewayFilter 作用于单条路由,需在路由里配置;GlobalFilter 对所有路由生效,注册为 Bean 即自动加载 |
| 「网关校验后,后端拦截器就该全删」 | 工程里删掉的是校验逻辑,解析逻辑保留(ThreadLocal 依赖它);全删后服务拿不到当前用户 |
「网关里可以像 MVC 那样 response.setStatus(401); return false;」 | WebFlux 响应式模型没有这些同步 API,必须用 ServerHttpResponse.writeWith(Mono) |
| 「白名单写在过滤器代码里更安全」 | 写 yml/Nacos 才能不改代码调整;配置变更要走审查,和安全性不冲突 |
| 「双重配置(网关 excludedUrls + 拦截器 excludePathPatterns)是冗余 bug」 | 两层各管各的放行:网关拦全局入口,拦截器管服务内注册,课程方案下需手动保持一致 |
官方文档
- Spring Cloud Gateway - Global Filters
- Spring Cloud Gateway - GatewayFilter Factories
- Project Reactor - Reactor 核心特性
总结
- 统一鉴权的动机:N 个服务各写一套拦截器不可维护,把校验抽到网关做一次,后端「信任网关」。
- 实现载体是
GlobalFilter+Ordered:全局生效、可排序;流程为白名单判断 → 读 Authorization 头 → 剥 Bearer →JwtUtils.verifyToken→ 失败 401 + JSON。 - 白名单配置化(
gateway.excludedUrls+@Value注入),登录、验证码等公开接口免校验,新增公开接口只改配置。 - Gateway 是 WebFlux 响应式栈,拦截逻辑用
Mono/DataBuffer写响应,与 MVC 拦截器的 boolean 模型是两套世界观。 - 后端拦截器去留的答案在工程里:校验上移网关,解析下沉保留(ThreadLocal),理解这是课程简化,生产可按需恢复双层验签。
- 一个
AuthFilter通过/app/、/admin/路由前缀与四条白名单,同时管住用户和管理员两套 token 体系。
下一篇网关:统一鉴权代码实现把 AuthFilter 的完整代码逐行落地并实测。
454



被折叠的 条评论
为什么被折叠?



