Bootstrap

spring gateway网关接口黑名单实现

1. 背景

灵活配置线上接口黑名单,可以临时调用

2. 网关路由配置application.yml

server:
  port: 8868
spring:
  cloud:
    gateway:
      globalcors: # 全局的跨域配置
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
        # options请求 就是一种询问服务器是否浏览器可以跨域的请求
        # 如果每次跨域都有询问服务器是否浏览器可以跨域对性能也是损耗
        # 可以配置本次跨域检测的有效期maxAge
        # 在maxAge设置的时间范围内,不去询问,统统允许跨域
        corsConfigurations:
          '[/**]':
            allowedOriginPatterns: # 允许哪些网站的跨域请求
              - "*"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*"    # 允许在请求中携带的头信息
            allowCredentials: true    # 允许在请求中携带cookie
            maxAge: 360000            # 本次跨域检测的有效期(单位毫秒)
            # 有效期内,跨域请求不会一直发option请求去增大服务器压力
      routes: # 网关路由配置
        - id: route_to_provider #service服务,业务逻辑
          uri: lb://cloud-k8s-enterprise-service #通过服务发现找到 Service 服务
          predicates:
            - Path=/pay/**,/wx/callback/**
        - id: route_to_server #web服务 入口
          uri: lb://cloud-k8s-enterprise-server #通过服务发现找到 Service 服务
          predicates:
            - Path=/**
      #default-filters:
      # - name: BlacklistFilter
        
        #BlacklistFilter 在 default-filters 中使用时可能导致了错误。default-filters 是一个全局过滤器的配置,通常这些过滤器需要是 Spring Cloud Gateway 提供的预定义过滤器,而不是自定义的全局过滤器。你的 BlacklistFilter 是自定义的全局过滤器,不能直接放在 default-filters 中。
logging:
  level:
    org.springframework.cloud.gateway: DEBUG

2.1. 坑点:无法进行路由转发,配置格式;无法加载自定义全局过滤器

spring.cloud.gateway: 注意坑点!!!!!!!

这里如果单独把nacos配置拎出去,注释了,结果网关路由gateway缺少cloud这层配置,一直无法进行路由转发,网关配置未生效

server:
  port: 8868
spring:
#  application:
#    name: service-gateway
#  cloud:  注意坑点!!!!!!! 这里有人单独把nacos配置拎出去,注释了,结果网关路由gateway缺少cloud这层配置,一直无法进行路由转发,网关配置未生效
#    nacos:
#      discovery:
#        server-addr: 127.0.0.1:8848
    gateway:
      globalcors: # 全局的跨域配置

default-filters:

- name: BlacklistFilter

BlacklistFilter 在 default-filters 中使用时可能导致了错误。default-filters 是一个全局过滤器的配置,通常这些过滤器需要是 Spring Cloud Gateway 提供的预定义过滤器,而不是自定义的全局过滤器。你的 BlacklistFilter 是自定义的全局过滤器,不能直接放在 default-filters 中。

可以配置在某个单独的服务下

#单服务黑名单网关配置demo
spring:
  cloud:
    gateway:
      routes:
        - id: dynamic_blacklist_route
          uri: http://localhost:8080
          predicates:
            - Path=/api/v1/**
          filters:
            - name: BlacklistFilter
server:
  port: 8868
spring:
#  application:
#    name: service-gateway
#  cloud:  注意坑点!!!!!!! 这里有人单独把nacos配置拎出去,注释了,结果网关路由gateway缺少cloud这层配置,一直无法进行路由转发,网关配置未生效
#    nacos:
#      discovery:
#        server-addr: 127.0.0.1:8848
    gateway:
      globalcors: # 全局的跨域配置
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
        # options请求 就是一种询问服务器是否浏览器可以跨域的请求
        # 如果每次跨域都有询问服务器是否浏览器可以跨域对性能也是损耗
        # 可以配置本次跨域检测的有效期maxAge
        # 在maxAge设置的时间范围内,不去询问,统统允许跨域
        corsConfigurations:
          '[/**]':
            allowedOriginPatterns: # 允许哪些网站的跨域请求
              - "*"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*"    # 允许在请求中携带的头信息
            allowCredentials: true    # 允许在请求中携带cookie
            maxAge: 360000            # 本次跨域检测的有效期(单位毫秒)
            # 有效期内,跨域请求不会一直发option请求去增大服务器压力
      routes: # 网关路由配置
        - id: route_to_provider #service服务,业务逻辑
          uri: lb://cloud-service #通过服务发现找到 Service 服务
          predicates:
            - Path=/apiv1/**,/apiv2/**
        - id: route_to_server #web服务 入口
          uri: lb://cloud-server #通过服务发现找到 Service 服务
          predicates:
            - Path=/**
logging:
  level:
    org.springframework.cloud.gateway: DEBUG

2.2. 帮助文档

Spring Colud gateway 网关引入转发无效 (404)_gateway_虔浅-云原生

跟着大佬们的文章,想玩一下gateway api网关。经过一系列ctrl+c和ctrl+v的操作,项目的基本就搭建好了;

1.引入依赖 pom.xml

<!--网关依赖-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--不要引入web!不要引入web!不要引入web!gateway中已经包含-->

2.创建启动类

//如果有注册机(nacos,eureka)什么的可以不用引入
//@EnableDiscoveryClient,只需在配置文件里面配置好就行
@SpringBootApplication
public class GatewayServer {
    public static void main(String[] args) {
        SpringApplication.run(GatewayServer.class,args);
    }
}

3.配置文件

server:
    port: 9999                    #服务端口

spring:
    application:
        name: gateway      #服务名称
    cloud:
        ############# nacos配置中心 start (没有注册机可以不用配置这一块) #############
        nacos:
            # nacos配置中心    #nacos的配置文件名称(Data Id)叫 服务名称.yml ,
            # 组名(Group)
            config:
                server-addr: http://nacos服务器
                file-extension: yml
                namespace: xxx #命名空间(md5)
                group: 分组名    #分组
            #发现配置
            discovery:
                server-addr: http://www.lang9725.fun/find/
                namespace:  xxx #命名空间(md5)
                group: batw
        ############# nacos配置中心 end #############

        ############## 网关配置 start ##############
        gateway:
            #开启网关,和很多地方说不一样,很多地方都是这个是默认开启的,
            #但设置的话网关功能将无效
            enabled: true
            routes:
                - id: server_finance        #id 唯一即可
                  uri: http://localhost:44444  #用转发路径
                  predicates:
                      - Path=/test/test/**     # **表示转发地址下的全部都可以通过 
        ############## 网关配置 end ##############

这里要一个被转发服务器地址: http://localhost:44444/test/test/任意地址 并保证这个地址没问题,我们的测试网关地址:http://localhost:9999/test/test/任意地址,保证两个地址的返回效果一致(负载均衡效果到达预期)

前期没有配置spring.cloud.gateway.enabled=true,测试网关一直到报404,看了很多大佬debug,不明所以,最后发现是这边配置没有加,而是配置一个spring.cloud.gateway.discovery.locator.enabled=true,后面加了spring.cloud.gateway.enabled=true就可以了,这个东西应该是和版本,和依赖包一定联系吧。

3. nacos配置bootstrap.yml

spring: #springboot的配置
  application:
    name: cloud-gateway
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        namespace: cloud-local
        file-extension: yml
        shared-configs:
          - data-id: blacklist.yml # 在 Nacos 中的配置 Data ID
            refresh: true           # 启用动态刷新
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: cloud-local 

4. 黑名单配置blacklist.yml

blacklist:
  paths:
    - /coupon/manualGrantPowerCard
    - /coupon/manualGrantVoucher

4.1. 加载配置类BlacklistConfig.java

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @author shenlifang
 * @since 黑名单配置类
 * @date 2024/10/21 11:16
 */
