在单体架构中,用户登录校验通常只需编写一个拦截器(Interceptor)即可统一处理。同时,用户上下文信息(如用户 ID、角色等)可以方便地通过 ThreadLocal 在整个请求生命周期内进行存储和共享。
然而,当系统演进为微服务架构后,原有的方案面临两个关键挑战:
- 认证逻辑重复:每个微服务都需要独立实现登录校验逻辑,导致代码冗余、维护成本上升;
- 上下文传递困难:由于服务之间相互隔离,ThreadLocal无法跨服务共享用户信息,传统的线程局部变量机制不再适用。
为解决上述问题,通常引入 **API 网关(Gateway)**作为统一入口,在网关层集中完成身份认证与鉴权,并将用户上下文信息(如用户 ID、权限标识等)以标准化方式(例如通过 HTTP 请求头)透传给下游微服务。各微服务在接收到请求后,可从请求头中解析并重建本地上下文(如重新设置 ThreadLocal),从而在保持服务解耦的同时,实现安全、高效的身份传递与使用。
1. 快速入门
1. 创建项目
单独创建一个独立的模块 gateway-service用于管理网关相关内容。
2. 引入依赖
XML
<dependencies>
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos discovery-->
<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>
</dependencies>
3. 创建启动类
创建正常的项目启动类即可
java
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
4. 配置路由
bash
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1}:8848 # 建议使用环境变量提升灵活性
gateway:
routes:
- id: test # 路由 ID,需全局唯一
uri: lb://test-service # 目标服务地址,`lb://` 表示启用负载均衡,从注册中心动态获取服务实例
predicates:
- Path=/test/** # 路径匹配规则:所有以 `/test/` 开头的请求将被路由到 `test-service`
当请求到达网关时,Spring Cloud Gateway 会根据配置的 路由断言(Predicates) 判断是否匹配当前路由规则。若请求路径满足 /test/**,网关将:
- 通过服务名 text-service 从 Nacos 注册中心获取可用的服务实例列表;
- 基于内置的负载均衡策略(默认为 Ribbon 或 Spring Cloud LoadBalancer)选择一个具体实例;
- 将原始请求转发至该实例,并保留原始路径(如 /test/hello 会被完整转发);
- 下游微服务处理请求后,响应结果经由网关返回给客户端。
2. 网关登录校验
在微服务架构中,API 网关作为系统的统一入口,非常适合承担身份认证与权限校验的职责。通过将登录校验逻辑前置到网关层,可以实现以下优势:
- 集中管控:所有请求必须先经过网关,避免在每个微服务中重复实现认证逻辑;
- 安全统一:一旦校验失败,可直接在网关拦截请求,无需将无效请求转发至下游服务;
- 上下文透传:认证成功后,可将用户关键信息(如用户 ID、角色、租户标识等)封装到 HTTP 请求头中(例如 X-User-Id、X-User-Roles),随请求一起转发给目标微服务。
下游微服务在接收到请求后,可从请求头中提取用户信息,并将其存入本地的 ThreadLocal 上下文中。这样,在当前请求处理链路中的任意位置(如 Service、DAO 层)都能便捷、高效地访问用户上下文,同时保持服务间的解耦。
1. 网关过滤器
在网管实现过滤器有两种方法:
- GatewayFilter
- GlobalFilter
2. 自定义拦截器
1. 自定义 GatewayFilter 拦截器
在 Spring Cloud Gateway 中,可以通过继承 AbstractGatewayFilterFactory实现自定义的 局部过滤器(GatewayFilter) 。该过滤器可通过 YAML 配置灵活应用于全部路由 或特定路由。
java
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
// 获取请求对象
ServerHttpRequest request = exchange.getRequest();
// 执行自定义逻辑
System.out.println("【PrintAny 过滤器】请求路径: " + request.getURI().getPath());
// 继续执行后续过滤器和目标服务
return chain.filter(exchange);
};
}
}
命名规范说明 :
Spring Cloud Gateway 会根据配置中的名称(如 PrintAny)自动查找对应的 PrintAnyGatewayFilterFactoryBean。因此,类名必须以 GatewayFilterFactory 结尾,配置中只需使用前缀部分。
应用方式有两种:
1. 应用方式一:全局生效(作用于所有路由)
java
spring:
cloud:
gateway:
default-filters:
- PrintAny
2. 应用方式二:仅作用于特定路由
java
spring:
cloud:
gateway:
routes:
- id: test_route
uri: lb://test-service
predicates:
- Path=/test/**
filters:
- PrintAny
这样拦截器就只会作用于一个路由下
2. 自定义 GlobalFilter 拦截器
在 Spring Cloud Gateway 中,GlobalFilter 用于实现对所有路由统一生效 的过滤逻辑(如身份认证、日志记录、限流等)。与 GatewayFilter不同,GlobalFilter无需在配置文件中声明,只需将其注册为 Spring Bean,即可在网关启动后自动生效。
java
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求信息
ServerHttpRequest request = exchange.getRequest();
System.out.println("【GlobalFilter】请求路径: " + request.getURI().getPath());
// 继续执行后续过滤器及目标服务
return chain.filter(exchange);
}
@Override
public int getOrder() {
// 定义过滤器执行顺序:数值越小,优先级越高
return 0;
}
}
关键说明:
- 自动生效 :只要类上标注了 @Component(或其他方式注册为 Spring Bean),Spring Cloud Gateway 会自动发现并将其加入全局过滤器链,无需任何 YAML 配置。
- 作用范围 :对所有进入网关的请求生效,无论其匹配哪个路由。
- 执行顺序 :通过实现 Ordered 接口的 getOrder() 方法控制。若需在内置过滤器(如路由转发)之前执行,应返回较小的值(例如
-1或 HIGHEST_PRECEDENCE)。
⚠️ 注意:GlobalFilter 无法像 GatewayFilter 那样"仅对部分路由生效"。如果需要条件性处理(如跳过某些路径),必须在 filter() 方法内部通过exchange.getRequest().getPath() 等方式自行判断。
3. 登录校验
在微服务架构中,用户认证应集中在 API 网关完成,而各业务微服务只需信任网关传递的用户身份信息。这既提升了安全性,又避免了重复鉴权逻辑。
整体流程如下:
- 网关拦截请求,校验 JWT Token;
- 若有效,将用户 ID 注入请求头(如
user-info)并转发; - 下游微服务通过拦截器提取该 Header,重建本地用户上下文。
1. 编写拦截代码
java
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {
// 构建与解析 JWT 令牌的工具
private final JwtTool jwtTool;
// 存放排除的路径
private final AuthProperties authProperties;
// 用来解析路径的工具
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取Request
ServerHttpRequest request = exchange.getRequest();
/**
这里可以判断请求路径是否可以跳过
if(isExclude(request.getPath().toString())){
// 无需拦截,直接放行
return chain.filter(exchange);
}
**/
// 获取请求头中的token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if (!CollUtils.isEmpty(headers)) {
token = headers.get(0);
}
// 校验并解析token
Long userId = null;
try {
// 自定义 Token 工具进行校验,与单体 Token 工具类似,可自行编写
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
// 如果无效,拦截
ServerHttpResponse response = exchange.getResponse();
// 抛出 401 状态码
response.setRawStatusCode(401);
return response.setComplete();
}
/**
如果有效,将用户信息封装在请求头中
**/
String userInfo = userId.toString();
ServerWebExchange build = exchange.mutate()
.request(b -> b.header("user-info", userInfo))
.build();
// 放行
return chain.filter(exchange);
}
// 判断是否是排除路径
private boolean isExclude(String antPath) {
/**
authProperties 假设这个集合中存放的是需要排除的路径
例如:/users/login 等
建议将这些排除路径卸载配置文件中便于后期管理
**/
for (String pathPattern : authProperties.getExcludePaths()) {
if(antPathMatcher.match(pathPattern, antPath)){
return true;
}
}
return false;
}
/**
* 设置过滤器优先级:值越小,执行越早。
* 此处设为 0,确保在路由、重写等默认过滤器之前执行。
*/
@Override
public int getOrder() {
return 0;
}
}
2. 获取用户信息
当通过网关时会解析请请求头中 token 信息,并将解析后的用户数据存放在请求头中,传递给相应的实例,那么在每个微服务中就只需要拦截请求,然后从请求头中获取用户数据即可。
由于每个业务微服务都需要从请求头中提取用户信息,可将此逻辑封装到公共模块(如 common-starter) 中复用
java
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求头中的信息
String userInfo = request.getHeader("user-info");
// 将信息保存在 ThreadLocal 中
if(!StrUtil.isEmptyIfStr(userInfo)){
UserContext.setUser(Long.valueOf(userInfo));
}
// 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 请求结束,清理 ThreadLocal,防止内存泄漏
UserContext.removeUser();
}
}
说明:
- UserContext 是基于 ThreadLocal 的工具类,用于在线程内共享用户 ID;
- afterCompletion 确保每次请求结束后清理上下文。
在定义完拦截器之后需要注册,因为所有请求都需拦截判断请求头中是否存在用户信息,所以就是最基础的注册代码:
java
@Configuration
// 仅当应用是基于 Servlet(即普通微服务)时才加载此配置
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
为什么加 @ConditionalOnClass(DispatcherServlet.class)?
Spring Cloud Gateway 基于 WebFlux(响应式),不包含 DispatcherServlet;
普通微服务基于 Spring MVC(Servlet 阻塞模型),包含 DispatcherServlet;
因此,该条件能自动排除网关服务,避免在非 MVC 环境中错误加载拦截器。
⚠️ 重要前提:网关项目不得引入 spring-boot-starter-web,否则会导致条件误判。
4. 微服务之间传递用户信息
在微服务架构中,当一个服务(如 order-service)通过 Feign 客户端 调用另一个服务(如 user-service)时,若需将当前登录用户的身份信息传递给被调用方,就需要在 Feign 调用链路中自动注入用户上下文。
由于所有 @FeignClient 接口通常集中定义在 公共 API 模块(如 api-service 或 xxx-api) 中,我们可以在该模块中统一注册一个 Feign 请求拦截器(RequestInterceptor),实现用户信息的自动透传。
java
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 从本地线程上下文中获取当前用户 ID
Long userId = UserContext.getUser();
if (userId == null) {
// 若无用户信息(如系统内部调用),则跳过
return;
}
// 将用户 ID 添加到请求头,供下游服务使用
template.header("user-info", userId.toString());
}
};
}
通过以上代码,在微服务之间发起请求也会将用户信息添加在请求投中,方便被调用服务获取用户信息。
说明:
UserContext.getUser() 从 ThreadLocal 中获取当前请求绑定的用户 ID(通常由前置拦截器或网关注入);
template.header(...) 会向 Feign 发起的 HTTP 请求中添加自定义 Header;
该拦截器对所有 Feign 客户端生效,无需逐个配置。
5. 总结
以上内容的主要流程:
java
客户端
│
▼
[API 网关]
├─ 1. 验证 JWT Token
├─ 2. 提取 userId
├─ 3. 从 Nacos 查找 order-service 实例
└─ 4. 转发请求 + Header: user-info=12345
│
▼
[order-service]
├─ 5. 拦截器解析 user-info → UserContext
└─ 6. 通过 Feign 调用 user-service
│
▼
[Feign 拦截器]
└─ 7. 自动注入 user-info=12345 到请求头
│
▼
[user-service]
└─ 8. 拦截器重建 UserContext