本篇文章基于黑马程序员的微服务课程内容,结合个人学习过程中的理解与思考进行整理。本节将围绕以下几个问题展开:什么是网关和配置管理
前面那篇文章,我们了解如何把一个单体的项目拆成分布式微服务项目,并且讲解一下各个服务之间是如何通信的。发现一些问题:
- 每个微服务都有不同的地址和端口,那么前端需要调用微服务的功能时,就会出现以下问题:
- 前端需要调用不同微服务的时候,地址和端口太多,不易维护
- 前端无法调用Nacos,当后端端口发送改变的时候,前端察觉不到
- 单体项目需要做登录、权限校验,还有为了方便用户信息传递登录时保存用户信息,那么拆分成微服务就会出现以下问题:
- 每个微服务都需要编写登录校验、用户信息保存
- 当微服务之间的调用的时候,该如何传递用户信息?
这篇文章就是解决这些问题的,也是这篇文章的主题------网关:
- 网关路由:解决前端请求路口统一的问题
- 网关鉴权:解决统一登录校验和用户信息获取问题
- 统一配置管理:解决微服务,配置文件重复的问题和配置热更新的问题
1.网关路由
1.1 认识网关
网关(Gateway) 是微服务架构中的统一入口,它位于客户端与服务端之间,接收所有外部请求,然后根据请求内容将其转发到对应的后端微服务。
可以理解为:网关是微服务系统的"前门"。