@Component
@RefreshScope
@ConfigurationProperties(prefix = "blacklist")
public class BlacklistConfig {
    private List<String> paths;

    // Getter 和 Setter
    public List<String> getPaths() {
        return paths;
    }

    public void setPaths(List<String> paths) {
        this.paths = paths;
    }
}

5. 启动类,扫描包GatewayApplication.java


import com.ly.cloud.gateway.config.BlacklistConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
@EnableConfigurationProperties(BlacklistConfig.class)
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

6. 自定义全局过滤器BlacklistFilter.java

package com.cloud.gateway.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * @author shenlifang
 * @date 2024/10/21 11:12
 * @since 接口黑名单过滤器
 */
@Component
public class BlacklistFilter implements GlobalFilter, Ordered {

    // 黑名单接口路径列表,可以通过配置文件或者数据库来动态管理
    private final BlacklistConfig blacklistConfig;

    // 通过构造器注入 BlacklistConfig
    public BlacklistFilter(BlacklistConfig blacklistConfig) {
        this.blacklistConfig = blacklistConfig;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();

        // 获取配置中的黑名单路径
        List<String> blacklistedPaths = blacklistConfig.getPaths();

        // 检查请求路径是否在黑名单中
        if (isBlacklistedPath(path, blacklistedPaths)) {
            // 如果匹配黑名单,返回 403 Forbidden
            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);

            // return exchange.getResponse().setComplete();

            // 设置响应头,表示内容类型为 JSON
            exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);

