网关:网络关口,负责请求路由,转发,身份校验
一、配置网关的路由转发规则
整体流程 :
- 前端发起请求
http://127.0.0.1:8080/items/list,请求先到达网关。 - 网关从 Nacos 注册中心拉取微服务列表。
- 网关读取路由配置,用断言 (predicates) 判断当前请求路径是否匹配规则。
- 如果匹配成功,网关通过
lb://负载均衡,把请求转发给对应的微服务。 - 微服务处理完请求,结果原路返回给前端。

Nacos 两个作用:
- 微服务启动 → 服务注册,把自己信息上报给 Nacos
- 网关启动 → 服务拉取,拿到所有微服务实例地址,用来负载均衡
yaml代码
spring:
cloud:
gateway:
routes: # 路由集合,可以配置多条路由规则
# 第一条路由
- id: item # 路由规则id,自定义,只要求唯一,用来区分不同路由
uri: lb://item-service # 路由目标微服务;lb = loadBalance负载均衡
# 网关去Nacos找到名为item-service的服务,做负载均衡转发
predicates: # 路由断言:判断请求是否满足规则,满足才转发
- Path=/items/** # 路径断言:请求路径以 /items/ 开头,就匹配这条路由
# `**` 代表匹配后面任意多级路径,例如 /items/list、/items/1
# 第二条路由示例
- id: xx
uri: lb://xx-service
predicates:
- Path=/xx/**
- id:路由标识,自定义、唯一,只是给这条路由起名字,没有业务转发作用。
- uri :转发目标地址
lb://服务名:从 Nacos 获取服务实例,自动负载均衡(必须引入 nacos-discovery 依赖)
- predicates(断言) :匹配条件 ,只有请求满足断言,这条路由才生效。
Path=/items/**:路径匹配。访问网关/items/xxx就会命中该路由。
举例:
前端请求地址:http://127.0.0.1:8080/search/list
- 请求访问网关 8080 端口
- 网关读取断言:
Path=/items/**,/search/**,请求路径/search/list满足/search/**规则,命中这条item-service路由 uri: lb://item-service→ 网关去 Nacos 拉取item-service商品微服务实例- 负载均衡转发请求给 item-service 微服务
- item-service 里
SearchController的/search/list接口接收请求,处理完数据原路返回给前端

①Path=/items/**,/search/**逗号
,代表 或 的关系,满足任意一个就匹配成功**代表匹配后面任意多级子路径可以匹配(会转发给 item-service)
/items/list/items/1/search/list(就是咱们前面 SearchController 的接口)/search/xxx/abc不会匹配
/user/list(前缀不是 items、也不是 search)
二、路由属性
网关路由对应的 Java 类型:RouteDefinition,4 个核心属性:id,uri,predicates,filters
- id :路由唯一标示 作用:给这条路由起一个唯一名字,用来区分多条路由,没有转发逻辑,自定义即可。
- uri:路由目标地址 作用:请求匹配成功后,转发的目标地址。 两种写法:
lb://服务名:去 Nacos 拉取实例,负载均衡转发(微服务项目用这个)http://ip:端口:直接写死固定地址
- predicates(路由断言) 作用:判断请求是否符合当前路由。只有断言全部满足,这条路由才生效,请求才会转发。
| 名称 | 说明 | YAML 示例 |
|---|---|---|
| Path | 请求路径匹配(最常用),判断请求 url 路径是否符合规则 | - Path=/items/**,/search/** |
| Method | 判断请求方式,限定 GET/POST 等 | - Method=GET,POST |
| Header | 判断请求头是否包含指定 header,支持正则 | - Header=X-Request-Id, \d+ |
| Query | 判断请求参数,要求 url 带某个参数 | - Query=name |
| Host | 判断访问的域名 host | - Host=**.hmall.com |
| RemoteAddr | 判断客户端来源 IP | - RemoteAddr=192.168.1.1/24 |
我们前面用的
Path=/items/**,/search/**就是路径断言。
- filters(路由过滤器) 作用:对请求或者响应做特殊处理。 可以在请求转发给微服务之前,修改请求;微服务返回结果之后,修改响应。 举例:去掉请求路径前缀、添加请求头、限流。
StripPrefix
配置:- StripPrefix=1 前端访问网关:http://网关ip:8080/api/items/list
- 原始路径:
/api/items/list - 去掉 1 段前缀
/api - 转发给微服务的路径:
/items/list
StripPrefix=1:去掉路径最前面的 1 段路径前缀
http://127.0.0.1:8080/api/items/list网关拿到的原始请求路径 :/api/items/list拆分路径分段: 第 1 段:
api第 2 段:items第 3 段:list
StripPrefix=1→ 删掉1段api:/items/list
StripPrefix=2:删 2 段/api/items/list→/list
bash
spring:
cloud:
gateway:
routes:
- id: item-service
uri: lb://item-service
predicates:
- Path=/api/items/** # 匹配所有/api/items开头
filters:
- StripPrefix=1 # 删掉第一段/api
YAML 代码对照
bash
spring:
cloud:
gateway:
routes:
- id: item-service # id:路由唯一标识
uri: lb://item-service # uri:目标转发地址
predicates: # predicates:路由断言,匹配请求
- Path=/items/**,/search/**
filters: # filters:路由过滤器,处理请求/响应
- StripPrefix=1
流程:前端请求进来 → predicates 断言判断是否匹配 → 匹配成功 → filters 过滤器处理请求 → 转发到 uri 对应的微服务 → 微服务返回响应 → filters 过滤器处理响应 → 返回前端
三、登录校验

网关过滤器分两大类:
- GatewayFilter(路由过滤器) 作用在你指定的某一条路由上 ,默认不生效,必须写在对应路由的 filters 里面配置才生效
- GlobalFilter(全局过滤器) 作用于网关全部路由,所有请求都会经过, 代码声明成 Bean 之后,自动生效,不需要在 yaml 路由里额外配置
bash
spring:
cloud:
gateway:
routes:
- id: item-service
uri: lb://item-service
predicates:
- Path=/items/**,/search/**
filters:
# 这条过滤器【只对item-service这条路由生效】GatewayFilter
- AddRequestHeader=truth, anyone long-press like button will be rich
- id: user-service
uri: lb://user-service
predicates:
- Path=/addresses/**,/users/**
# default-filters 默认过滤器:所有路由都会执行
default-filters:
- AddRequestHeader=truth, anyone long-press like button will be rich
| 类型 | 生效范围 | 使用方式 |
|---|---|---|
| GatewayFilter 路由过滤器 | 单个指定路由 | 在 routes 下对应路由的 filters 配置;只作用当前路由 |
| GlobalFilter 全局过滤器 | 网关所有路由 | Java 代码写 Bean,自动全局生效,不用写 yaml |
| default-filters 默认过滤器 | 网关所有路由 | yaml 配置,作用全部路由,属于配置型 GatewayFilter |
1.自定义GlobalFilter(用的最多)
java
/**
* 自定义全局过滤器 GlobalFilter
* 实现Ordered接口,用来设置过滤器执行优先级
*/
@Component
public class MyGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求对象
ServerHttpRequest request = exchange.getRequest();
// 获取请求头
HttpHeaders headers = request.getHeaders();
System.out.println("headers = " + headers);
// 从请求头获取token
String token = headers.getFirst("token");
// 判断token是否为空
if (token == null || token.isEmpty()) {
// token不存在,设置响应状态码401 未授权
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 直接结束请求,不放行,拦截请求
return exchange.getResponse().setComplete();
}
// token存在,放行,继续向后执行
return chain.filter(exchange);
/**
* 设置过滤器执行顺序
* 返回的值越小,优先级越高,过滤器越早执行
*/
@Override
public int getOrder() {
// 过滤器执行顺序,值越小,优先级越高
return 0;
}
}
2.登录校验
application.yaml
bash
hm:
jwt:
location: classpath:hmall.jks # RSA密钥库位置,与登录服务(hm-service)共用同一密钥
alias: hmall
password: hmall123
tokenTTL: 30m
auth:
exclude-paths: # 登录校验白名单,命中直接放行
- /user/login
- /user/register
- /doc.html
location: classpath:hmall.jks
- jks:Java 密钥库文件,存放 RSA 公私钥对
- 放在 resources 下(classpath);登录服务拿私钥生成 token,网关拿公钥校验 token(非对称特点)
alias: hmall:密钥在 jks 库里面的别名,用来定位密钥password: hmall123:jks 密钥库的访问密码,读取密钥库需要tokenTTL: 30m:token 过期时间 30 分钟
exclude-paths:不需要校验 token 的白名单接口
/user/login:登录接口,还没有 token,必须放行/user/register:注册接口/doc.html:接口文档页面(Knife4j)
AuthProperties.java自定义配置属性类
java
@Data
@ConfigurationProperties(prefix = "hm.auth")
public class AuthProperties {
private List<String> excludePaths;
}
@ConfigurationProperties(prefix = "hm.auth"):读取 yaml 中hm.auth开头的配置,自动绑定到类的成员变量。private List<String> excludePaths:接收 yaml 里的白名单路径集合。- 必须在启动类 添加注解
@EnableConfigurationProperties(AuthProperties.class)开启配置属性扫描。@Datalombok 注解,自动生成 get/set 方法。
自定义异常 UnauthorizedException
java
/**
* 未授权异常:JWT校验失败时抛出
* 网关不依赖 hm-common(其knife4j会传递引入spring-webmvc,与Gateway的WebFlux冲突),
* 因此在网关内部定义该异常
*/
public class UnauthorizedException extends RuntimeException {
public UnauthorizedException(String msg) {
super(msg);
}
public UnauthorizedException(String msg, Throwable e) {
super(msg, e);
}
}
- JWT 校验失败(无 token、过期、篡改)抛出此运行时异常
- 在 GlobalFilter 捕获,返回 401 状态码
JwtTool.java 工具类核心
java
/**
* JWT工具类(从 hm-service 复制,与登录服务共用同一套RSA密钥)
*/
@Component
public class JwtTool {
private final JWTSigner jwtSigner;
public JwtTool(KeyPair keyPair) {
this.jwtSigner = JWTSignerUtil.createSigner("rs256", keyPair);
}
public String createToken(Long userId, Duration ttl) {
// 1.生成jws
return JWT.create()
.setPayload("user", userId)
.setExpiresAt(new Date(System.currentTimeMillis() + ttl.toMillis()))
.setSigner(jwtSigner)
.sign();
}
public Long parseToken(String token) {
// 1.校验token是否为空
if (token == null) {
throw new UnauthorizedException("未登录");
}
// 2.校验并解析jwt
JWT jwt;
try {
jwt = JWT.of(token).setSigner(jwtSigner);
} catch (Exception e) {
throw new UnauthorizedException("无效的token", e);
}
// 2.校验jwt是否有效
if (!jwt.verify()) {
// 验证失败
throw new UnauthorizedException("无效的token");
}
// 3.校验是否过期
try {
JWTValidator.of(jwt).validateDate();
} catch (ValidateException e) {
throw new UnauthorizedException("token已经过期");
}
// 4.数据格式校验
Object userPayload = jwt.getPayload("user");
if (userPayload == null) {
// 数据为空
throw new UnauthorizedException("无效的token");
}
// 5.数据解析
try {
return Long.valueOf(userPayload.toString());
} catch (RuntimeException e) {
// 数据格式有误
throw new UnauthorizedException("无效的token");
}
}
}
Algorithm.HMAC256对称加密,网关和登录服务秘钥必须一致verifier.verify(token):一次性校验签名、token 格式、过期时间- 核心:校验失败抛异常,不是返回 null,过滤器要用 try-catch 捕获
getClaim("user_id"):读取 JWT 载荷里存放的用户 ID
AuthGlobalFilter.java核心过滤器
java
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final AuthProperties authProperties;
private final JwtTool jwtTool;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 1.获取请求路径
String path = request.getPath().toString();
// 2.判断是否需要做登录拦截
if (isExclude(path)) {
// 白名单,放行
return chain.filter(exchange);
}
// 3.获取token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if (headers != null && !headers.isEmpty()) {
token = headers.get(0);
}
// 4.校验并解析token(JwtTool内部失败抛UnauthorizedException)
Long userId;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
// 捕获鉴权异常,返回401
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 5.传递用户信息:把userId放入请求头传给下游微服务
ServerHttpRequest newRequest = request.mutate()
.header("user-id", userId.toString())
.build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
// 6.放行
return chain.filter(newExchange);
}
private boolean isExclude(String path) {
// 循环配置里的白名单路径
for (String excludePath : authProperties.getExcludePaths()) {
if (path.contains(excludePath)) {
return true;
}
}
return false;
}
@Override
public int getOrder() {
// 鉴权过滤器优先执行
return 0;
}
}
执行流程:
1.先写:application.yaml(配置文件)
-
再写:AuthProperties.java 配置属性类
-
自定义异常 UnauthorizedException
-
JwtTool.java JWT 工具
-
最后写:AuthGlobalFilter.java 全局过滤器
3.网关传递到微服务
网关鉴权成功后,把 userId 放入请求头user-info 转发给下游微服务;下游微服务通过MVC 拦截器 读取请求头里的 user-info,存入ThreadLocal,在 controller/service 中随时获取当前登录用户。分层:Gateway 网关层(WebFlux) → 下游微服务(SpringMVC)
- 网关 AuthGlobalFilter:token 校验成功,mutate 新增
user-info请求头- 写 UserContext 工具类(ThreadLocal)
- 写 UserInfoInterceptor 拦截器,preHandle 读取 header 存入 TL,afterCompletion 清理
- MvcConfig 配置类,注册拦截器,让拦截器生效
网关模块:AuthGlobalFilter.java
java
// 5.传递用户信息:校验成功,修改exchange,添加user-info请求头
exchange = exchange.mutate()
.request(builder -> builder.header("user-info", userId.toString()))
.build();
// 6.放行,注意:放行要用修改后的exchange
return chain.filter(exchange);
下游微服务:UserInfoInterceptor.java(MVC 拦截器)
java
/**
* 用户信息拦截器:读取网关切片后通过 user-info 请求头透传的userId,
* 存入UserContext(ThreadLocal),供同线程内controller/service直接获取
*/
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.从请求头拿到网关传过来的user-info
String userInfo = request.getHeader("user-info");
// 2.判断不为空,转Long,存入ThreadLocal
if (StrUtil.isNotBlank(userInfo)) {
UserContext.setUser(Long.valueOf(userInfo));
}
// 3.放行
return true;
}
// 请求结束后,清理ThreadLocal,防止线程池复用导致串号、内存泄漏
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
UserContext.removeUser();
}
}
UserContext.java 工具类(ThreadLocal 封装)
java
public class UserContext {
// ThreadLocal,保存当前线程的userId
private static final ThreadLocal<Long> TL = new ThreadLocal<>();
// 存入用户id
public static void setUser(Long userId){
TL.set(userId);
}
// 获取用户id
public static Long getUser(){
return TL.get();
}
// 删除,清理
public static void removeUser(){
TL.remove();
}
}
MvcConfig.java 注册拦截器(让拦截器生效)
java
/**
* MVC配置:注册UserInfoInterceptor
* 通过META-INF/spring.factories自动装配,
* 使包名为com.qcby.*的微服务(cart-service/item-service)也能自动加载,无需逐个配置
*/
@Configuration
@ConditionalOnClass(DispatcherServlet.class) // 仅在SpringMVC环境下生效
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册用户信息拦截器,对所有请求生效
registry.addInterceptor(new UserInfoInterceptor());
}
}
4.OpenFeign传递用户(微服务之间调用)
前面网关把user-info放到请求头,同一个请求内,A 微服务调用 B 微服务(OpenFeign 远程调用)。
问题:默认 Feign 发起远程调用时,不会自动携带当前请求的请求头 ,B 微服务拿不到
user-info,UserContext 为空。
解决方案:Feign 请求拦截器 RequestInterceptor
写一个 Feign 拦截器,在 Feign 发起远程调用之前 ,从当前线程UserContext取出 userId,手动放到 Feign 新请求的请求头user-info里。
注意:不是从 HttpServletRequest 拿 header!直接从 ThreadLocal 拿 userId
java
@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 1.从当前线程的ThreadLocal获取userId
Long userId = UserContext.getUser();
// 2.判断不为空,添加到feign请求头 user-info
if(userId != null){
template.header("user-info", userId.toString());
}
5.总结
网关统一鉴权,网关把用户 ID 传给下游微服务;微服务之间 OpenFeign 远程调用时,继续把用户 ID 往下传递,全程用user-info请求头 + ThreadLocal。
前端 → Gateway 网关(校验 JWT,添加user-info请求头) → 微服务 A(MVC 拦截器读 header → ThreadLocal) → A 通过 OpenFeign 调用 B(Feign 拦截器从 ThreadLocal 取出 userId,放入 Feign 请求头user-info) → 微服务 B(MVC 拦截器读 header → ThreadLocal)
前端请求携带 token → Gateway 网关
- 执行
AuthGlobalFilter全局过滤器,网关里所有请求都会先进这个过滤器- 过滤器做登录校验:白名单判断 → 获取 Authorization 的 token → JWT 校验解析得到 userId,失败返回 401
- 校验成功:使用
exchange.mutate()新增user-info请求头,转发请求到下游微服务 A- 微服务 A:
hm-commonMVC 拦截器读取user-info,userId 存入 ThreadLocal(UserContext)- A 服务OpenFeign调用B:hm-api:触发 Feign 的
RequestInterceptor,从 UserContext 拿 userId,添加到 Feign 请求头user-info- 请求到达 B 微服务:B 的 MVC 拦截器读取
user-info存入 B 的 ThreadLocal- A、B 服务的 Controller/Service 都可以通过 UserContext.getUser () 拿到登录用户 ID
- 请求结束,MVC 拦截器清理 ThreadLocal四、配置管理
四、结构目录
浏览器/前端(nginx:80)
│
hm-gateway:8080 ── JWT统一鉴权、路由、user-info透传
│ lb://(Nacos服务发现 + LoadBalancer)
┌────┴────────────┐
cart-service:8081 item-service:8082 (未来还有 user/order-service)
└──── Feign ──────┘ (hm-api 定义客户端接口)
│
MySQL(hmall 库) + Nacos 注册中心
html
hmall项目根目录
├─ hmall-nginx # Nginx静态资源服务器,托管前端页面,反向代理请求转发到网关hm-gateway
├─ hm-gateway # 网关模块(SpringCloud Gateway)【登录校验就在这里】
├─ hm-common # 公共通用模块,**被所有微服务依赖**
├─ hm-api # OpenFeign接口定义模块,存放远程调用客户端接口
├─ hm-service # 基础公共服务(部分项目版本)
├─ cart-service # 购物车微服务
├─ item-service # 商品微服务
1.hm-gateway 网关模块(核心:登录校验 GlobalFilter)
端口统一入口,所有前端请求先进网关,统一鉴权、路由转发
hm-gateway
└─ src/main/java/com/hmall/gateway
├─ config
│ ├─ AuthProperties.java # 读取配置文件:白名单路径(不需要登录的接口)
│ ├─ JwtProperties.java # JWT配置:密钥、有效期
│ └─ SecurityConfig.java # 安全相关配置
├─ filters
│ └─ AuthGlobalFilter.java # ✅【重点】自定义GlobalFilter全局过滤器,登录校验逻辑在这里
├─ exception # 网关全局异常
├─ util # JWT工具类,解析token
└─ GatewayApplication.java # 网关启动类
└─ resources
├─ application.yaml # 网关路由配置、nacos地址
└─ hmall.jks # JWT签名证书(RSA非对称加密)
核心工作
AuthGlobalFilter:实现GlobalFilter,拦截请求,校验 JWT- 校验成功后,新增
user-info请求头携带 userId,转发请求给下游微服务 - 白名单放行登录、注册接口,其余接口强制校验 token
2.hm-common 公共模块(所有微服务都要引入这个依赖!)
公共抽取,避免重复代码,利用 spring.factories 实现自动装配
html
hm-common
└─ src/main/java/com/hmall/common
├─ advice # 全局异常处理器 @RestControllerAdvice
├─ config # 通用配置类
├─ domain # 通用返回结果类 Result
├─ exception # 自定义业务异常
├─ interceptor
│ └─ UserInfoInterceptor.java # MVC拦截器:读取user-info请求头,userId存入ThreadLocal(UserContext)
└─ utils
└─ UserContext.java # ThreadLocal工具类,存/取/清除当前登录用户userId
└─ resources
└─ META-INF
└─ spring.factories # 自动装配文件:让SpringBoot自动加载拦截器、配置类,业务微服务不用写@Configuration
关键:cart-service、item-service 引入 hm-common 依赖后,自动注入
UserInfoInterceptor,不用自己写拦截器代码
3. hm-api 模块(OpenFeign 接口包)
Feign 接口统一存放,微服务之间远程调用的接口定义,供各个微服务引入依赖
html
hm-api
└─ src/main/java/com/hmall/api
├─ client # Feign客户端接口(@FeignClient),例如ItemClient、CartClient
├─ config # Feign配置类:RequestInterceptor Feign拦截器(传递user-info请求头!)
└─ dto # Feign远程调用用到的DTO传输对象
重点:
RequestInterceptor写在这里,引入 hm-api 的微服务自动获得 Feign 拦截能力,远程调用自动携带 userId
4.cart-service 购物车微服务(业务微服务示例)
cart-service
└─ src/main/java/com/qcby/cartservice
├─ client # 引入hm-api里的FeignClient,远程调用其他服务(如商品服务)
├─ controller # 购物车接口,Controller直接从UserContext获取userId
├─ domain # 实体类、DTO
├─ mapper # Mybatis Mapper接口,操作购物车表
├─ service # 业务逻辑层
└─ CartServiceApplication # 启动类
└─ resources
├─ application.yaml # 基础配置
├─ application-dev.yaml # 开发环境配置
└─ application-local.yaml # 本地环境配置
- 依赖:hm-common + hm-api
- 自动拥有:MVC 用户拦截器 + Feign 用户传递拦截器,不用重复写代码
6. hm-service(基础服务模块)
hm-service
└─ src/main/java/com/hmall
├─ config
├─ controller
├─ domain
├─ interceptor
├─ mapper
├─ service
├─ utils
└─ HMallApplication
7. hmall-nginx
Nginx①托管前端页面, 直接返回 HTML、JS、CSS、图片,不需要经过 Java 后端 。②反向代理 前端页面里调用接口(登录、查购物车),请求路径是/api/**,Nginx 识别到这类动态接口请求,转发给 hm-gateway 网关,再由网关路由到各个微服务。