Fork me on Gitee

SpringCloud Gateway实战

SpringCloud Gateway

认识SpringCloud Gateway

  • SpringCloud Gateway 是 Spring 官方最新推出的一款基于 SpringFramework 5,Project Reactor和SpringBoot 2之上开发的网关。

  • 它与第一代网关 Zuul 不同的是:gateway 是异步非阻塞的(netty + webflux 实现 )zuul是同步阻塞请求的。

  • Gateway三大组成部分

    • Route路由:由ID和目标URI组成
    • Predicate断言
    • Filter过滤器

image-20240308202932196

SpringCloud Gateway和Zuul最核心的区别

image-20240308222426448

SpringCloud Gateway工作模型图及解读

image-20240308203309225

SpringCloud Gateway三大核心概念

Route、Predicate、Filter

image-20240308222612547

Spring Cloud Gateway 过滤器

全局过滤器和局部过滤器

  • 全局过滤器作用于所有的路由不需要单独配置,通常用来实现统一-化处理的业务需求
  • 局部过滤器实现并生效的三个步骤
      1. 需要实现 GatewayFilter, Ord注册到 Spring 容器中。
      1. 加入到过滤器工厂,并且将工厂注册到Spring容器中
      1. 在配置文件中进行配置,如果)配置则不启用此过滤器规则(路由规则)

SpringCloud Gateway 路由的配置

常见的三种配置方式

  1. 在代码中注入 RouteLocator Bean,并手工编写配置路由定义;
  2. 在 application.yml、bootstrap.ym| 等配置文件中配置 spring.cloud.gateway;
  3. 通过配置中心(Nacos)实现动态的路由配置

谓词Predicate的原理与应用

谓词Predicate是什么

由Java 8引入,位于Java.util.function包中,是一个FunctionInterface(函数式接口)

image-20240308203643866

@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class PredicateTest {

	public static List<String> MICRO_SERVICE = Arrays
			.asList("nacos", "authority", "gateway", "ribbon", "feign", "hystrix", "e-commerce");

	/**
	 * 方法主要用于参数符不符合规则,返回值是boolean
	 */
	@Test
	public void testPredicateTest() {
		Predicate<String> letterLengthLimit = s -> s.length() > 5;
		MICRO_SERVICE.stream().filter(letterLengthLimit).forEach(System.out::println);
	}

	/**
	 * and 方法等同于 逻辑与 & ,存在短路特性,需要所有的条件都满足
	 * 长度大于5 且 字符串以 gate 开头
	 */
	@Test
	public void testPredicateAnd(){
		Predicate<String> letterLengthLimit = s -> s.length() >5;
		Predicate<String> letterStartWith = s -> s.startsWith("gate");
		MICRO_SERVICE.stream().filter(
				letterLengthLimit.and(letterStartWith)
		).forEach(System.out::println);

	}

	/**
	 * or 等同于逻辑或||
	 */
	@Test
	public void testPredicateOr(){
		Predicate<String> letterLengthLimit = s -> s.length() >5;
		Predicate<String> letterStartWith = s -> s.startsWith("gate");
		//要么第一个能通过,要么第二个通过
		MICRO_SERVICE.stream().filter(
				letterLengthLimit.or(letterStartWith)
		).forEach(System.out::println);
	}

	/**
	 * negate 等同于逻辑非 !
	 */
	@Test
	public void testNegate(){
		Predicate<String> letterStartWith = s -> s.startsWith("gate");
		MICRO_SERVICE.stream().filter(
				letterStartWith.negate()
		).forEach(System.out::println);

	}


	/**
	 * isEqual 类似与Object中 equals(),区别在于先判断对象是否为null, 如果不为null,再使用equals进行比较
	 */
	@Test
	public void testPredicateEqual(){
		Predicate<String> equalGateway = s ->Predicate.isEqual("gateway").test(s);
		 MICRO_SERVICE.stream().filter(equalGateway).forEach(System.out::println);
	}


}

比如可查看PathRoutePredicateFactory类。

image-20240308204758353

集成Alibaba Nacos实现动态路由配置

静态路由配置

静态路由配置写在配置文件中(yml或者properties文件中),端点是:Spring.cloud.gateway

image-20240308205153161

缺点非常明显,每次改动都需要网关模块重新部署。

动态路由配置

image-20240308205305192

nacos:
  gateway:
    route:
      config:
        # 即为配置中心中的配置文件名称 
        data-id: e-commerce-gateway-router
        group: e-commerce

GatewayConfig: 定于配置类,读取Nacos相关的配置项,用于监听配置监听器

