03网关与nacos配置管理
微服务拆分完成后,五个独立服务(用户、商品、购物车、交易、支付)暴露了新的问题:每个微服务都有不同的地址或端口,前端调用时要访问不同地址;前端无法直接连接 Nacos,无法实时更新服务列表;单体架构时只需一次用户登录、身份校验,微服务后每个服务都要重复编写登录校验逻辑;微服务间相互调用时还难以传递用户信息。引入网关与 Nacos 配置管理正好可以一并解决这些问题,主要包含三件事:网关路由解决前端请求入口、网关过滤器解决统一登录校验与用户信息传递、Nacos 统一配置管理解决配置重复和热更新。
一、网关概述
网关就是网络的关口,负责请求的路由、转发、身份校验。前端请求不再直接打向某个微服务,而是统一发到网关,由网关判断该把请求路由到哪个具体服务。

网关需要完成三件事:身份校验、路由转发、负载均衡。请求到达网关后先做身份校验,校验通过再根据规则路由到对应微服务,并通过注册中心拉取服务列表完成负载均衡。

在 SpringCloud 中网关的实现主要有两种:
- Spring Cloud Gateway:Spring 官方出品,基于 WebFlux 响应式编程,无需调优即可获得优异性能。
- Netflix Zuul:Netflix 出品,基于 Servlet 的阻塞式编程,需要调优才能获得与 SpringCloudGateway 类似的性能。
本项目选用 Spring Cloud Gateway。
二、网关路由
2.1 快速入门
网关服务搭建非常简单,共四步:创建新模块、引入网关依赖、编写启动类、配置路由规则。整个流程对外只暴露一个网关地址,背后连接 Nacos 注册中心完成服务注册与发现。
2.1.1 引入依赖
创建新的 hm-gateway 模块,引入网关依赖:
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>2.1.2 配置路由规则
在 application.yaml 中配置路由:
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.150.101:8848
gateway:
routes:
- id: item # 路由规则id,自定义,唯一
uri: lb://item-service # 路由目标微服务,lb代表负载均衡
predicates: # 路由断言,判断请求是否符合规则
- Path=/items/** # 以请求路径做判断,以/items开头则符合
- id: xx
uri: lb://xx-service
predicates:
- Path=/xx/**完整的请求流程是:前端请求 http://127.0.0.1:8080/items/list → 网关根据 /items/** 路由规则匹配 → 通过 Nacos 拉取 item-service 实例列表 → 负载均衡选择一个实例 → 转发请求到目标微服务。
2.2 路由属性
网关路由对应的 Java 类型是 RouteDefinition,其中常见的属性有:
- id:路由唯一标示
- uri:路由目标地址
- predicates:路由断言,判断请求是否符合当前路由
- filters:路由过滤器,对请求或响应做特殊处理
2.2.1 路由断言
Spring 提供了 12 种基本的 RoutePredicateFactory 实现:
| 名称 | 说明 | 示例 |
|---|---|---|
| After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
| Before | 是某个时间点之前的请求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
| Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
| Cookie | 请求必须包含某些 cookie | - Cookie=chocolate, ch.p |
| Header | 请求必须包含某些 header | - Header=X-Request-Id, \d+ |
| Host | 请求必须是访问某个 host(域名) | - Host=.somehost.org,.anotherhost.org |
| Method | 请求方式必须是指定方式 | - Method=GET,POST |
| Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/** |
| Query | 请求参数必须包含指定参数 | - Query=name, Jack 或者 - Query=name |
| RemoteAddr | 请求者的 ip 必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
| Weight | 权重处理 | - Weight=group1, 2 |
| XForwardedRemoteAddr | 基于请求的来源 IP 做判断 | - XForwardedRemoteAddr=192.168.1.1/24 |
2.2.2 路由过滤器
网关中提供了 33 种路由过滤器,每种过滤器都有独特的作用。常用的包括:
| 名称 | 说明 | 示例 |
|---|---|---|
| AddRequestHeader | 给当前请求添加一个请求头 | AddRequestHeader=headerName,headerValue |
| RemoveRequestHeader | 移除请求中的一个请求头 | RemoveRequestHeader=headerName |
| AddResponseHeader | 给响应结果中添加一个响应头 | AddResponseHeader=headerName,headerValue |
| RemoveResponseHeader | 从响应结果中移除一个响应头 | RemoveResponseHeader=headerName |
| RewritePath | 请求路径重写 | RewritePath=/red/?(?.*), /${segment} |
| StripPrefix | 去除请求路径中的 N 段前缀 | StripPrefix=1,则路径 /a/b 转发时只保留 /b |
三、网关登录校验
3.1 网关请求处理流程
要在网关转发之前完成登录校验,需要先理解 Spring Cloud Gateway 的请求处理流程。整个流程包含几个核心组件:
- HandlerMapping(路由映射器):默认实现是
RoutePredicateHandlerMapping,根据请求找到匹配的路由并存入上下文,然后把请求交给 WebHandler 处理。 - WebHandler(请求处理器):默认实现是
FilteringWebHandler,它会加载网关中配置的多个过滤器,放入集合并排序,形成过滤器链,然后依次执行。 - Filter(过滤器):内部可以包含 pre 和 post 两部分逻辑,分别在请求路由到微服务之前和之后执行。所有 Filter 的 pre 逻辑依次顺序执行通过后,请求才会被路由到微服务,否则会被拦截,后续过滤器不再执行。微服务返回结果后,再倒序执行 Filter 的 post 逻辑。
- NettyRoutingFilter(Netty 路由过滤器):负责将请求转发到微服务,当微服务返回结果后存入上下文。
3.2 自定义过滤器
要在网关做登录校验,需要自定义过滤器。网关过滤器有两种:
- GatewayFilter:路由过滤器,作用于任意指定的路由;默认不生效,要配置到路由后生效。
- GlobalFilter:全局过滤器,作用范围是所有路由;声明后自动生效。
两种过滤器的过滤方法签名完全一致:
public interface GlobalFilter {
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
public interface GatewayFilter extends ShortcutConfigurable {
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}其中 ServerWebExchange 是请求上下文,包含整个过滤器链内共享数据(如 request、response);GatewayFilterChain 是过滤器链,当前过滤器执行完后调用它的 filter 方法即可放行到下一个过滤器。

3.2.1 自定义 GlobalFilter
自定义 GlobalFilter 比较简单,直接实现 GlobalFilter 接口即可:
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求
ServerHttpRequest request = exchange.getRequest();
// 2.过滤器业务处理
System.out.println("GlobalFilter pre阶段 执行了。");
// 3.放行
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
System.out.println("post阶段 执行了。");
}));
}
@Override
public int getOrder() {
// 过滤器执行顺序,值越小,优先级越高
return 0;
}
}3.2.2 自定义 GatewayFilter
自定义 GatewayFilter 不是直接实现 GatewayFilter,而是实现 AbstractGatewayFilterFactory,类名必须以 GatewayFilterFactory 为后缀,方便配置使用:
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
// 自定义配置属性,成员变量名称很重要,下面会用到
@Data
public static class Config {
private String a;
private String b;
private String c;
}
// 将 Config 字节码传递给父类,父类负责帮我们读取 yaml 配置
public PrintAnyGatewayFilterFactory() {
super(Config.class);
}
// 将变量名称依次返回,顺序很重要,将来读取参数时需要按顺序获取
@Override
public List<String> shortcutFieldOrder() {
return List.of("a", "b", "c");
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 编写过滤器逻辑
System.out.println("PrintAny filter 执行了");
// 放行
return chain.filter(exchange);
}
};
}
}在 yaml 中通过类名前缀声明使用:
spring:
cloud:
gateway:
default-filters:
- PrintAny=1,2,33.3 实现登录校验
黑马商城基于 JWT 实现登录校验,相关 JWT 工具原本在 hm-service 模块,可以将其拷贝到 gateway 模块,然后基于 GlobalFilter 实现统一登录校验。
校验逻辑:判断请求路径是否在白名单 → 获取请求头中的 token → 校验并解析 token → 校验通过则放行,否则返回 401。
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final JwtTool jwtTool;
private final AuthProperties authProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取Request
ServerHttpRequest request = exchange.getRequest();
// 2.判断是否不需要拦截
if (isExclude(request.getPath().toString())) {
return chain.filter(exchange);
}
// 3.获取请求头中的token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if (!CollUtils.isEmpty(headers)) {
token = headers.get(0);
}
// 4.校验并解析token
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}
// 5.如果有效,传递用户信息(详见下一节)
System.out.println("userId = " + userId);
// 6.放行
return chain.filter(exchange);
}
private boolean isExclude(String antPath) {
for (String pathPattern : authProperties.getExcludePaths()) {
if (antPathMatcher.match(pathPattern, antPath)) {
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}3.4 网关传递用户
网关解析出用户身份后,需要把它传给下游微服务,使微服务也能拿到当前登录用户。整体链路是:网关过滤器校验 JWT → 保存用户到请求头 → 微服务 MVC 拦截器读取请求头 → 保存到 ThreadLocal 供业务使用。

3.4.1 网关保存用户到请求头
在登录校验拦截器中,校验成功后修改下游请求:
exchange.mutate() // mutate就是对下游请求做更改
.request(builder -> builder.header("user-info", userInfo))
.build();3.4.2 微服务拦截器获取用户
由于每个微服务都需要获取登录用户,将拦截器定义在 hm-common 模块中,微服务引入依赖即可生效,无需重复编写。在 hm-common 模块下定义 HandlerInterceptor:

public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 1.获取请求头中的用户信息
String userInfo = request.getHeader("user-info");
// 2.判断是否为空
if (StrUtil.isNotBlank(userInfo)) {
// 不为空,保存到ThreadLocal
UserContext.setUser(Long.valueOf(userInfo));
}
// 3.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 移除用户
UserContext.removeUser();
}
}再在 hm-common 中编写 SpringMVC 配置类注册拦截器,并通过 SpringBoot 自动装配让所有微服务都能加载到它:
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}在 resources/META-INF/spring.factories 中声明:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MvcConfig3.5 OpenFeign 传递用户
微服务之间通过 OpenFeign 调用时也需要传递用户信息。OpenFeign 提供了 RequestInterceptor 拦截器接口,所有由 OpenFeign 发起的请求都会先调用拦截器处理请求:
public interface RequestInterceptor {
void apply(RequestTemplate template);
}利用 RequestTemplate 提供的方法可以修改请求头。这样每次 OpenFeign 发起请求时都会调用该方法,自动把当前线程中的用户写入请求头,传递给下游微服务:

@Bean
public RequestInterceptor userInfoRequestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 获取登录用户
Long userId = UserContext.getUser();
if (userId == null) {
return;
}
// 如果不为空则放入请求头中,传递给下游微服务
template.header("user-info", userId.toString());
}
};
}至此,整个微服务登录解决方案就形成了完整闭环:浏览器携带 JWT → 网关 GlobalFilter 校验并保存用户到请求头 → 微服务 HandlerInterceptor 读取并写入 ThreadLocal → OpenFeign RequestInterceptor 把 ThreadLocal 中的用户写入下游请求头。
四、配置管理
4.1 为什么需要配置管理
微服务项目普遍存在三个配置痛点:
- 微服务重复配置过多,例如数据库、日志、Swagger 等配置在 5 个服务中几乎一样,维护成本高。
- 业务配置经常变动,每次修改都要重启服务。
- 网关路由配置写死,路由变更需要重启网关,影响整个后端系统。
Nacos 提供的配置管理服务正好覆盖这三个场景:配置共享、配置热更新、动态路由。
4.2 配置共享
4.2.1 添加共享配置到 Nacos
把一些通用配置抽取到 Nacos 中,包括 Jdbc、MybatisPlus、日志、Swagger、OpenFeign 等配置。例如 shared-jdbc.yaml、shared-log.yaml、shared-swagger.yaml。

4.2.2 拉取共享配置
SpringCloud 在初始化上下文时会先读取 bootstrap.yaml(或 bootstrap.properties),把 Nacos 地址配置到 bootstrap.yaml 中,就可以在项目引导阶段拉取 Nacos 中的配置。

以 cart-service 为例:
- 引入依赖:
<!--nacos配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>- 新建
bootstrap.yaml:
spring:
application:
name: cart-service # 服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 共享配置
- dataId: shared-jdbc.yaml # 共享jdbc配置
- dataId: shared-log.yaml # 共享日志配置
- dataId: shared-swagger.yaml # 共享swagger配置- 简化
application.yaml,最终只保留本服务独有的内容(端口、Feign 配置、Swagger 信息、数据库名等)。
4.3 配置热更新
配置热更新是指修改 Nacos 中的配置文件时,微服务无需重启即可让配置生效。实现前提条件有两个:
- Nacos 中要有一个与微服务名有关的配置文件,dataId 格式为
${spring.application.name}-${spring.profiles.active}.${file-extension}。 - 微服务中要以特定方式读取需要热更新的配置属性。
例如购物车数量上限的配置,先在 Nacos 中添加 cart-service-dev.yaml,写入:
hm:
cart:
maxItems: 10 # 购物车商品数量上限在微服务中读取该属性有两种方式。方式一:使用 @ConfigurationProperties 注解类,自动绑定配置前缀:
@Data
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
private int maxItems;
}方式二:使用 @Value 注解字段,并配合 @RefreshScope 注解类:
@Data
@RefreshScope
public class CartProperties {
@Value("${hm.cart.maxItems}")
private int maxItems;
}业务中直接注入 CartProperties 即可使用,Nacos 修改后无需重启服务就能生效。
4.4 动态路由
动态路由的目标是把网关的路由配置保存到 Nacos,当 Nacos 中的路由配置变更时推送最新配置到网关,实时更新网关中的路由信息。要完成两件事:
- 监听 Nacos 配置变更的消息。
- 当配置变更时,将最新的路由信息更新到网关路由表。
4.4.1 监听 Nacos 配置变更
通过 NacosConfigManager 拿到 ConfigService,调用 getConfigAndSignListener 在首次拉取配置的同时注册监听器:
private final NacosConfigManager nacosConfigManager;
public void initRouteConfigListener() throws NacosException {
// 1.注册监听器并首次拉取配置
String configInfo = nacosConfigManager.getConfigService()
.getConfigAndSignListener(dataId, group, 5000, new Listener() {
@Override
public Executor getExecutor() {
return null;
}
@Override
public void receiveConfigInfo(String configInfo) {
// 监听到配置变更,更新一次配置
updateConfigInfo(configInfo);
}
});
// 2.首次启动时,更新一次配置
updateConfigInfo(configInfo);
}4.4.2 更新路由表
监听到路由信息后,利用 RouteDefinitionWriter 来更新路由表。该接口提供两个方法:save 用于添加或覆盖路由(如果路由 id 重复则覆盖旧路由),delete 用于根据路由 id 删除某个路由。
public interface RouteDefinitionWriter {
/** 更新路由到路由表,如果路由id重复,则会覆盖旧的路由 */
Mono<Void> save(Mono<RouteDefinition> route);
/** 根据路由id删除某个路由 */
Mono<Void> delete(Mono<String> routeId);
}更新时一般先清空旧路由再写入新路由,避免残留。
4.4.3 路由配置语法
为了方便解析从 Nacos 读取到的路由配置,推荐使用 JSON 格式的路由配置,例如:
[
{
"id": "item",
"uri": "lb://item-service",
"predicates": [{
"name": "Path",
"args": {
"_genkey_0": "/items/**",
"_genkey_1": "/search/**"
}
}],
"filters": []
},
{
"id": "cart",
"uri": "lb://cart-service",
"predicates": [{
"name": "Path",
"args": {"_genkey_0": "/carts/**"}
}],
"filters": []
}
]这样将来新增或调整微服务路由时,只需在 Nacos 控制台修改 JSON 配置即可,无需重启网关。
总结
本文围绕网关与配置管理完成了三件事:
- 网关路由:使用 Spring Cloud Gateway 作为统一入口,通过
routes配置实现基于路径的路由转发,背后结合 Nacos 完成服务发现与负载均衡。 - 网关登录校验:通过自定义
GlobalFilter完成 JWT 校验,把用户信息写入请求头传递给下游微服务;微服务通过HandlerInterceptor读取请求头并保存到 ThreadLocal;OpenFeign 再通过RequestInterceptor把用户信息传递给下游调用。 - 配置管理:利用 Nacos 配置管理服务完成配置共享、配置热更新和动态路由,既减少了微服务的重复配置,又解决了频繁重启服务的问题,还支持网关路由的实时变更。