Spring Boot微服务架构中的网关路由
想象一下你到了一个大型购物中心,这个购物中心有很多商店,每个商店都有自己专门销售的商品。现在,购物中心里有一些信息服务台,这就像是微服务架构中的网关
。
你作为客户,想要购买不同的商品,可能需要分别前往电器、服装和书籍等不同的商店。如果没有指引,你可能要走很多冤枉路才能找到你需要的商店。这时,信息服务台(网关
)就派上了用场。你只需要告诉信息服务台你的需求,服务台就会告诉你各个商店的位置,并指引你如何快速到达。更棒的是,服务台还会告诉你哪些商店正在搞促销,帮你找到性价比最高的商品。
在Spring Boot微服务架构中,网关
起着类似信息服务台的作用。每一个微服务就像一个个独立的商店,它们处理着不同类型的业务需求。而网关
则负责将外部请求引导到正确的服务(商店)上。当客户端(用户)发出一个请求时,不需要直接与众多分散的微服务通信。相反,它们只需要与网关交互,网关就像一个智能路由器,知道如何将请求转发到正确的服务上。
这样做的好处是:
-
简化客户端操作:就像你不需要记住商场内所有商店的具体位置,客户端也不需要了解后端服务的具体地址。
-
统一入口:服务台提供了购物中心的统一访问点,同样,网关提供了一个统一的接口给客户,无论背后有多少服务,对外呈现的都是统一的门面。
-
安全与隔离:服务台能够阻止非顾客进入,网关也可以防止未授权的访问,确保只有合适的请求被路由到服务端。
-
流量控制和监控:就如同服务台可以根据客流量调整商场的运营,网关可以监控流量并实施流控策略,如限流和断路。
所以,在Spring Boot微服务架构中,网关就好比是购物中心的信息服务台,是客户与商店(微服务)之间的桥梁,既方便了顾客,也让商场的运营更加高效。
配置一个基础的网关微服务
1、创建项目,引入依赖
<!--网关-->
<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>
2、通过项目的配置文件配置路由
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/**,/search/** # 这里是以请求路径作为判断规则
- id: cart
uri: lb://cart-service
predicates:
- Path=/carts/**
- id: user
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
- id: trade
uri: lb://trade-service
predicates:
- Path=/orders/**
- id: pay
uri: lb://pay-service
predicates:
- Path=/pay-orders/**
登录校验和传递信息
到这里配置其实就完成了,但是还可以在次基础之上添加网关微服务更多的功能,比如把登录校验放在网关,这样就不需要所有其它微服务都去做一个微服务了。基于jwt的登录校验,下游服务可能需要jwt密钥里的某些信息,这个时候也可以通过网关去完成转发。
不过,这里存在几个问题:
-
网关路由是配置的,请求转发是Gateway内部代码,我们如何在转发之前做登录校验?
-
网关校验JWT之后,如何将用户信息传递给微服务?
-
微服务之间也会相互调用,这种调用不经过网关,又该如何传递用户信息?
网关过滤器
登录校验必须在请求转发到微服务之前做,否则就失去了意义。而网关的请求转发是Gateway
内部代码实现的,要想在请求转发之前做登录校验,就必须了解Gateway
内部工作的基本原理。
如图所示:
-
客户端请求进入网关后由
HandlerMapping
对请求做判断,找到与当前请求匹配的路由规则(Route
),然后将请求交给WebHandler
去处理。 -
WebHandler
则会加载当前路由下需要执行的过滤器链(Filter chain
),然后按照顺序逐一执行过滤器(后面称为Filter
)。 -
图中
Filter
被虚线分为左右两部分,是因为Filter
内部的逻辑分为pre
和post
两部分,分别会在请求路由到微服务之前和之后被执行。 -
只有所有
Filter
的pre
逻辑都依次顺序执行通过后,请求才会被路由到微服务。 -
微服务返回结果后,再倒序执行
Filter
的post
逻辑。 -
最终把响应结果返回。
如图中所示,最终请求转发是有一个名为NettyRoutingFilter
的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到NettyRoutingFilter
之前,这就符合我们的需求了!
网关过滤器链中的过滤器有两种:
-
GatewayFilter
:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route
. -
GlobalFilter
:全局过滤器,作用范围是所有路由,不可配置。
其实GatewayFilter
和GlobalFilter
这两种过滤器的方法签名完全一致,FilteringWebHandler
在处理请求时,会将GlobalFilter
装饰为GatewayFilter
,然后放到同一个过滤器链中,排序以后依次执行。Gateway
中内置了很多的GatewayFilter
,详情可以参考官方文档:Spring Cloud Gateway
注意:过滤器链之外还有一种过滤器,HttpHeadersFilter,用来处理传递到下游微服务的请求头。例如org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter可以传递代理请求原本的host头到下游微服务。
实现自定义HttpHeadersFilter
:
实现HttpHeadersFilter
接口:创建一个类实现HttpHeadersFilter
接口,并实现filter
方法来执行你的头信息处理逻辑。
import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter;
import org.springframework.http.HttpHeaders;
import org.springframework.web.server.ServerWebExchange;
public class CustomHttpHeadersFilter implements HttpHeadersFilter {
@Override
public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) {
HttpHeaders headers = new HttpHeaders();
headers.putAll(input);
// 在这里修改headers,例如添加一个自定义头
headers.add("X-Custom-Header", "Custom Value");
return headers;
}
}
注册HttpHeadersFilter
:你可以在配置文件中或者通过代码注册自定义的HttpHeadersFilter
。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GatewayConfig {
@Bean
public HttpHeadersFilter customHttpHeadersFilter() {
return new CustomHttpHeadersFilter();
}
}
注册后,CustomHttpHeadersFilter
就会作为HttpHeadersFilter
启动,并在请求到达下游服务之前对HTTP头进行处理。该处理步骤通常在请求被发送到下游服务之前,较晚的过滤器执行阶段之后发生。
实现自定义GatewayFilter
:
自定义GatewayFilter
不是直接实现GatewayFilter
,而是实现AbstractGatewayFilterFactory
。最简单的方式是这样的:
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求
ServerHttpRequest request = exchange.getRequest();
// 编写过滤器逻辑
System.out.println("过滤器执行了");
// 放行
return chain.filter(exchange);
}
};
}
}
注意:该类的名称一定要以
GatewayFilterFactory
为后缀!
然后在yaml配置中这样使用:
spring:
cloud:
gateway:
default-filters:
- PrintAny # 此处直接以自定义的GatewayFilterFactory类名称前缀类声明过滤器
另外,这种过滤器还可以支持动态配置参数,不过实现起来比较复杂,示例:
@Component
public class PrintAnyGatewayFilterFactory // 父类泛型是内部类的Config类型
extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
@Override
public GatewayFilter apply(Config config) {
// OrderedGatewayFilter是GatewayFilter的子类,包含两个参数:
// - GatewayFilter:过滤器
// - int order值:值越小,过滤器执行优先级越高
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取config值
String a = config.getA();
String b = config.getB();
String c = config.getC();
// 编写过滤器逻辑
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
// 放行
return chain.filter(exchange);
}
}, 100);
}
// 自定义配置属性,成员变量名称很重要,下面会用到
@Data
static class Config{
private String a;
private String b;
private String c;
}
// 将变量名称依次返回,顺序很重要,将来读取参数时需要按顺序获取
@Override
public List<String> shortcutFieldOrder() {
return List.of("a", "b", "c");
}
// 返回当前配置类的类型,也就是内部的Config
@Override
public Class<Config> getConfigClass() {
return Config.class;
}
}
然后在yaml文件中使用:
spring:
cloud:
gateway:
default-filters:
- PrintAny=1,2,3 # 注意,这里多个参数以","隔开,将来会按照shortcutFieldOrder()方法返回的参数顺序依次复制
上面这种配置方式参数必须严格按照shortcutFieldOrder()方法的返回参数名顺序来赋值。
还有一种用法,无需按照这个顺序,就是手动指定参数名:
spring:
cloud:
gateway:
default-filters:
- name: PrintAny
args: # 手动指定参数名,无需按照参数顺序
a: 1
b: 2
c: 3
实现自定义GlobalFilter:
自定义GlobalFilter则简单很多,直接实现GlobalFilter即可,而且也无法设置动态参数:
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 编写过滤器逻辑
System.out.println("未登录,无法访问");
// 放行
// return chain.filter(exchange);
// 拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}
@Override
public int getOrder() {
// 过滤器执行顺序,值越小,优先级越高
return 0;
}
}
登录校验示例(使用GlobalFilter拦截器):
package com.hmall.gateway.filters;
import cn.hutool.core.text.AntPathMatcher;
import com.hmall.common.exception.UnauthorizedException;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
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.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final AuthProperties authProperties;
private final JwtTool jwtTool;
private final AntPathMatcher antPathMatcher = new AntPathMatcher(); //AntPath匹配器
@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(headers != null && !headers.isEmpty()){
token = headers.get(0);
}
//4、校验并解析token
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
//拦截,设置响应状态码为401
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//传递用户信息
String userInfo = userId.toString();
ServerWebExchange swe = exchange.mutate()
.request(builder -> builder.header("user-info", userInfo))
.build();
//6、放行
return chain.filter(swe);
}
private boolean isExclude(String path) {
for (String pathPattern : authProperties.getExcludePaths()) {
if (antPathMatcher.match(pathPattern,path)) {
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
微服务获取用户
现在,网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时,微服务又该如何获取用户身份呢?
定义拦截器获取,最好将拦截器定义到公共服务中(这样下游服务之间引入公共服务依赖就可以了):
package com.hmall.common.interceptors;
import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1、获取登录用户信息
String userInfo = request.getHeader("user-info");
//2、判断是否获取到用户,如果有,存入Thread Local
if(StrUtil.isNotBlank(userInfo)){
UserContext.setUser(Long.valueOf(userInfo));
}
//3、放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清理用户
UserContext.removeUser();
}
}
注册拦截器:
package com.hmall.common.config;
import com.hmall.common.interceptors.UserInfoInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
不过,需要注意的是,这个配置类默认是不会生效的,因为它所在位置无法被其它微服务扫描到。基于SpringBoot的自动装配原理,我们要将其添加到resources
目录下的META-INF/spring.factories
文件中:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MvcConfig
现在,下游服务便可以从ThreadLocal中获得信息,但当微服务通过OpenFeign互相调用时,是没有传递用户信息的,这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor来在请求头之中添加信息。
我们只需要实现这个接口,然后实现apply方法,利用RequestTemplate
类来添加请求头,将用户信息保存到请求头中。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。
package com.hmall.api.config;
import com.hmall.common.utils.UserContext;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
Long userId = UserContext.getUser();
if(userId != null){
requestTemplate.header("user-info", userId.toString());
}
}
};
}
}