Bootstrap

SpringCloud Gateway 学习笔记

SpringCloud Gateway 学习笔记

作者:王珂

邮箱:[email protected]



前言

本文主要记录总结在使用SpringCloud组件Gateway时需要注意的点,方便在日后使用查询参考。

SpringBoot版本:2.7.15

SpringCloud版本:2021.0.5

SpringCloudAlibaba版本:2021.0.5.0

一、Gateway的基础组成

1.1 路由定义Route

一个Route模块由以下几个模块组成

  1. id

一个route定义一个为一个的id

  1. uri

route的目标地址

  1. predicate

使用predicate匹配http请求的任何内容

  1. predicate

  2. filter

示例:

spring:
  cloud:
    gateway:
      routes: // 所有的route都定义在routes下
        - id: item // route的id,唯一标识
          uri: lb://item-service // 路由目标服务,lb代表负载均衡
          predicates: // 所有predicate都定义在predicates下
            - Path=/items/** 以请求路径做为判断
        - id: xxx
          uri: lb://xxx
          predicates:
             xxx

网关路由对应的Java类型是RouteDefinition,其常见的属性有:

  • id 路由唯一标识

  • uri 路由目标地址

  • predicates 路由断言,判断请求是否符合当前路由

  • filters 路由过滤器,对请求或响应做特殊处理

1.2 路由断言Predicate

Spring提供了12种基本的RoutePredicateFactory实现:

名称说明示例
After某个时间点之后的请求- After=2017-01-20T17:42:47.789-07:00[America/Denver]
Before某个时间点之前的请求- Before=2017-01-20T17:42:47.789-07:00[America/Denver]
Between某两个时间点之间的请求- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
Cookies请求必须包含某些Cookies- 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/{segment}
Query请求参数必须包含指定的参数- Query=green
RemoteAddr请求的IP必须是指定范围- RemoteAddr=192.168.1.1/24
Weight权重处理- Weight=group1, 2
XForwarded Remote Addr基于请求的来源IP做判断- XForwardedRemoteAddr=192.168.1.1/24

1.3 路由过滤器Filter

网关过滤器有两种,分别是

  • GatewayFilter: 路由过滤器,作用于任意指定的的路由,默认不生效,需要配置到路由后生效。

    这种过滤器也可以配置在default-filters下当作全局过滤器。

    网关提供了33种这种过滤器,如下:

名称说明示例
AddRequestHeader给当前请求添加一个请求头AddRequestHeader=headerName,headerValue
RemoveRequestHeader移除请求种的一个请求头RemoveRequestHeader=headerName
AddResponseHeader给响应结构中添加一个响应头AddResponseHeader=headerName,headerValue
RemoveResponseHeader移除响应结果中一个响应头RemoveResponseHeader=headerName
RewritePath请求路径重新RewritePath=/red/?(?.*),/${segment}
StripPrefix去除请求路径中N段前缀StripPrefix=1 // 则路径/a/b转发时只保留/b
spring:
  cloud:
    gateway:
      routes:
        id: xxx
        uri: xxx
        predicates:
          xxx
        filters:
          - AddRequestHeader=xxx # GatewayFilter类型的过滤器
  • GlobalFilter: 全局过滤器,作用范围是所有路由,声明后自动生效

默认过滤器 default-filters,对所有的过滤器都生效

spring:
  cloud:
    gateway:
      routes:
        ...
      default-filters: # 默认过滤器,对所有过滤器都生效
        - AddRequestHeader=name, wangke

二、过滤器的使用

2.1 登录校验过滤器

网关登录校验需要关注的几个问题

1)如何在网关转发之前做登录校验?

2)网关如何将用户信息传递给微服务?

3)微服务之间如何传递用户信息?

在这里插入图片描述

@Component
public class TokenGlobalFilter implements GlobalFilter, Ordered {

    /**
     * 
     * @param exchange 网关内部的上下文,保存网关内部的共享数据(如:request, response)
     * @param chain 过滤器链
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 判断token有效性 ...
        // 放行过滤器(使用了责任链模式)
        return chain.filter(exchange);
    }

    /**
     * 设置过滤器的优先级(值越大优先级月低)
     * @return -1
     */
    @Override
    public int getOrder() {
        return -1;
    }
}