/** 
 *
 * 配置类, 读取nacos相关的配置项,用于配置监听器
 * 
 */
@Configuration
public class GatewayConfig {

	/**
	 * 读取配置的超时时间
	 */
	public static final long DEFAULT_TIMEOUT = 30000;

	/**
	 * nacos服务器地址
	 */
	public static String NACOS_SERVER_ADD;

	/**
	 * 命名空间
	 */
	public static String NACOS_NAMESPACE;

	/**
	 * data—id
	 */
	public static String NACOS_ROUTE_DATA_ID;

	/**
	 * 分组id
	 */
	public static String NACOS_ROUTE_GROUP;

	@Value("${spring.cloud.nacos.discovery.server-addr}")
	public void setNacosServerAdd(String nacosServerAdd){
		NACOS_SERVER_ADD = nacosServerAdd;
	}

	@Value("${spring.cloud.nacos.discovery.namespace}")
	public void setNacosNamespace(String nacosNamespace){
		NACOS_NAMESPACE = nacosNamespace;
	}

	@Value("${nacos.gateway.route.config.data-id}")
	public void setNacosRouteDataId(String nacosRouteDataId){
		NACOS_ROUTE_DATA_ID = nacosRouteDataId;
	}

	@Value("${nacos.gateway.route.config.group}")
	public void setNacosRouteGroupId(String group){
		NACOS_ROUTE_GROUP = group;
	}

}

实现动态更新路由网关Service,实现对路由信息的动态更新:添加路由定义和更新路由定义

/**
 * 事件推送Aware : 动态更新路由网关 Service
 *
 *
 */
@Slf4j
@Service
@SuppressWarnings("all")
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {

    /**
     * 写路由定义
     */
    private final RouteDefinitionWriter routeDefinitionWriter;
    /**
     * 获取路由定义
     */
    private final RouteDefinitionLocator routeDefinitionLocator;

    /**
     * 事件发布
     */
    private ApplicationEventPublisher publisher;


    public DynamicRouteServiceImpl(RouteDefinitionWriter routeDefinitionWriter, RouteDefinitionLocator routeDefinitionLocator, ApplicationEventPublisher publisher) {
        this.routeDefinitionWriter = routeDefinitionWriter;
        this.routeDefinitionLocator = routeDefinitionLocator;
        this.publisher = publisher;
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        //完成事件推送句柄的初始化
        this.publisher = applicationEventPublisher;
    }

    /**
     * 增加路由定义
     * @param routeDefinition
     * @return
     */
    public String addRouteDefination(RouteDefinition definition){
        log.info("gateway add route:[{}]",definition);

        //保存路由配置并发布
        routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        //发布事件通知给Gateway,同步新增的路由定义
        this.publisher.publishEvent(new RefreshRoutesEvent(this));

        return "success";
    }


    /**
     * 更新路由
     * @param definitions
     * @return
     */
    public String updateList(List<RouteDefinition> definitions){
        log.info("gateway update route : [{}]",definitions);

        //先拿到当前Gateway中存储的路由定义
        List<RouteDefinition> routeDefinitionsExists = routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();

        if(!CollectionUtil.isEmpty(routeDefinitionsExists)){
            //需要清除掉之前所有的 “旧的” 路由定义
            routeDefinitionsExists.forEach(rd ->{
                log.info("delete route defination: [{}]",rd);
                deleteById(rd.getId());
            });
        }

        //把更新的路由定义同步到 gateway中
        definitions.forEach(definition ->updateByRouteDefination(definition));
        return "success";
    }

    /**
     * 根据路由id删除路由配置
     * @param id
     * @return
     */
    private String deleteById(String id){
        try{
            log.info("gateway delete route id :[{}]",id);
            this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
            //发布事件通知给gateway 更新路由定义
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
            return "delte success";
        }catch (Exception ex){
            log.error("gateway delete route fail:[{}]",ex.getMessage(),ex);
            return "delete fail";
        }
    }

    /**
     * 更新路由
     * 更新的实现策略比较简单: 删除+新增=更新
     * @param definition
     * @return
     */
    private String updateByRouteDefination(RouteDefinition definition){
        try{
            log.info("gateway update route :[{}]",definition);
            this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
        }catch (Exception ex){
            return "update fail,not find route routeId "+definition.getId();
        }
        try{
            this.routeDefinitionWriter.save(Mono.just(definition)).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
            return "success";
        }catch (Exception ex){
            return "update route fail";
        }
    }

}

连接到NacosConfigService,读取配置信息,增加及更改路由信息。

