Spring Cloud微服务 —— 网关路由、统一鉴权与动态配置管理


一、问题引入:微服务拆分后的痛点

模块 服务名称
用户服务 user-service
商品服务 item-service
购物车服务 cart-service
交易服务 trade-service
支付服务 pay-service

拆分后出现了几个常见问题

问题一:前端访问困难

  • 每个微服务的端口不同,前端要维护多个地址;
  • 前端无法通过 Nacos 获取实时服务列表。

问题二:身份认证割裂

单体架构下只需登录一次即可共享用户信息;
而微服务拆分后,每个服务都需要单独做登录校验与用户信息传递。

解决方案:使用微服务网关 Gateway!


二、网关路由(Gateway Routing)

2.1 网关是什么?

网关(Gateway)是微服务系统的统一入口,承担以下功能:

  • 请求转发与路由;
  • 登录校验与权限控制;
  • 日志记录与限流。

类比现实:

网关就像园区传达室的大爷,所有人(请求)都得先经过他:

  • 不合法的请求会被拦截;
  • 合法请求会被带到对应的服务。

2.2 Spring Cloud Gateway 简介

Spring 提供了两种网关方案:

方案 状态 特点
Netflix Zuul 已淘汰 Servlet 模式,性能低
Spring Cloud Gateway ✅ 推荐 基于 WebFlux,性能高,响应式支持

官网:https://spring.io/projects/spring-cloud-gateway


2.3 快速上手

① 创建网关模块

在项目根目录创建新模块:

1
hm-gateway

② 引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<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>

③ 启动类

1
2
3
4
5
6
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}