            // 返回描述信息
            // String responseBody = "{\"successful\":false,\"code\":403,\"message\":\"接口服务禁用: 该接口服务已加入黑名单,禁止访问!\"}";
            String responseBody = "{\"error\": \"Access denied: The requested path is blacklisted.\"}";


            // 写入响应体
            DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
            DataBuffer buffer = bufferFactory.wrap(responseBody.getBytes(StandardCharsets.UTF_8));

            // 完成响应
            return exchange.getResponse().writeWith(Mono.just(buffer));

        }
        // 如果没有匹配黑名单,继续执行后续过滤器链
        return chain.filter(exchange);
    }

    // 判断路径是否在黑名单中
    private boolean isBlacklistedPath(String path, List<String> blacklistedPaths) {
        return blacklistedPaths.stream().anyMatch(path::startsWith);
    }

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

7. 优化改造:优先从redis中获取

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.concurrent.TimeUnit;

@Component
public class BlacklistFilter implements GlobalFilter, Ordered {

    private final BlacklistConfig blacklistConfig;
    private final RedisTemplate<String, List<String>> redisTemplate;

    // 通过构造器注入 BlacklistConfig 和 RedisTemplate
    public BlacklistFilter(BlacklistConfig blacklistConfig, RedisTemplate<String, List<String>> redisTemplate) {
        this.blacklistConfig = blacklistConfig;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();

        // 优先从 Redis 中获取黑名单路径
        List<String> blacklistedPaths = redisTemplate.opsForValue().get("blacklist_paths");

        // 如果 Redis 中没有黑名单路径,则从配置文件中获取并缓存到 Redis
        if (blacklistedPaths == null || blacklistedPaths.isEmpty()) {
            blacklistedPaths = blacklistConfig.getPaths();

            // 将配置文件中的黑名单缓存到 Redis,有效期设置为 10 分钟
            redisTemplate.opsForValue().set("blacklist_paths", blacklistedPaths, 10, TimeUnit.MINUTES);
        }

        // 检查请求路径是否在黑名单中
        if (isBlacklistedPath(path, blacklistedPaths)) {
            // 如果匹配黑名单,返回 403 Forbidden
            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
            return exchange.getResponse().setComplete();
        }

        // 如果没有匹配黑名单,继续执行后续过滤器链
        return chain.filter(exchange);
    }

    // 判断路径是否在黑名单中
    private boolean isBlacklistedPath(String path, List<String> blacklistedPaths) {
        return blacklistedPaths.stream().anyMatch(path::startsWith);
    }

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

8. 网关启动过程中日志打印网关配置属性

如果没打印,查看yaml文件配置是否有问题

2024-10-21 17:14:02.677 DEBUG 8754 --- [ main] o.s.c.gateway.config.GatewayProperties : Routes supplied from Gateway Properties: [RouteDefinition{id='route_to_provider', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/apiv1/**, _genkey_1=/apiv2/**}}], filters=[], uri=lb://cloud-service, order=0, metadata={}}, RouteDefinition{id='route_to_server', predicates=[PredicateDefinition{name='Path', args={_genkey_0=/**}}], filters=[], uri=lb://cloud-server, order=0, metadata={}}]

日志中的 o.s.c.gateway.config.GatewayProperties 是 Spring Cloud Gateway 在启动过程中输出的日志。具体解释如下:

  • o.s.c.gateway.config.GatewayProperties:这是类的名称,表示 org.springframework.cloud.gateway.config.GatewayProperties。这个类负责管理 Spring Cloud Gateway 的配置属性,包括网关的路由、过滤器等信息。
  • Routes supplied from Gateway Properties::这条日志表明,Spring Cloud Gateway 已经从应用的配置文件(例如 application.yml 或者 bootstrap.yml)中读取并加载了路由配置。

  • RouteDefinition:表示每一条路由定义。
    • id:路由的唯一标识符,例如 route_to_providerroute_to_server
    • predicates:路由谓词,用来匹配请求路径。例如,Path 谓词匹配 /apiv1/**/apiv2/** 等路径。
    • filters:表示应用在路由上的过滤器,在当前配置中为空(没有应用额外的过滤器)。
    • uri:目标 URI,lb://cloud-service 表示通过负载均衡(LoadBalancer)转发请求到服务 cloud-service
    • order:路由的优先级,值越小优先级越高。
    • metadata:存储路由的元数据,当前为空。

Spring Cloud Gateway 从配置文件中正确读取并加载了两个路由定义,分别是 route_to_providerroute_to_server,并根据配置的路径规则(Path)匹配请求并进行转发。

如果路由匹配不正确,可以通过查看这些日志确认路由是否如预期加载。

;