/**
 *
 * 通过Nacos 下发动态路由配置,监听nacos中路由配置的变更
 * DependsOn是强调依赖类GatewayConfig Bean先注入,在注册该类
 * 
 */
@Slf4j
@Component
@DependsOn("gatewayConfig")
public class DynamicRouteServiceImplByNacos {

    /**
     * Nacos配置服务客户端
     */
    private ConfigService configService;

    private final DynamicRouteServiceImpl dynamicRouteService;


    public DynamicRouteServiceImplByNacos(DynamicRouteServiceImpl dynamicRouteService) {
        this.dynamicRouteService = dynamicRouteService;
    }

    /**
     * 初始化 Nacos Config,填充 Nacos配置中心信息
     *
     */
    private ConfigService initConfigService() {

        try {
            Properties properties = new Properties();
            properties.setProperty("serverAddr", GatewayConfig.NACOS_SERVER_ADD);
            properties.setProperty("namespace", GatewayConfig.NACOS_NAMESPACE);
            return configService = NacosFactory.createConfigService(properties);
        } catch (NacosException ex) {
            log.error("init gateway nacos config error :[{}]", ex.getMessage(), ex);
            return null;
        }
    }

    /**
     * 监听 nacos下发的动态路由配置信息
     *
     * @param dataId nacos dataId
     * @param group nacos Group
     */
    private void dynamicRouteByNacosListener(String dataId, String group) {

        try {
            // 给nacos config客户端增加一个监听器
            configService.addListener(dataId, group, new Listener() {

                /**
                 * 自己提供线程池执行操作
                 */
                @Override
                public Executor getExecutor() {
                    return null;
                }

                /**
                 * 监听器收到配置更新
                 * @param configInfo Nacos中最新的配置定义
                 */
                @Override
                public void receiveConfigInfo(String configInfo) {
                    log.info("start to update config :[{}]", configInfo);
                    List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
                    log.info("update route :[{}]", definitionList.toString());

                    dynamicRouteService.updateList(definitionList);
                }
            });
        } catch (NacosException ex) {
            log.error("dynamic update gateway config error :[{}]", ex.getMessage(), ex);
        }
    }

    /**
     * Bean在容器中构造完成之后会执行 init 方法,读取初始化Nacos配置中心内容
     */
    @PostConstruct
    public void init() {
        log.info("gateway route init...");
        try {
            // 初始化 Nacos 配置客户端
            configService = initConfigService();
            if (null == configService) {
                log.error("init config service fail");
                return;
            }

            // 通过Nacos Config 并指定路由配置路径去获取路由配置
            String configInfo = configService.getConfig(
                    GatewayConfig.NACOS_ROUTE_DATA_ID,
                    GatewayConfig.NACOS_ROUTE_GROUP,
                    GatewayConfig.DEFAULT_TIMEOUT
            );

            log.info("get current gateway config:[{}]", configInfo);
            List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
            if (CollectionUtil.isNotEmpty(definitionList)) {
                for (RouteDefinition definition : definitionList) {
                    log.info("init gateway config :[{}]", definition.toString());
                    dynamicRouteService.addRouteDefination(definition);
                }
            }


        } catch (Exception ex) {
            log.error("gateway route init has some error :[{}]", ex.getMessage(), ex);
        }

        // 设置监听器
        dynamicRouteByNacosListener(GatewayConfig.NACOS_ROUTE_DATA_ID, GatewayConfig.NACOS_ROUTE_GROUP);
    }

}

解读SpringCloud Gateway Filter

image-20240308213408812

全局过滤器

RouteToRequestUrlFilter.java,实现路由url转换过程

image-20240308213810658

局部过滤器

前缀过滤器

image-20240308214051403

举例

spring:
	cloud:
		routes:
			-id:qinyi
			uri:http://example/org
			filters:
				-PrefixPath=/mypath

如果请求/hello命中了,就会变成http://example/org/mypath/hello

另一个局部过滤器StripPrefixGatewayFilterFactory.

image-20240308214519009

spring:
	cloud:
		routes:
			-id:qinyi
			uri:http://example/org
			filters:
				- StripPrefix=2
				

举例:/name/bar/foo 会截断为 /foo

SpringCloud Gateway的执行流程

image-20240308214943933

举例:自定义HeaderToken局部过滤器,用于对请求header中token进行校验

HeadTokenGatewayFilter

/**
 *
 * HTTP 请求头部携带 token验证过滤器
 *
 */
