Matuto的博客

Matuto的博客

马图图

岁月变迁何必不悔,尘世喧嚣怎能无愧。

25 文章数
1 评论数

微服务网关 SpringCloud - Gateway

马图图
2024-06-23 / 0 评论 / 1936 阅读 / 0 点赞

网关的作用就是负责请求的路由、转发、身份校验。

在SpringCloud当中,提供了两种网关实现方案:

  • Netflix Zuul:早期实现,已经淘汰了
  • SpringCloudGatyeway:基于Spring的WebFlux技术,完全支持响应式编程,吞吐能力更强

快速入门

创建项目-导入依赖

<!--网关-->  
<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>

配置路由

spring:  
  application:  
    name: gateway  
  cloud:  
    nacos:  
      server-addr: 192.168.10.150:8848  
    gateway:  
      routes: # 路由配置  
        - id: item # 路由id  
          uri: lb://item-service # lb 代表0负载均衡 后边是服务地址  
          predicates:  
            - Path=/items/**, /search/** # 路径匹配 也可以配置正则表达式 使用 ,分割多个路径

|名称|说明|示例| |---|---|---| |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|权重处理||

网关登录校验

网关的请求流程

不过,这里存在几个问题:

  • 网关路由是配置的,请求转发是Gateway内部代码,我们如何在转发之前做登录校验?
  • 网关校验JWT之后,如何将用户信息传递给微服务?
  • 微服务之间也会相互调用,这种调用不经过网关,又该如何传递用户信息?

路由过滤器

配置路由过滤器

gateway:  
  routes: # 路由配置  
    - id: item # 路由id  
      uri: lb://item-service # lb 代表0负载均衡 后边是服务地址  
      predicates:  
        - Path=/items/**, /search/** # 路径匹配 也可以配置正则表达式 使用 ,分割多个路径  
      filters:  
        - AddRequestHeader=X-Request-Id,${spring.application.name}-${random.uuid}

配置全局路由过滤器

gateway:  
  routes: # 路由配置  
    - id: item # 路由id  
      uri: lb://item-service # lb 代表0负载均衡 后边是服务地址  
      predicates:  
        - Path=/items/**, /search/** # 路径匹配 也可以配置正则表达式 使用 ,分割多个路径  
  default-filters:  
    - AddRequestHeader=X-Request-Id,${spring.application.name}-${random.uuid}

自定义过滤器

如图所示:

  1. 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。
  2. WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为**Filter**)。
  3. 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为prepost两部分,分别会在请求路由到微服务之前之后被执行。
  4. 只有所有Filterpre逻辑都依次顺序执行通过后,请求才会被路由到微服务。
  5. 微服务返回结果后,再倒序执行Filterpost逻辑。
  6. 最终把响应结果返回。 如图中所示,最终请求转发是有一个名为NettyRoutingFilter的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。我们需要定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到**NettyRoutingFilter**之前

网关过滤器链中的过滤器有两种:

  • GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route.
  • GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。

他们的方法签名完全一致:

/**
 * 处理请求并将其传递给下一个过滤器
 * @param exchange 当前请求的上下文,其中包含request、response等各种数据
 * @param chain 过滤器链,基于它向下传递请求
 * @return 根据返回值标记当前请求是否被完成或拦截,chain.filter(exchange)就放行了。
 */
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

FilteringWebHandler在处理请求时,会将GlobalFilter装饰为GatewayFilter,然后放到同一个过滤器链中,排序以后依次执行。

自定义GatewayFilter

@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为后缀!

使用自定义过滤器:

spring:
  cloud:
    gateway:
      default-filters:
            - PrintAny # 此处直接以自定义的GatewayFilterFactory类名称前缀类声明过滤器

自定义带参数GatewayFilter


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

}

使用

spring:
  cloud:
    gateway:
      default-filters:
            - PrintAny=1,2,3 # 注意,这里多个参数以","隔开,将来会按照shortcutFieldOrder()方法返回的参数顺序依次复制

自定义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;
    }
}

实现登录校验

编写登录校验拦截器的处理逻辑,保存用户信息到请求头中。

package com.hmall.gateway.filter;

import com.hmall.common.exception.UnauthorizedException;
import com.hmall.common.utils.CollUtils;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private final JwtTool jwtTool;

    private final AuthProperties authProperties;

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @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 (!CollUtils.isEmpty(headers)) {
            token = headers.get(0);
        }
        // 4.校验并解析token
        Long userId = null;
        try {
            userId = jwtTool.parseToken(token);
        } catch (UnauthorizedException e) {
            // 如果无效,拦截
            ServerHttpResponse response = exchange.getResponse();
            response.setRawStatusCode(401);
            return response.setComplete();
        }

        // TODO 5.如果有效,传递用户信息
        // 5.传递用户信息  
		String userInfo = userId.toString();  
		ServerWebExchange build = exchange.mutate()  
		        .request(builder -> builder.header("user-info", userInfo))  
		        .build();
        // 6.放行
        return chain.filter(build);
    }

    private boolean isExclude(String antPath) {
        for (String pathPattern : authProperties.getExcludePaths()) {
            if(antPathMatcher.match(pathPattern, antPath)){
                return true;
            }
        }
        return false;
    }

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

编写拦截器获取用户

package com.hmall.common.interceptor;

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.判断是否为空
        if (StrUtil.isNotBlank(userInfo)) {
            // 不为空,保存到ThreadLocal
                UserContext.setUser(Long.valueOf(userInfo));
        }
        // 3.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserContext.removeUser();
    }
}

拦截器想要生效需要编写 SpringMVC 的配置类,配置登录拦截器:

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());
    }
}

不过,需要注意的是,这个配置类默认是不会生效的,因为它所在的包是com.hmall.common.config,与其它微服务的扫描包不一致,无法被扫描到,因此无法生效。

基于SpringBoot的自动装配原理,我们要将其添加到resources目录下的META-INF/spring.factories文件中:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.hmall.common.config.MyBatisConfig,\
  com.hmall.common.config.MvcConfig

这里还需要注意一点,自动装配时,网关是不需要拦截器的,需要过滤掉网关,这里使用了 @ConditionalOnClass 注解,网关不是基于 SpringMVC 实现的,没有 DispatcherServlet 类。

OpenFeign 传递用户信息

以上我们完成了从网关获取用户信息,保存并传递到服务中,但是当业务复杂时,需要做服务间的调用时,就会导致后边的服务不通过网关,也就拿不到用户信息,这时就需要在服务发起调用时将用户信息存入请求头中,

Feign中提供了一个拦截器接口:feign.RequestInterceptor

public interface RequestInterceptor {

  /**
   * Called for every request. 
   * Add data using methods on the supplied {@link RequestTemplate}.
   */
  void apply(RequestTemplate template);
}

具体实现为:

@Bean
public RequestInterceptor userInfoRequestInterceptor(){
    return new RequestInterceptor() {
        @Override
        public void apply(RequestTemplate template) {
            // 获取登录用户
            Long userId = UserContext.getUser();
            if(userId == null) {
                // 如果为空则直接跳过
                return;
            }
            // 如果不为空则放入请求头中,传递给下游微服务
            template.header("user-info", userId.toString());
        }
    };
}

登录验证整体流程为: 网关 -> 过滤器获取信息并存入请求头 -> 拦截器提取信息放入 ThreadLocal -> 在服务中使用 -> 通过OpenFeign 的 RequestInterceptor 接口 在服务间进行传递。

上一篇 下一篇
评论
来首音乐
最新回复
光阴似箭
今日已经过去小时
这周已经过去
本月已经过去
今年已经过去个月
文章目录
每日一句