④ 路由配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.150.101:8848
gateway:
routes:
- id: item
uri: lb://item-service
predicates:
- Path=/items/**,/search/**
- id: user
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
- id: cart
uri: lb://cart-service
predicates:
- Path=/carts/**

⑤ 测试访问

1
http://localhost:8080/items/page?pageNo=1&pageSize=1

请求会自动被转发到 item-service


三、网关统一鉴权(JWT 登录校验)

3.1 鉴权逻辑

传统方案中,每个微服务都验证 JWT,存在两个问题:

  • 每个服务都需保存秘钥,不安全;
  • 每个服务都要写一遍鉴权逻辑,冗余。

解决方案:统一在网关层做登录校验


3.2 网关过滤器 Filter 机制

Gateway 的请求处理流程:

  1. HandlerMapping 匹配路由;
  2. WebHandler 执行过滤器链;
  3. 每个 Filter 执行 pre/post 两段逻辑;
  4. 只有 pre 全通过,请求才会被路由。

🧠 因此,只要我们在 NettyRoutingFilter 之前 插入自定义过滤器,即可实现登录校验。


3.3 自定义全局过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

private final JwtTool jwtTool;
private final AuthProperties authProperties;
private final AntPathMatcher matcher = new AntPathMatcher();

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();

// 1. 检查是否在白名单
if (isExclude(request.getPath().toString())) return chain.filter(exchange);

// 2. 获取 token
String token = request.getHeaders().getFirst("authorization");

// 3. 校验并解析
Long userId;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
exchange.getResponse().setRawStatusCode(401);
return exchange.getResponse().setComplete();
}

// 4. 将用户信息写入请求头
ServerHttpRequest newRequest = exchange.getRequest()
.mutate()
.header("user-info", userId.toString())
.build();

return chain.filter(exchange.mutate().request(newRequest).build());
}

private boolean isExclude(String path) {
return authProperties.getExcludePaths().stream()
.anyMatch(pattern -> matcher.match(pattern, path));
}

@Override
public int getOrder() { return 0; }
}

✅ 校验成功后,用户信息通过请求头传递到下游服务。

四、微服务中获取登录用户信息

4.1 拦截器保存用户上下文

hm-common 模块中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
String userInfo = req.getHeader("user-info");
if (StrUtil.isNotBlank(userInfo)) {
UserContext.setUser(Long.valueOf(userInfo));
}
return true;
}

@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception ex) {
UserContext.removeUser();
}
}

通过自动装配 (spring.factories) 全局生效。


五、Feign 调用中的用户信息传递

微服务之间通过 OpenFeign 调用时,也要传递用户信息。

实现方式:定义一个 Feign 请求拦截器。

1
2
3
4
5
6
7
8
9
@Bean
public RequestInterceptor userInfoRequestInterceptor() {
return template -> {
Long userId = UserContext.getUser();
if (userId != null) {
template.header("user-info", userId.toString());
}
};
}

这样,每个 Feign 请求都会自动附带登录用户信息,实现端到端的鉴权链路。


六、统一配置管理(Nacos Config)

6.1 为什么要用配置中心?

当前问题:

  • 配置分散在各服务;
  • 修改配置需重启;
  • 多处重复配置。

使用 Nacos Config,集中管理、动态刷新、共享配置。


6.2 共享配置步骤

1️⃣ Nacos 控制台创建共享配置:

  • shared-jdbc.yaml(数据库)
  • shared-log.yaml(日志)
  • shared-swagger.yaml(接口文档)

2️⃣ 在服务中引入依赖并创建 bootstrap.yaml

1
2
3
4
5
6
7
8
9
10
11
12
spring:
application:
name: cart-service
cloud:
nacos:
server-addr: 192.168.150.101
config:
file-extension: yaml
shared-configs:
- dataId: shared-jdbc.yaml
- dataId: shared-log.yaml
- dataId: shared-swagger.yaml

3️⃣ 本地 application.yaml 保留个性化配置。


6.3 配置热更新

在 Nacos 中创建:

1
Data ID: cart-service.yaml

内容:

1
2
3
hm:
cart:
maxAmount: 1

在微服务中读取:

1
2
3
4
5
6
@Component
@ConfigurationProperties(prefix = "hm.cart")
@Data
public class CartProperties {
private Integer maxAmount;
}

✅ 修改 Nacos 配置后无需重启即可生效!


七、动态路由(Gateway + Nacos)

静态路由写在 application.yml 中,更新需重启。
我们可以使用 Nacos 动态监听 + RouteDefinitionWriter 实现热更新。

实现步骤:

在 Nacos 添加 gateway-routes.json

1
2
3
4
5
6
7
8
9
10
[
{
"id": "item",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/items/**"}
}],
"uri": "lb://item-service"
}
]

网关中监听配置变更:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package com.hmall.gateway.route;

import cn.hutool.json.JSONUtil;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.hmall.common.utils.CollUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;

@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouteLoader {

private final RouteDefinitionWriter writer;
private final NacosConfigManager nacosConfigManager;

// 路由配置文件的id和分组
private final String dataId = "gateway-routes.json";
private final String group = "DEFAULT_GROUP";
// 保存更新过的路由id
private final Set<String> routeIds = new HashSet<>();

@PostConstruct
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);
}

private void updateConfigInfo(String configInfo) {
log.debug("监听到路由配置变更,{}", configInfo);
// 1.反序列化
List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
// 2.更新前先清空旧路由
// 2.1.清除旧路由
for (String routeId : routeIds) {
writer.delete(Mono.just(routeId)).subscribe();
}
routeIds.clear();
// 2.2.判断是否有新的路由要更新
if (CollUtils.isEmpty(routeDefinitions)) {
// 无新路由配置,直接结束
return;
}
// 3.更新路由
routeDefinitions.forEach(routeDefinition -> {
// 3.1.更新路由
writer.save(Mono.just(routeDefinition)).subscribe();
// 3.2.记录路由id,方便将来删除
routeIds.add(routeDefinition.getId());
});
}
}

修改 Nacos 配置后,几秒钟内路由自动更新,无需重启!


八、知识总结

功能 实现组件 核心技术
路由转发 Spring Cloud Gateway routes + predicates
登录鉴权 GlobalFilter + JWT 统一校验入口
用户传递 请求头 + 拦截器 UserContext
微服务间传递 Feign RequestInterceptor 自动附加请求头
配置共享 Nacos Config shared-configs
热更新 Nacos 推送机制 @ConfigurationProperties
动态路由 Nacos + RouteDefinitionWriter JSON 动态加载