public class HeadTokenGatewayFilter implements GatewayFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 从Http Header中寻找key为token value为imooc的键值对
        String name = exchange.getRequest().getHeaders().getFirst("token");
        if("imooc".equals(name)){
            return chain.filter(exchange);
        }

        //标记此次请求没有权限,并结束这次请求
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();
    }

    /**
     * 数字越大,优先级越低
     * @return 优先级level
     */
    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE+2;
    }
}

加入到过滤器工厂

/**
 * Head Token 局部过滤器
 * 
 */
@Component
public class HeaderTokenGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
    @Override
    public GatewayFilter apply(Object config) {
        return new HeadTokenGatewayFilter();
    }
}

添加到配置文件中

"filters": [
      {
        "name": "HeaderToken"
      },
      {
      	# 截断路由前缀第一个/之前的内容,即/imooc
        "name": "StripPrefix",
        "args": {
          "parts": "1"
        }
      }
]

实现全局网关缓存请求Body数据

GlobalCacheRequestBodyFilter.java,用于缓存请求body

/**
 *
 * 缓存请求body的全局过滤器
 * Spring webFlux
 * 
 */
@Slf4j
@Component
@SuppressWarnings("all")
public class GlobalCacheRequestBodyFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        boolean isloginOrRegister =
                exchange.getRequest().getURI().getPath().contains(GatewayConstant.LOGIN_URI)
                        || exchange.getRequest().getURI().getPath().contains(GatewayConstant.REGISTER_URI);

        if (null == exchange.getRequest().getHeaders().getContentType()
                || !isloginOrRegister) {
            return chain.filter(exchange);
        }

        // DataBufferUtils.join 拿到请求中的数据 --> DataBuffer
        return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {

            // 确保数据缓冲区不被释放, 必须要 DataBufferUtils.retain
            DataBufferUtils.retain(dataBuffer);
            // defer、just 都是去创建数据源, 得到当前数据的副本
            Flux<DataBuffer> cachedFlux = Flux.defer(() ->
                    Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
            // 重新包装 ServerHttpRequest, 重写 getBody 方法, 能够返回请求数据
            ServerHttpRequest mutatedRequest =
                    new ServerHttpRequestDecorator(exchange.getRequest()) {
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return cachedFlux;
                        }
                    };
            // 将包装之后的 ServerHttpRequest 向下继续传递
            return chain.filter(exchange.mutate().request(mutatedRequest).build());
        });

    }




    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE+1;
    }
}

由于请求只对注册或者登录失效。定义常量

/**
 * 网关常量定义
 *
 */
public class GatewayConstant {

    /**
     * 登录 uri
     */
    public static final String LOGIN_URI = "/e-commerce/login";

    /**
     * 注册 uri
     */
    public static final String REGISTER_URI = "/e-commerce/register";

    /**
     * 授权中心拿到token的uri 格式化接口
     */
    public static final String AUTHORITY_CENTER_TOKEN_URL_FORMAT = "http://%s:%s/ecommerce-authority-center/authority/token";

    /**
     * 去授权中心注册并拿到 token的uri 格式化接口
     */
    public static final String AUTHORITY_CENTER_REGISTER_URL_FORMAT = "http://%s:%s/ecommerce-authority-center/authority/register";
}

配合全局登录鉴权过滤器GlobalLoginOrRegisterFilter,通过CachedFilter获得数据

@Slf4j
@Component
public class GlobalLoginOrRegisterFilter implements GlobalFilter, Ordered {

    /**
     * 注册中心客户端,可以从注册中心获取服务实例信息
     */
    private final LoadBalancerClient loadBalancerClient;

    private final RestTemplate restTemplate;

    public GlobalLoginOrRegisterFilter(LoadBalancerClient loadBalancerClient, RestTemplate restTemplate) {
        this.loadBalancerClient = loadBalancerClient;
        this.restTemplate = restTemplate;
    }

