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 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 的请求处理流程:
- HandlerMapping 匹配路由;
- WebHandler 执行过滤器链;
- 每个 Filter 执行 pre/post 两段逻辑;
- 只有 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();
if (isExclude(request.getPath().toString())) return chain.filter(exchange);
String token = request.getHeaders().getFirst("authorization");
Long userId; try { userId = jwtTool.parseToken(token); } catch (UnauthorizedException e) { exchange.getResponse().setRawStatusCode(401); return exchange.getResponse().setComplete(); }
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;
private final String dataId = "gateway-routes.json"; private final String group = "DEFAULT_GROUP"; private final Set<String> routeIds = new HashSet<>();
@PostConstruct public void initRouteConfigListener() throws NacosException { String configInfo = nacosConfigManager.getConfigService() .getConfigAndSignListener(dataId, group, 5000, new Listener() { @Override public Executor getExecutor() { return null; }
@Override public void receiveConfigInfo(String configInfo) { updateConfigInfo(configInfo); } }); updateConfigInfo(configInfo); }
private void updateConfigInfo(String configInfo) { log.debug("监听到路由配置变更,{}", configInfo); List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class); for (String routeId : routeIds) { writer.delete(Mono.just(routeId)).subscribe(); } routeIds.clear(); if (CollUtils.isEmpty(routeDefinitions)) { return; } routeDefinitions.forEach(routeDefinition -> { writer.save(Mono.just(routeDefinition)).subscribe(); routeIds.add(routeDefinition.getId()); }); } }
|
修改 Nacos 配置后,几秒钟内路由自动更新,无需重启!
八、知识总结
| 功能 |
实现组件 |
核心技术 |
| 路由转发 |
Spring Cloud Gateway |
routes + predicates |
| 登录鉴权 |
GlobalFilter + JWT |
统一校验入口 |
| 用户传递 |
请求头 + 拦截器 |
UserContext |
| 微服务间传递 |
Feign RequestInterceptor |
自动附加请求头 |
| 配置共享 |
Nacos Config |
shared-configs |
| 热更新 |
Nacos 推送机制 |
@ConfigurationProperties |
| 动态路由 |
Nacos + RouteDefinitionWriter |
JSON 动态加载 |