从图中可以看出,网关作为后端的一部分,承担了统一入口的作用。前端只需请求网关,不需要关心各个微服务的具体地址。网关接收到请求后,会通过访问 Nacos 注册中心获取服务的最新信息,并根据配置的路由规则将请求转发到对应服务,同时实现负载均衡和服务发现,简化了前端调用逻辑,也增强了系统的灵活性与可维护性。
1.2 快速实现
- 创建网关模块
- 引入网关的依赖
- 编写启动类
- 编写配置文件
1.3 配置文件
这里主要讲解一下配置文件
yaml
server:
port: 8080 # 网关端口,给前端的端口
spring:
application:
name: gateway-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos注册中心地址
gateway:
routes:
# 订单服务路由
- id: order_route
uri: lb://order-service # 负载均衡到订单服务
predicates:
- Path=/order/** # 匹配/order开头的请求
- Method=GET,POST # 允许GET/POST请求
filters:
- StripPrefix=1 # 移除第一段路径(/order)
- AddRequestHeader=Gateway,true # 添加请求头
# 用户服务路由
- id: user_route
uri: lb://user-service
predicates:
- Path=/user/** # 匹配/user路径
- After=2025-01-01T00:00:00.000+08:00 # 时间生效范围
filters:
- PrefixPath=/api # 添加前缀 /api/user/**
这里我们重点关注predicates
,也就是路由断言。SpringCloudGateway中支持的断言类型有很多:
名称 | 说明 | 示例 |
---|---|---|
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 | 权重处理 |
2.网关登录校验
2.1 思路分析(JWT为例)

我们看到这张图网关需要完成的工作为:
- JWT信息校验
- 将用户信息传递到下游
- 然后微服务之间传递用户信息
2.2 网关过滤器
首先,登录校验一定要放在转发路由之前去完成,因此我们需要先了解网关是如何进行工作的。

- 接受前端的请求,
HandlerMapper
,进行路由匹配 - 然后经过一系列的过滤器链
- 路径处理过滤器
- 添加信息头处理器
- 自定义处理器
- 等等
- 路由转发到相应的微服务中
- 微服务返回结果再次经过过处理器链
我们需要做的就是在转发到微服务之前进行登录校验,就是自定义一个网关过滤器。
网关过滤器链中的过滤器有两种:
GatewayFilter
: 路由过滤器,作用范围比较灵活,可以是任意指定的路由RouteGlobalFilter
:全局过滤器,作用范围是所有路由,不可配置。
如何理解这个不可配置是什么意思? 意思就是不可以通过配置文件动态配置
2.2.1 基于全局过滤器实现登录校验
我们就自定义一个基于全局过滤器的登录校验过滤器
1. 创建自定义全局过滤器
我们创建一个LoginCheckGlobalFilter
类,实现GlobalFilter
和Ordered
接口:
java
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class LoginCheckGlobalFilter implements GlobalFilter, Ordered {
// 白名单路径(不需要登录校验的路径)
private static final List<String> WHITE_LIST = Arrays.asList(
"/api/user/login",
"/api/user/register",
"/api/doc.html",
"/api/swagger-ui.html"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取请求路径
String path = exchange.getRequest().getPath().toString();
// 2. 检查是否在白名单中
if (WHITE_LIST.contains(path)) {
return chain.filter(exchange); // 直接放行
}
// 3. 获取token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
// 4. 校验token
if (StringUtils.isBlank(token) || !validateToken(token)) {
// 未登录或token无效
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 5. token有效,放行请求
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1; // 设置过滤器执行顺序(数值越小优先级越高)
}
// 模拟token校验方法(实际项目中应调用认证服务)
private boolean validateToken(String token) {
// 这里简单模拟校验逻辑
return token != null && token.startsWith("Bearer ");
}
}
2. 过滤器逻辑说明
- 白名单检查:放行登录、注册等不需要认证的接口
- Token获取 :从请求头
Authorization
中获取JWT token - Token校验 :
- 无效token:返回401 Unauthorized
- 有效token:放行请求
2.3 微服务直接如何传递用户信息
现在,网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时,微服务又该如何获取用户身份呢?
由于网关发送请求到微服务依然采用的是Http
请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用SpringMVC的拦截器来实现登录用户信息获取,并存入ThreadLocal,方便后续使用。
大致流程
首先每一次请求可以看做一个线程,之前在单体项目的时候。经过登录校验的时候,把信息放入线程的上下文中。我微服务项目也可以根据这个方法,进行改造一下:
- 网关 → 微服务 :通过 HTTP请求头 传递用户信息(如JWT解析后的用户ID)。
- 微服务内部 :通过 SpringMVC拦截器 + ThreadLocal 存储用户信息,避免重复解析。
- 微服务间调用(OpenFeign) :通过 Feign拦截器 透传用户信息,确保链路完整。
具体步骤
1. 网关传递用户信息到微服务
网关(如Spring Cloud Gateway) :在登录校验后,将用户信息(如userId
、username
)添加到请求头中,转发到下游微服务。
yaml
# 网关配置示例(Spring Cloud Gateway)
spring:
cloud:
gateway:
default-filters:
- name: AddRequestHeader
args:
name: X-User-Id
value: "#{@userContext.getUserId()}" # 从JWT解析后注入
2. 微服务接收并存储用户信息
2.1 定义ThreadLocal上下文
java
public class UserContext {
private static final ThreadLocal<Long> userIdHolder = new ThreadLocal<>();
public static void setUserId(Long userId) {
userIdHolder.set(userId);
}
public static Long getUserId() {
return userIdHolder.get();
}
public static void clear() {
userIdHolder.remove();
}
}
2.2 拦截器解析请求头
java
@Component
public class UserInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String userId = request.getHeader("X-User-Id");
if (userId != null) {
UserContext.setUserId(Long.valueOf(userId));
}
return true;
}
@Override
public void afterCompletion(...) {
UserContext.clear(); // 避免内存泄漏
}
}
2.3 注册拦截器
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserInterceptor userInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userInterceptor);
}
}
3. OpenFeign透传用户信息
微服务间调用时,需通过Feign拦截器将用户信息附加到请求头,确保链路透明。
3.1 Feign拦截器实现
java
复制
java
@Component
public class FeignUserInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
Long userId = UserContext.getUserId();
if (userId != null) {
template.header("X-User-Id", String.valueOf(userId));
}
}
}
3.2 启用Feign拦截器
确保Feign客户端扫描到该拦截器(通常已自动注入)。
4. 完整流程总结
- 用户请求 → 网关:携带JWT令牌。
- 网关 :解析JWT → 将
userId
放入请求头 → 转发到微服务A。 - 微服务A :
- 拦截器读取
X-User-Id
→ 存入UserContext
(ThreadLocal)。 - 业务代码通过
UserContext.getUserId()
获取用户信息。
- 拦截器读取
- 微服务A → 微服务B (通过OpenFeign):
- Feign拦截器自动附加
X-User-Id
到请求头。
- Feign拦截器自动附加
- 微服务B:重复步骤3的流程。
3. 配置管理
我们已经解决了微服务间的通信、注册发现、路由、登录鉴权 等核心问题,但还有三大痛点悬而未决:
- 网关路由硬编码 :路由规则写在
application.yml
里,改个路径就得重启网关? - 业务配置写死 :数据库连接、业务开关、限流阈值全在代码里,调个参数就得重新打包部署?
- 重复配置爆炸 :每个微服务都复制粘贴Redis、MySQL、日志配置,一改全改,维护成本飙升?
解决方案:统一配置中心
用Nacos/Apollo 等配置中心,把所有配置集中管理,实现:
-
动态路由 :网关路由规则放到配置中心,热更新无需重启。
yaml# Nacos中动态配置网关路由 spring: cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path=/user/** # 修改后立即生效
-
业务配置动态化 :用
@RefreshScope
注解,运行时刷新配置。java@Component @RefreshScope public class DynamicConfig { @Value("${order.timeout:30}") // 配置中心修改后自动更新 private Long timeout; }
-
共享配置模板:
- 通用配置 (如Redis、MySQL)抽成
shared-config.yml
,所有微服务复用。 - 差异化配置(如端口、服务名)独立维护,避免冗余。
- 通用配置 (如Redis、MySQL)抽成