    /**
     * 登录、注册、鉴权
     * 1. 如果是登录或注册,则去授权中心拿到token,并返回给客户端
     * 2. 如果是访问其他服务,则鉴权,没有权限,返回 401
     *
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        if (request.getURI().getPath().contains(GatewayConstant.LOGIN_URI)) {
            //去授权中心拿token
            String token = getTokenFromAuthorityCenter(request, GatewayConstant.AUTHORITY_CENTER_TOKEN_URL_FORMAT);
            //Header中不能设置null
            response.getHeaders().add(CommonConstant.JWT_USER_INFO_KEY,
                    null == token ? "null": token);
            response.setStatusCode(HttpStatus.OK);
            return response.setComplete();
        }
        //如果是注册
        if (request.getURI().getPath().contains(GatewayConstant.REGISTER_URI)) {
            //去授权中心拿token,先创建用户,在返回token
            String token = getTokenFromAuthorityCenter(request, GatewayConstant.AUTHORITY_CENTER_REGISTER_URL_FORMAT);
            response.getHeaders().add(
                    CommonConstant.JWT_USER_INFO_KEY,
                    null == token ? "null" : token
            );
            response.setStatusCode(HttpStatus.OK);
            return response.setComplete();
        }

        //3. 访问其他服务,则鉴权,校验是否能从token中解析用户信息
        HttpHeaders headers = request.getHeaders();
        String token = headers.getFirst(CommonConstant.JWT_USER_INFO_KEY);
        LoginUserInfo loginUserInfo = null;
        try{
            loginUserInfo = TokenParseUtil.parseUserInfoFromToken(token);
        }catch (Exception ex){
            log.error("parse user info from token error :[{}]",ex.getMessage(),ex);
        }

        //获取不到登录用户信息,返回401
        if(null == loginUserInfo){
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //解析通过,则放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE + 2;
    }

    /**
     * 从授权中心获取token
     *
     * @param request
     * @param uriFormat
     * @return
     */
    private String getTokenFromAuthorityCenter(ServerHttpRequest request, String uriFormat) {

        //service id 就是服务名字,负载均衡
        ServiceInstance serviceInstance = loadBalancerClient.choose(

                CommonConstant.AUTHORITY_CENTER_SERVICE_ID

        );
        log.info("Nacos Client Info :[{}],[{}],[{}]", serviceInstance.getServiceId(), serviceInstance.getInstanceId(), JSON.toJSONString(serviceInstance.getMetadata()));
        String requestUrl = String.format(
                uriFormat, serviceInstance.getHost(), serviceInstance.getPort()
        );
        UsernameAndPassword requestBody = JSON.parseObject(
                parseBodyFromRequest(request), UsernameAndPassword.class
        );
        log.info("login request url and body :[{}],[{}]", requestUrl, JSON.toJSONString(requestBody));
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        JwtToken token = restTemplate.postForObject(
                requestUrl, new HttpEntity<>(JSON.toJSONString(requestBody), headers), JwtToken.class
        );
        if (null != token) {
            return token.getToken();
        }
        return null;
    }

    /**
     * 从post请求中获取到请求数据
     *
     * @param request
     * @return
     */
    private String parseBodyFromRequest(ServerHttpRequest request) {

        //获取请求体
        Flux<DataBuffer> body = request.getBody();
        AtomicReference<String> bodyRef = new AtomicReference<>();

        //订阅缓冲区去消费请求体中的数据
        body.subscribe(buffer -> {
            CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
            //一定要使用DataBufferUtils.release释放掉,否则会出现内存泄漏
            DataBufferUtils.release(buffer);
            bodyRef.set(charBuffer.toString());
        });

        //获取请求体中的数据
        return bodyRef.get();
    }
}

举例

[
  {
    "id": "e-commerce-nacos-client",
    "predicates": [
      {
        "args": {
          "pattern": "/imooc/ecommerce-nacos-client/**"
        },
        "name": "Path"
      }
    ],
    "uri": "lb://e-commerce-nacos-client",
    "filters": [
      {
        "name": "HeaderToken"
      },
      {
        "name": "StripPrefix",
        "args": {
          "parts": "1"
        }
      }
    ]
  }
]
  • id:路由配置主键,需要全局唯一

  • predicates:完成路由匹配

  • uri:从注册中心中获取到服务

  • filter

    • StripPrefix:可配置跳过前缀,实现转发

配置登录请求转发规则(代码实现)

使用代码去定义路由规则,在网关层面拦截登录和注册接口,适合比较简单的路由规则。

/**
 *
 * 配置登录请求转发规则
 */
@Configuration
public class RouteLocatorConfig {

    /**
     * 使用代码去定义路由规则,在网关层面拦截登录和注册接口
     *
     * @param builder
     * @return
     */
    @Bean
    public RouteLocator loginRouteLocator(RouteLocatorBuilder builder) {

        //手动定义 Gateway路由规则需要指定 id、path和uri
        return builder.routes().route("e_commerce_authority",
                r -> r.path(
                        "/imooc/e-commerce/login",
                "/imooc/e-commerce/register"
                ).uri("http://localhost:9000/")
        ).build();
    }
}

posted @ 2024-03-08 22:37  shine-rainbow  阅读(162)  评论(0编辑  收藏  举报