在这里插入图片描述
解析Token

网关传递用户信息

在网关登录校验过滤器中,从Token中解析出正确的用户信息,将其保存在Http请求头中向下传递。

要修改转发的微服务请求,需要用到ServerWebExchange类提供的API

// 传递用户信息
String userInfo = "省略...";
ServerWebExchange serverWebExchange = exchange.mutate() // mutate就是对下游请求做修改
        .request(builder -> builder.header("userInfo", userInfo))
        .build();

用户拦截器

因为在每个微服务中都需要获取登录的用户信息,因此可以直接在common模块中定义需要的拦截器,其它微服务引用即可。

UserContext工具类

/**
 * 用户上下文
 *
 * @author Ke Wang
 * @since 2024-06-25
 */
public class UserContext {

    private static final ThreadLocal<String> tl = new ThreadLocal<>();

    /**
     * 保存当前用户信息到ThreadLocal
     * @param userId
     */
    public static void setUser(String userId) {
        tl.set(userId);
    }

    /**
     * 获取当前登录的用户信息
     * @return userId
     */
    public static String getUser() {
        return tl.get();
    }

    /**
     * 移除当前登录的用户信息
     */
    public static void  removeUser() {
        tl.remove();
    }

}

拦截器类

public class UserInfoInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        // 1.获取登录的用户信息
        String userInfo = request.getHeader("userInfo");

        // 2.如果获取到了用户,则存入ThreadLocal
        if (!StringUtils.isEmpty(userInfo)) {
            UserContext.setUser(userInfo);
        }

        // 3. 放行
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler, ModelAndView modelAndView) throws Exception {
        // 当请求处理完成之后清理用户信息
        UserContext.removeUser();
    }
}

拦截器配置类

@Configuration
// 对于SpringMVC该配置类才生效(Gateway是基于Webflux的,对其不会起作用)
@ConditionalOnClass(DispatcherServlet.class)
public class CommonWebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserInfoInterceptor());
    }

}

OpenFeign传递用户信息

微服务之间的调用是通过OpenFeign来实现的。假如微服务A调用微服务B,微服务A中的用户信息是从网关的请求头中获取的,当调用微服务B时,也是需要将用户信息通过请求头传递给微服务B的。

OpenFeign中提供了一个拦截器接口,所有由OpenFeign发起的请求都会先调用拦截器处理请求。

public interface RequestInterceptor {
    /**
     * 在每次发起请求前调用,template可以对请求头进行修改
     */
    void apply(RequestTemplate template);
}

示例:

@Component
public class TokenFeignClientInterceptor implements RequestInterceptor {

    /**
     * OpenFeigin拦截器,再每次OpenFeign发起请求前进行拦截
     * @param requestTemplate 请求模板
     */
    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 添加token
        requestTemplate.header("Authorization", request.getHeader("Authorization"));

        // 添加userinfo
        requestTemplate.header("userInfo", UserContext.getUser());
    }

}

2.2 自定义GatewayFilter

自定义GatewayFilter不是直接实现GatewayFilter,而是继承工厂类AbstractGatewayFilterFactory,由工厂根据不同的config生产出自定义的GatewayFilter。

无参的自定义GatewayFilter

public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {

    @Override
    public GatewayFilter apply(Object config) {

        /*
         * 返回匿名配置new GatewayFilter(),但匿名内部类无法在实现Ordered接口,因此无法在确定过滤器的顺序
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                System.out.println("Print Andy is running ...");
                return chain.filter(exchange);
            }
        };
        */

        /**
         *  提供了装饰类 OrderedGatewayFilter(), 增加了Ordered接口的实现
         */
        return new OrderedGatewayFilter(new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                System.out.println("Print Any is running ...");
                return chain.filter(exchange);
            }
        }, 0);
    }

}

注意:自定义过滤器工厂类的名字是XxxGatewayFilterFactory(后缀是GatewayFilterFactory),在配置文件中配置过滤器的前缀Xxx。

配置文件中的配置

spring:
  cloud:
    gateway:
      default-filters:
        - PrintAny

有参的自定义GatewayFilter

public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {

    @Override
    public GatewayFilter apply(Config config) {

        /*
         * 返回匿名配置new GatewayFilter(),但匿名内部类无法在实现Ordered接口,因此无法在确定过滤器的顺序
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                System.out.println("Print Andy is running ...");
                return chain.filter(exchange);
            }
        };
        */

        /**
         *  提供了装饰类 OrderedGatewayFilter(), 增加了Ordered接口的实现
         */
        return new OrderedGatewayFilter(new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                String a = config.getA();
                String b = config.getB();
                String c = config.getC();
                System.out.println(a);
                System.out.println(b);
                System.out.println(c);
                System.out.println("Print Any is running ...");
                return chain.filter(exchange);
            }
        }, 0);
    }

    /**
     * 自定义配置属性
     * 成员变量名很重要,下面会用到
     */
    @Data
    public static class Config {
        private String a;
        private String b;
        private String c;
    }

    /**
     * 将变量名以此返回,顺序很重要,将来读取参数时需要按顺序获取
     */
    @Override
    public List<String> shortcutFieldOrder() {
        ArrayList<String> params = new ArrayList<>();
        params.add("a");
        params.add("b");
        params.add("c");
        return params;
    }

    public PrintAnyGatewayFilterFactory() {
        super(Config.class);
    }
}

三、动态网关

spring-cloud-gateway-server包提供了RouteDefinitionWriter接口,可以动态修改路由。

动态路由的实现可以通过配置中心或数据库来实现。

3.1 基于配置中心的动态路由

实现动态路由的原理是:

将路由信息配置在配置中心,对配置信息进行监控,将路由变化信息通过RouteDefinitionWriter更新路由表。

1)在nacos中配置路由信息

dataId名称:gateway-routes.json

group: DEFAULT_GROUP

[
    {
        "id": "service-user",
        "uri": "lb://demo-springcloud-alibaba-service-user",
        "predicates": [{
            "name": "Path",
            "args": {
                "_genkey_0": "/sys/**",
                "_genkey_1": "/user/**"
            }
        }],
        "filters": []
    }
]

2)编写动态路由组件DynamicRoute

@Component
@RequiredArgsConstructor
public class DynamicRoute {

    private final NacosConfigManager nacosConfigManager;

    private final RouteDefinitionWriter routeDefinitionWriter;

    /**
     * 路由Id集合
     */
    private static Set<String> routeIds = new HashSet<>();

    private String dataId = "gateway-routes.json";
    private String group = "DEFAULT_GROUP";

    private Long timeout = 3000L;

    /**
     * 初始化路由信息
     * @PostConstruct 表示在组件初始化完成之后执行
     * @throws NacosException
     */
    @PostConstruct
    private void initRoutes() throws NacosException {
        // routeInfo是从配置中心拉取到的配置信息
        String routeInfo = nacosConfigManager.getConfigService()
                .getConfigAndSignListener(dataId, group, timeout, new Listener() {
                    @Override
                    public Executor getExecutor() {
                        return null;
                    }

                    /**
                     * 当配置发送变化时,监听到了变化
                     * @param s
                     */
                    @Override
                    public void receiveConfigInfo(String configInfo) {
                        // 监听到了配置变化,需要更新路由表
                        updateConfigInfo(configInfo);
                    }
                });

        // 当类初始化完成后加载路由表
        updateConfigInfo(routeInfo);
    }

    /**
     * 更新路由表
     * @param configInfo 路由表JSON
     */
    private void updateConfigInfo(String configInfo) {
        ObjectMapper objectMapper = new ObjectMapper();
        List<RouteDefinition> routeDefinitions = null;
        try {
            routeDefinitions = objectMapper.readValue(configInfo, new TypeReference<List<RouteDefinition>>() {});
        } catch (JsonProcessingException e) {
            throw new RuntimeException("路由信息配置有无");
        }

        if (routeDefinitions == null || routeDefinitions.size() == 0) {
            return;
        }

        // 当路由有变化时,先删除所有路由
        if (routeIds != null && !routeIds.isEmpty()) {
            for (String routeId: routeIds) {
                routeDefinitionWriter.delete(Mono.just(routeId)).subscribe();
            }
        }

        routeIds.clear();

        // 再重新添加路由
        for (RouteDefinition routeDefinition: routeDefinitions) {
            routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
            routeIds.add(routeDefinition.getId());
        }
    }

}

3.2 基于数据库的动态路由

将路由信息配置在数据库中,当通过接口修改路由数据后,再调用RouteDefinitionWriter更新路由表。

1)GatewayService

定义一个GatewayService,用于从数据库加载所有路由,并且提供修改路由的接口

public interface GatewayService {

    /**
     * 加载所有路由
     * @return 路由数据
     */
    String loadAllRoutes();

    /**
     * 加载(更新)一条路由
     * @param gatewayEntity
     * @return
     */
    String loadRoute(GatewayEntity gatewayEntity);

}

接口实现类 GatewayServiceImpl

@Service("gatewayService")
public class GatewayServiceImpl implements GatewayService, ApplicationEventPublisherAware {

    private ApplicationEventPublisher publisher;

    @Resource
    private GatewayDAO gatewayDAO;

    @Resource
    private RouteDefinitionWriter routeDefinitionWriter;

    /**
     * 加载所有路由
     * @return true/false
     */
    @Override
    public String loadAllRoutes() {
        List<GatewayEntity> allRoutes = gatewayDAO.findAll();
        if (CollectionUtils.isEmpty(allRoutes)) {
            return "failed";
        }

        allRoutes.stream().forEach(gatewayEntity -> {
            loadRoute(gatewayEntity);
        });

        return "success";
    }

    /**
     * 加载一条路由
     * @param gatewayEntity
     * @return
     */
    @Override
    public String loadRoute(GatewayEntity gatewayEntity) {
        // 申明一个路由定义
        RouteDefinition routeDefinition = new RouteDefinition();

        // 定义的路由唯一的id
        routeDefinition.setId(gatewayEntity.getRouteId());

        // 定义uri
        URI uri = null;
        if ("0".equals(gatewayEntity.getRouteType())) {
            // 如果配置路由type为0的话 则从注册中心根据服务名获取转发地址
            uri = UriComponentsBuilder.fromUriString("lb://" + gatewayEntity.getRouteUrl() + "/").build().toUri();
        } else {
            // 如果配置路由type为1的话,则数据库存储的是即要转发的地址
            uri = UriComponentsBuilder.fromHttpUrl(gatewayEntity.getRouteUrl()).build().toUri();
        }
        routeDefinition.setUri(uri);

        // 定义Predicate
        PredicateDefinition predicate = new PredicateDefinition();
        // 路由断言采用路由匹配(除了路径匹配还有Header, Host, Method, Query, RemoteAddr)
        predicate.setName("Path");
        // 路由转发地址
        Map<String, String> predicateParams = new HashMap<>(8);
        predicateParams.put("pattern", gatewayEntity.getRoutePattern());
        predicate.setArgs(predicateParams);
        routeDefinition.setPredicates(Arrays.asList(predicate));

        // 定义Filter
        FilterDefinition filterDefinition = new FilterDefinition();

        // 路径去前缀
//        filterDefinition.setName("StripPrefix");
//        Map<String, String> filterParams = new HashMap<>(8);
//        filterParams.put("_genkey_0", "1");
//        filterDefinition.setArgs(filterParams);

        // 转发带域名
        Map<String, String> preserveHostFilterParams = new HashMap<>();
        filterDefinition.setName("PreserveHostHeader");
        preserveHostFilterParams.put("_genkey_0", "true");
        filterDefinition.setArgs(preserveHostFilterParams);

        routeDefinition.setFilters(Arrays.asList(filterDefinition));

        // 保存
        routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();

        // 发布刷新路由事件
        this.publisher.publishEvent(new RefreshRoutesEvent(this));

        return "success";
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }

}

2)Spring Boot容器启动之后调用网关服务Gateway初始化路由

@Component
@Slf4j
public class GatewayRunner implements ApplicationRunner {

    @Resource
    private GatewayService gatewayService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("执行从数据库加载所有路由");
        gatewayService.loadAllRoutes();
    }

}
;