【Springcloud】路由网关GateWay

【Springcloud】路由网关GateWay

【一】简单说明

微服务技术必须要有网关,让网关挡在服务的前面,进行一些日志、限流、权限、安全架构等功能,好比是医院的挂号台,给病人分配医生。

Zuul是旧的网关,GateWay是新的网关。

【二】GateWay是什么

【1】启示

不要只把重心放在编写代码上,现在更多的是进行配置,要学习的是知识体系和技术选型。

【2】GateWay厉害之处

SpringCloud2.0以上版本中,没有对新版本的Zuul2.0以上最新高性能版本进行集成,仍然还是使用Zuul1.x非reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于webflux框架实现的,而webFlux框架底层则使用了高性能的reactor模式通信框架Netty。

GateWay的目标是提供统一的路由方式,且基于filter链的方式提供了网关基本的工程,旨在提供一种简单而有效的方式来对API进行路由,以及提供了一些强大的过滤器功能,例如:安全,监控/指标、熔断、限流、重试等。在高并发下,GateWay是基于异步非阻塞模型上进行开发的,就很有优势。

【3】GateWay能干什么

路由转发(反向代理),执行过滤器链,鉴权,流量控制,熔断,日志监控

网关,旨在为微服务架构提供一种简单有效的统一的API路由管理方式。同时,基于Filter链的方式提供了网关的基本功能,比如:鉴权、流量控制、熔断、路径重写、黑白名单、日志监控等。
基本功能如下:
(1)统一入口:暴露出网关地址,作为请求唯一入口,隔离内部微服务,保障了后台服务的安全性
(2)鉴权校验:识别每个请求的权限,拒绝不符合要求的请求
(3)动态路由:能够匹配任何请求属性,动态的将请求路由到不同的后端集群中

在这里插入图片描述

【4】GateWay的特性

(1)特性

1-基于spring framework5,project reactor和spring boot2.0进行构建,使用非阻塞API
2-动态路由:能够匹配任何请求属性
3-可以对路由指定Predicate(断言)和Filter(过滤器)
4-集成Hystrix的断路器功能
5-集成SpringCloud服务发现功能
6-易于编写的Predicate(断言)和Filter(过滤器)
7-请求限流功能
8-支持路径重写

(2)三个重要的概念

(1)路由Route
路由是构建网关的基本模块,由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
(2)断言Predicate
参考的是Java8的java.util.function.Predicate开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
(3)过滤Filter
指的是Spring框架中GateWayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改

在这里插入图片描述

(3)GateWay工作流程

在这里插入图片描述

客户端向GateWay发出web请求,通过一些匹配条件,定位到真正的服务节点,在GateWay Handler Mapping中找到与请求相匹配的路由,把请求发送到Handler ,并且在这个转发过程的前后,进行一些精细化控制。predicate就是我们的匹配条件;而Filter,就可以理解为一个无所不能的拦截器,有了这两个元素,再加上目标uri,就可以实现一个具体的路由了。

Handler再通过指定的过滤器链来吧请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或之后(post)执行业务逻辑

Filter在pre类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等。在post类型的过滤器中可以做响应内容、响应头的修改,日志的输出、流量监控等,有着非常重要的作用。

【5】微服务架构中网关在哪里?

在这里插入图片描述

【6】GateWay和Zuul有什么区别?为什么选择GateWay?

(1)Zuul1模型的特性介绍
在这里插入图片描述在这里插入图片描述

(2)GateWay模型的特性
在这里插入图片描述
(3)总结区别
1-GateWay是SpringCloud团队研发出来的,
很多功能是Zuul没有的,GateWay用来非常的简单便捷

2-GateWay是基于异步非阻塞模型上进行开发的,性能方面不需要担心

多方面综合考虑GateWay是很理想的网关选择

【三】三大核心概念配置

【1】路由

(1)简单案例模板

spring:
  cloud:
	gateway:
	  routes:
	  - id: manager						# 路由唯一标识
		uri: lb://manager_server		# 路由指向目的地URL或服务名,客户端请求最终被转发到的微服务
		predicates:
		- Path=/manager/** 				# 断言:以manager开头的请求都负载到manager_server服务
		filters:
		- RewritePath=/manager/(?<segment>.*), /$\{segment} # 过滤器:过滤掉url里的manager,例如http://ip:port/manager/test -> http://ip:port/test
		order: 5						# 用于多个Route之间的排序,数值越小越靠前,匹配优先级越高

(2)路由配置(URI)

在spring cloud gateway中配置uri有三种方式,包括

现在断言的主要方式就是匹配请求路径

(1)websocket配置方式

spring:
  cloud:
    gateway:
      routes:
        - id: ruoyi-api
          uri: ws://localhost:9090/
          predicates:
            - Path=/api/**

(2)http地址配置方式

spring: 
  application:
    name: ruoyi-gateway
  cloud:
    gateway:
      routes:
        - id: ruoyi-system
          uri: http://localhost:9201/
          predicates:
            - Path=/system/**

(3)注册中心配置方式

spring:
  cloud:
    gateway:
      routes:
        - id: ruoyi-api
          uri: lb://ruoyi-api
          predicates:
            - Path=/api/**

get和lb
在这里插入图片描述

(3)代码配置

每一个服务都要在yml配置,这样最后导致庞大和臃肿,所以考虑另外一种方法就是硬代码配置,在代码中注入RouteLocator的Bean

@Configuration
public class GateWayConfig {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
        RouteLocatorBuilder.Builder routes=routeLocatorBuilder.routes();
        routes.route("path_route_atguigu",
                r ->  r.path("/guonei")
                        .uri("http://news.baidu.com/guonei"));
        return routes.build();
    }
}

【2】断言

(1)简单案例模板

spring:
  cloud:
    gateway:
      routes:
      - id: manager						# 路由唯一标识
        uri: https://manager_server
        predicates:
        - After=2017-01-20T17:42:47.789-07:00[America/Denver]	# 时间点后匹配
		- Before=2017-01-20T17:42:47.789-07:00[America/Denver]	# 时间点前匹配
		- Between=2017-01-20T17:42:47.789-07:00[America/Denver],2017-01-21T17:42:47.789-07:00[America/Denver]	# 时间区间匹配
		- Cookie=chocolate, ch.p						# 指定cookie正则匹配
		- Header=X-Request-Id, \d+						# 指定Header正则匹配
		- Host=**.somehost.org,**.anotherhost.org		# 请求Host匹配
		- Method=GET,POST								# 请求Method匹配指定请求方式
		- Path=/red/{segment},/blue/{segment}			# 请求路径正则匹配
		- Query=green									# 请求包含某参数
		- Query=red, gree.								# 请求包含某参数并且参数值匹配正则表达式(匹配red;green,greet,gree...)
		- RemoteAddr=192.168.1.1/24						# 远程地址匹配
		
		# 设置分组和权重,按照路由权重选择同一个分组中的路由
      - id: preManager1						# 路由唯一标识
        uri: https://preManager1
        predicates:
		- Weight=group1, 2
      - id: preManager2						# 路由唯一标识
        uri: https://preManager2
        predicates:
		- Weight=group1, 8

(2)路由规则(断言predicates)

(1)Datetime
匹配日期时间之后发生的请求

spring: 
  application:
    name: ruoyi-gateway
  cloud:
    gateway:
      routes:
        - id: ruoyi-system
          uri: http://localhost:9201/
          predicates:
            - After=2021-02-23T14:20:00.000+08:00[Asia/Shanghai]

(2)Cookie
匹配指定名称且其值与正则表达式匹配的cookie

spring: 
  application:
    name: ruoyi-gateway
  cloud:
    gateway:
      routes:
        - id: ruoyi-system
          uri: http://localhost:9201/
          predicates:
            - Cookie=loginname, ruoyi

测试 curl http://localhost:8080/system/config/1 --cookie “loginname=ruoyi”
就会跳转到:http://localhost:9201/

(3)Header
匹配具有指定名称的请求头,\d+值匹配正则表达式

spring: 
  application:
    name: ruoyi-gateway
  cloud:
    gateway:
      routes:
        - id: ruoyi-system
          uri: http://localhost:9201/
          predicates:
            - Header=X-Request-Id, \d+

(4)Host
匹配主机名的列表

spring: 
  application:
    name: ruoyi-gateway
  cloud:
    gateway:
      routes:
        - id: ruoyi-system
          uri: http://localhost:9201/
          predicates:
            - Host=**.somehost.org,**.anotherhost.org

(5)Method
匹配请求methods的参数,它是一个或多个参数

spring: 
  application:
    name: ruoyi-gateway
  cloud:
    gateway:
      routes:
        - id: ruoyi-system
          uri: http://localhost:9201/
          predicates:
            - Method=GET,POST

(6)Path
匹配请求路径

spring: 
  application:
    name: ruoyi-gateway
  cloud:
    gateway:
      routes:
        - id: ruoyi-system
          uri: http://localhost:9201/
          predicates:
            - Path=/system/**

(7)Query
匹配查询参数

spring: 
  application:
    name: ruoyi-gateway
  cloud:
    gateway:
      routes:
        - id: ruoyi-system
          uri: http://localhost:9201/
          predicates:
            - Query=username, abc.

(8)RemoteAddr
匹配IP地址和子网掩码

spring: 
  application:
    name: ruoyi-gateway
  cloud:
    gateway:
      routes:
        - id: ruoyi-system
          uri: http://localhost:9201/
          predicates:
            - RemoteAddr=192.168.10.1/0

(9)Weight
匹配权重

spring: 
  application:
    name: ruoyi-gateway
  cloud:
    gateway:
      routes:
        - id: ruoyi-system-a
          uri: http://localhost:9201/
          predicates:
            - Weight=group1, 8
        - id: ruoyi-system-b
          uri: http://localhost:9201/
          predicates:
            - Weight=group1, 2

【3】过滤器

(1)过滤器介绍

(1)按生命周期分类

  • 前置(pre)过滤器: 在请求被路由之前调用:在chain.filter(exchange)前编写过滤器逻辑
  • 后置(post)过滤器: 在路由到微服务之后调用:通过chain.filter(exchange).then(Mono.fromRunnable(() -> {过滤器逻辑})实现

(2)按类型分类

  • 局部(GatewayFilter)过滤器:作用在某一个路由上,使用时需要关联指定的路由
  • 全局(GlobalFilter)过滤器:作用在所有路由上,不需要在配置文件中配置

(2)内置局部过滤器与使用

1-各种内置局部过滤器
spring:
  cloud:
    gateway:
      routes:
      - id: gateway_filter
        uri: https://example.org
		predicates:
        - Path=/red/{segment}
        filters:
		# 1、为原始请求添加Header。headerName:X-Request-red,headerValue:blue。
        - AddRequestHeader=X-Request-red, blue		
		- AddRequestHeadersIfNotPresent=X-Request-Color-1:blue,X-Request-Color-2:green
		# 2、为原始请求添加参数。参数名,参数值
		- AddRequestParameter=red, blue
		# 3、为原始响应添加Header
		- AddResponseHeader=X-Response-Red, Blue
		# 4、剔除响应头中重复的值
		- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
		# 5、为原始请求路径添加前缀
		- PrefixPath=/mypath
		# 6、配置该过滤器后,会原始请求的host头信息,并原封不动的转发出去,而不是被gateway的http客户端重置。
		- PreserveHostHeader
		# 7、将原始请求重定向到指定的URL,参数为http状态码及重定向的url
		- RedirectTo=302, https://acme.org
		# 8、移除响应Body中的指定key
		- RemoveJsonAttributesResponseBody=id,color
		# 9、移除原始请求中的指定Header
		- RemoveRequestHeader=X-Request-Foo
		# 10、移除原始请求中的指定参数
		- RemoveRequestParameter=red
		# 11、移除响应中的指定Header
		- RemoveResponseHeader=X-Response-Foo
2-限流规则,根据URI限流

(1)添加依赖

<!-- spring data redis reactive 依赖 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

(2)修改yml

spring:
  cloud:
    gateway:
      routes:
      - id: gateway_filter
        uri: https://example.org
		predicates:
        - Path=/red/{segment}
        filters:
		# 12、请求限流,限流算法为令牌桶,以下示例为根据用户id做限流
		# @Configuration
		# public class RateLimiterConfig {
		#	  @Bean
		#	  public KeyResolver userKeyResolver() {
		#		  return exchange -> Mono.just(Objects.requireNonNull(exchange.getRequest().getQueryParams().getFirst("userId")));
		#     }
		# }
		- name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 10	# 允许用户每秒处理的请求数
            redis-rate-limiter.burstCapacity: 20	# 令牌桶的容量,即允许在 1 秒内完成的最大请求数。设置为 0 则表示拒绝所有请求。
            key-resolver: "#{@userKeyResolver}"		# 一个引用名为 userKeyResolver 的 bean 的 SpEL 表达式
		# 13、重写原始的请求路径
		- RewritePath=/red/?(?<segment>.*), /$\{segment}
		# 14、重写响应中的某个Header
		- RewriteResponseHeader=X-Response-Red, , password=[^&]+, password=***
		# 15、在转发请求之前,强制执行websession::save操作,保存会话状态
		- SaveSession
		# 16、修改原始的请求路径
		- SetPath=/{segment}
		# 17、修改原始请求中的指定Header值
		- SetRequestHeader=X-Request-Red, Blue
		# 18、修改原始响应中的指定Header值
		- SetResponseHeader=X-Response-Red, Blue
		# 19、修改原始响应的响应码
		- SetStatus=401
		# 20、剥离原始请求路径
		- StripPrefix=2
		# 21、请求重试
		- name: Retry
          args:
            retries: 3						# 重试次数
            statuses: BAD_GATEWAY			# 应被重试的 HTTP Status Codes
            methods: GET,POST				# 应被重试的 HTTP Methods
            backoff:						# 为重试配置指数级的 backoff。重试时间间隔的计算公式为 firstBackoff * (factor ^ n),n 是重试的次数;如果设置了 maxBackoff,最大的 backoff 限制为 maxBackoff. 如果 basedOnPreviousValue 设置为 true, backoff 计算公式为 prevBackoff * factor.
              firstBackoff: 10ms
              maxBackoff: 50ms
              factor: 2
              basedOnPreviousValue: false
		# 22、设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返413Payload Too Large
		- name: RequestSize
          args:
            maxSize: 5000000

(3)编写URI限流规则配置类

/**
 * 限流规则配置类
 */
@Configuration
public class KeyResolverConfiguration
{
    @Bean
    public KeyResolver pathKeyResolver()
    {
        return exchange ->  Mono.just(exchange.getRequest().getURI().getPath());
    }
}

(4)其他限流规则

1-参数限流:key-resolver: “#{@parameterKeyResolver}”

@Bean
public KeyResolver parameterKeyResolver()
{
	return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}

2-IP限流:key-resolver: “#{@ipKeyResolver}”

@Bean
public KeyResolver ipKeyResolver()
{
	return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}

(3)内置全局过滤器

(1)GatewayMetricsFilter(0):统计一些网关的性能指标
(2)RouteToRequestUrlFilter(10000):把浏览器的URL请求的Path路径添加到路由的URI之中。
(3)NettyRoutingFilter(2147483647):通过HttpClient客户端转发真实的URL,并存储返回的结果。
(4)NettyWriteResponseFilter(-1):在所有的其它的过滤器执行完成之后运行,将响应的数据发送给网关的客户端。
(5)ForwardRoutingFilter(2147483647):转发路由过滤器,若URI是forward模式,过滤器会将请求转发到DispatcherHandler来处理请求。
(6)ForwardPathFilter(0):解析路径,并将路径转发。
(7)LoadBalancerClientFilter(10100):负载均衡,解析服务名,获取真实服务地址。
(8)RemoveCachedBodyFilter(-2147483648):清除网关上下文中的缓存的请求Body。
(9)WebsocketRoutingFilter(2147483646):如果请求中的ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 属性对应的URL前缀为 ws 或 wss,会使用Spring Web Socket 模块转发WebSocket请求。WebSockets可以使用路由进行负载均衡。
(10)AdaptCachedBodyGlobalFilter(-2147482648):从请求中获取body缓存到网关上下文。

(4)自定义局部过滤器

局部过滤器需要在指定路由配置才能生效,默认是不生效的。注册局部过滤器与全局不同的是需要继承AbstractGatewayFilterFactory接口。

1-黑名单校验过滤器

顾名思义,就是不能访问的地址。实现自定义过滤器BlackListUrlFilter,需要配置黑名单地址列表blacklistUrl,当然有其他需求也可以实现自定义规则的过滤器。

spring:
  cloud:
    gateway:
      routes:
        # 系统模块
        - id: ruoyi-system
          uri: lb://ruoyi-system
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix=1
            - name: BlackListUrlFilter # 指定过滤器为BlackListUrlFilter
              args:
                blacklistUrl: # 黑名单列表作为参数
                - /user/list
/**
 * 黑名单过滤器
 * 
 * @author ruoyi
 */
@Component
public class BlackListUrlFilter extends AbstractGatewayFilterFactory<BlackListUrlFilter.Config>
{
    @Override
    public GatewayFilter apply(Config config)
    {
        return (exchange, chain) -> {

            String url = exchange.getRequest().getURI().getPath();
            if (config.matchBlacklist(url))
            {
                return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求地址不允许访问");
            }

            return chain.filter(exchange);
        };
    }

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

    public static class Config
    {
        private List<String> blacklistUrl;

        private List<Pattern> blacklistUrlPattern = new ArrayList<>();

        public boolean matchBlacklist(String url)
        {
            return !blacklistUrlPattern.isEmpty() && blacklistUrlPattern.stream().anyMatch(p -> p.matcher(url).find());
        }

        public List<String> getBlacklistUrl()
        {
            return blacklistUrl;
        }

        public void setBlacklistUrl(List<String> blacklistUrl)
        {
            this.blacklistUrl = blacklistUrl;
            this.blacklistUrlPattern.clear();
            this.blacklistUrl.forEach(url -> {
                this.blacklistUrlPattern.add(Pattern.compile(url.replaceAll("\\*\\*", "(.*?)"), Pattern.CASE_INSENSITIVE));
            });
        }
    }
}

(5)自定义全局过滤器

全局过滤器作用于所有的路由,不需要单独配置,我们可以用它来实现很多统一化处理的业务需求,比如权限认证,IP访问限制等等。

创建自定义全局过滤器类 ,实现GlobalFilter和Ordered两个接口。
(1)GlobalFilter:全局过滤拦截器
(2)Ordered:拦截器的顺序,数字越低,优先级越高

1-黑名单校验过滤器
/**
* 定义全局过滤器,会对所有路由生效
*/
@Slf4j
@Component // 让容器扫描到,等同于注册了
public class BlackListFilter implements GlobalFilter, Ordered {
	// 模拟黑名单(实际可以去数据库或者redis中查询)
	private static List<String> blackList = new ArrayList<>();
 
	static {
		blackList.add("0:0:0:0:0:0:0:1"); // 模拟本机地址
	}
	
	/**
	* 过滤器核心方法
	* @param exchange 封装了request和response对象的上下文
	* @param chain 网关过滤器链(包含全局过滤器和单路由过滤器)
	* @return
	*/
	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		// 思路:获取客户端ip,判断是否在黑名单中,在的话就拒绝访问,不在的话就放行
		ServerHttpRequest request = exchange.getRequest();
		ServerHttpResponse response = exchange.getResponse();
		
		// 从request对象中获取客户端ip
		String clientIp = request.getRemoteAddress().getHostString();
		
		// 拿着clientIp去黑名单中查询,存在的话就决绝访问
		if(blackList.contains(clientIp)) {
			// 拒绝访问,返回
			response.setStatusCode(HttpStatus.UNAUTHORIZED); // 状态码
			log.debug("=====>IP:" + clientIp + " 在⿊名单中,将被拒绝访问!");
			String data = "Request be denied!";
			DataBuffer wrap = response.bufferFactory().wrap(data.getBytes());
			return response.writeWith(Mono.just(wrap));
		}
		
		// 合法请求,放行,执行后续的过滤器
		return chain.filter(exchange);
	}
	
	/**
	* @return 过滤器的顺序(优先级),数值越小,优先级越高
	*/
	@Override
	public int getOrder() {
		return 0;
	}
}
2-跨域脚本过滤器
@Component
@ConditionalOnProperty(value = "security.xss.enabled", havingValue = "true")
public class XssFilter implements GlobalFilter, Ordered
{
    // 跨站脚本的 xss 配置,nacos自行添加
    @Autowired
    private XssProperties xss;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
    {
        ServerHttpRequest request = exchange.getRequest();
        // xss开关未开启 或 通过nacos关闭,不过滤
        if (!xss.getEnabled())
        {
            return chain.filter(exchange);
        }
        // GET DELETE 不过滤
        HttpMethod method = request.getMethod();
        if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE)
        {
            return chain.filter(exchange);
        }
        // 非json类型,不过滤
        if (!isJsonRequest(exchange))
        {
            return chain.filter(exchange);
        }
        // excludeUrls 不过滤
        String url = request.getURI().getPath();
        if (StringUtils.matches(url, xss.getExcludeUrls()))
        {
            return chain.filter(exchange);
        }
        ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange);
        return chain.filter(exchange.mutate().request(httpRequestDecorator).build());

    }

    private ServerHttpRequestDecorator requestDecorator(ServerWebExchange exchange)
    {
        ServerHttpRequestDecorator serverHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest())
        {
            @Override
            public Flux<DataBuffer> getBody()
            {
                Flux<DataBuffer> body = super.getBody();
                return body.buffer().map(dataBuffers -> {
                    DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
                    DataBuffer join = dataBufferFactory.join(dataBuffers);
                    byte[] content = new byte[join.readableByteCount()];
                    join.read(content);
                    DataBufferUtils.release(join);
                    String bodyStr = new String(content, StandardCharsets.UTF_8);
                    // 防xss攻击过滤
                    bodyStr = EscapeUtil.clean(bodyStr);
                    // 转成字节
                    byte[] bytes = bodyStr.getBytes();
                    NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
                    DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
                    buffer.write(bytes);
                    return buffer;
                });
            }

            @Override
            public HttpHeaders getHeaders()
            {
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(super.getHeaders());
                // 由于修改了请求体的body,导致content-length长度不确定,因此需要删除原先的content-length
                httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
                httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                return httpHeaders;
            }

        };
        return serverHttpRequestDecorator;
    }

    /**
     * 是否是Json请求
     * 
     * @param exchange HTTP请求
     */
    public boolean isJsonRequest(ServerWebExchange exchange)
    {
        String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
        return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
    }

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

【四】搭建Gateway网关整合Nacos

【1】添加依赖

使用的是springboot2.7.7,兼容的SpringCloud是2021.0.5版本

注意事项
(1)gateway项目不能引入spring-boot-starter-web依赖,也就不能使用HttpServletRequest和HttpServletResponse,可以引入
(2)nacos要使用bootstrap.yml,就要引入spring-cloud-starter-bootstrap
(3)

<properties>
        <allenStudy.version>1.0-SNAPSHOT</allenStudy.version>
        <java.version>1.8</java.version>
        <camunda.version>7.17.0</camunda.version>
        <mp.version>3.5.6</mp.version>
        <hutool.version>5.8.18</hutool.version>
        <modelmapper.version>3.1.1</modelmapper.version>
        <slf4j.version>1.7.36</slf4j.version>
        <freemarker.version>2.3.30</freemarker.version>
        <swagger.version>2.2.15</swagger.version>
        <springdoc.version>1.7.0</springdoc.version>
        <druid.version>1.2.23</druid.version>
        <mapstruct.version>1.5.5.Final</mapstruct.version>
        <lombok.version>1.18.30</lombok.version>
        <jedis.version>3.3.0</jedis.version>
        <redisson.version>3.6.5</redisson.version>
    </properties>
    <!-- 依赖管理:统一版本 -->
    <dependencyManagement>
        <dependencies>
            <!--SpringCloud Alibaba 微服务-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>2021.0.5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2021.0.5.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

        </dependencies>
    </dependencyManagement>
    <!-- 公共依赖(所有子模块继承) -->
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mp.version}</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>

        <!-- spring-boot-starter-mail -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

        <!-- ModelMapper -->
        <dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>${modelmapper.version}</version>
        </dependency>

        <!-- Validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- SLF4J for logging -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>

        <!--Swagger2-->
        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-models</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-ui</artifactId>
            <version>${springdoc.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>

        <!-- Nacos Discovery Starter -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2021.0.5.0</version>
        </dependency>
        <!-- Nacos Config Starter -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--2021.0.5版本起,Spring Cloud将不再默认启用bootstrap 包,需要手动引入-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>

        <!-- spring cloud gateway 依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <!--sentinel gateway-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

        <!-- 202.0.5版本的cloud,需要这个注解才能负载均衡也就是 lb:student才生效 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- Jedis客户端依赖 -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>${jedis.version}</version>
        </dependency>

        <!-- redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>${redisson.version}</version>
        </dependency>

        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-client</artifactId>
            <version>2.6.10</version>
        </dependency>

    </dependencies>

【2】配置yml

bootstrap.yml,其他数据库、redis的配置都在nacos配置中心里,不赘述

配置了两个路由,一个是单独的auth认证服务,用来实现登录注册等相关的认证功能。study学习服务,主要用来实现自己的一些学习案例

server:
  port: 9091 # 指定端口号
spring:
  main:
    ## 允许循环依赖
    allow-circular-references: true
  ## 应用配置
  application:
    ## 应用名称
    name: allen-gateway
  profiles:
    active: dev
    # active: uat1 # 通过spring.profiles.active 配置多份不同环境的配置文件
  # cloud 配置
  cloud:
    nacos:
      config:
        auto-refresh: true
        file-extension: yml ## 配置文件的后缀
#        shared-configs: ## 共享配置,通过【命名空间+分组+配置集】确定一个配置文件
#          - data-id: global-common.yml ## 配置集,即配置文件名称
#            group: global ## 分组为group
#            refresh: true ## 热刷新
#          - data-id: global-mybatisplus.yml
#            group: global
#            refresh: true
#          - data-id: global-config.yml
#            group: global
#            refresh: true
        prefix: gateway-application
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE
      routes:
        - id: auth-service # 认证服务
#          uri: http://localhost:8091
          uri: lb://allen-auth
          predicates:
            - Path=/auth/**
          filters:
            - StripPrefix=1

        # 学习模块
        - id: allen-study # 学习服务
          uri: lb://allen-study-module # 目标服务名称(模块 a 的服务名),使用 lb 前缀表示负载均衡
#          uri: http://localhost:8081 # 目标服务名称(模块 a 的服务名)
          predicates:
            - Path=/api/** # 匹配路径,案例:http://localhost:9091/api/4.2.0/employeeinfo/1896123273935491074
          filters:
            - StripPrefix=1 # 去除前缀(可选)转发后去除第一段路径'api'
            - name: BlackListUrlFilter # 自定义过滤器(黑名单过滤器)
              args:
                blacklistUrl:
                  - /user/list
#  sentinel:
#    transport:
#      dashboard: localhost:9092 # Sentinel 控制台地址
security:
  ignore:
    whites: /actuator/**, /auth/login/**,/auth/logout/**, /auth/register/**, /auth/refreshToken/**
#      port: 9093 # Sentinel 客户端与控制台通信的端口
# admin监控开放全部端点
management:
  endpoints:
    web:
      exposure:
        include: "*"
# admin监控日志
logging:
  file:
    name: logs/${spring.application.name}/info.log

bootstrap-dev.yml

spring:
  ## cloud 配置
  cloud:
    ## nacos 配置
    nacos:
      ## nacos 注册中心配置
      config:
        ## 服务器地址
        server-addr: localhost:8848
        ## 命名空间ID
        namespace: b3c27fd8-2f44-40d1-985a-9d743fa317b7
        username: nacos #登录名
        password: nacos #登录密码
        group: DEFAULT_GROUP
      discovery:
        server-addr: localhost:8848
        namespace: b3c27fd8-2f44-40d1-985a-9d743fa317b7
        username: nacos #登录名
        password: nacos #登录密码
        group: DEFAULT_GROUP

【3】启动类

将网关服务注册到Nacos
可以查看启动后的端口号,确认bootstrap.yml生效了

/**
 * 应用启动类
 *
 * @author AllenSun
 * @since 2025-03-15 13:57
 */
@SpringBootApplication
@EnableDiscoveryClient
@Log4j2
public class GatewayApplication {

    /**
     * 启动项
     *
     * @param args
     */
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(GatewayApplication.class, args);
        Environment env = context.getEnvironment();

        // 获取端口号(默认值设置为空字符串避免空指针)
        String port = env.getProperty("server.port", "未配置端口");
        String contextPath = env.getProperty("server.servlet.context-path", "");

        // 打印启动信息
        System.out.println("\n=================================");
        System.out.println("应用启动成功!访问地址: http://localhost:" + port + contextPath);
        System.out.println("=================================");
    }
}

【4】配置白名单

有些接口是不应该被拦截的,例如登录登出注册等接口,这些接口配置在yml里,然后读取在白名单实体类中

/**
 * 放行白名单配置
 * 
 * @author ruoyi
 */
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.ignore")
public class IgnoreWhiteProperties
{
    /**
     * 放行白名单配置,网关不校验此处的白名单
     */
    private List<String> whites = new ArrayList<>();

    public List<String> getWhites()
    {
        return whites;
    }

    public void setWhites(List<String> whites)
    {
        this.whites = whites;
    }
}

【5】自定义黑名单局部过滤器

package com.allen.study.filter;

import com.allen.study.common.exception.CustomRuntimeException;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

/**
 * 黑名单过滤(自定义局部过滤器)
 * @ClassName: BlackListUrlFilter
 * @Author: AllenSun
 * @Date: 2025/3/15 18:15
 */
@Component
public class BlackListUrlFilter extends AbstractGatewayFilterFactory<BlackListUrlFilter.Config>
{
    @Override
    public GatewayFilter apply(Config config)
    {
        return (exchange, chain) -> {

            String url = exchange.getRequest().getURI().getPath();
            if (config.matchBlacklist(url))
            {
                throw new CustomRuntimeException("请求地址不允许访问");
                // return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求地址不允许访问");
            }

            return chain.filter(exchange);
        };
    }


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

    public static class Config
    {
        private List<String> blacklistUrl;

        private List<Pattern> blacklistUrlPattern = new ArrayList<>();

        public boolean matchBlacklist(String url)
        {
            return !blacklistUrlPattern.isEmpty() && blacklistUrlPattern.stream().anyMatch(p -> p.matcher(url).find());
        }

        public List<String> getBlacklistUrl()
        {
            return blacklistUrl;
        }

        public void setBlacklistUrl(List<String> blacklistUrl)
        {
            this.blacklistUrl = blacklistUrl;
            this.blacklistUrlPattern.clear();
            this.blacklistUrl.forEach(url -> {
                this.blacklistUrlPattern.add(Pattern.compile(url.replaceAll("\\*\\*", "(.*?)"), Pattern.CASE_INSENSITIVE));
            });
        }
    }

}

【6】整合JWT认证,自定义认证全局过滤器

package com.allen.study.filter;

import cn.hutool.core.util.StrUtil;
import com.allen.study.common.constant.CacheConstants;
import com.allen.study.common.constant.SecurityConstants;
import com.allen.study.common.constant.TokenConstants;
import com.allen.study.common.exception.CustomRuntimeException;
import com.allen.study.common.utils.StringUtils;
import com.allen.study.common.utils.jwt.JwtUtils;
import com.allen.study.common.utils.redis.RedisUtils;
import com.allen.study.properties.IgnoreWhiteProperties;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @ClassName: JwtAuthFilter
 * @Author: AllenSun
 * @Date: 2025/3/15 22:48
 */
@Component
@Slf4j
public class JwtAuthFilter implements GlobalFilter, Ordered {

    // 排除过滤的 uri 地址,nacos自行添加
    @Autowired
    private IgnoreWhiteProperties ignoreWhite;

    @Autowired
    private RedisUtils redisUtils;

    @Autowired
    private JwtUtils jwtUtils;


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpRequest.Builder mutate = request.mutate();

        String url = request.getURI().getPath();
        log.info("开始认证token:{}",url);
        // 跳过白名单不需要验证的路径
        if (StringUtils.matches(url, ignoreWhite.getWhites())) {
            log.info("白名单路径,结束认证:{}",url);
            return chain.filter(exchange);
        }
        String token = getToken(request);
        // token令牌为空
        if (StringUtils.isEmpty(token)) {
            return unauthorizedResponse(exchange, "令牌不能为空");
        }
        // Jwt
        Claims claims = jwtUtils.extractAllClaims(token);
        if (claims == null) {
            return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
        }
        // 判断是否过期
        if (jwtUtils.isTokenExpired2(token)) {
            return unauthorizedResponse(exchange, "登录状态已过期");
        }
        // userid或者username为空的
        // String userid = jwtUtils.getUserId(claims);
        String username = claims.getSubject();
        if (StringUtils.isEmpty(username)) {
            return unauthorizedResponse(exchange, "令牌验证失败");
        }
        log.info("完成认证token:{}",url);
        // 设置用户信息到请求
        // addHeader(mutate, SecurityConstants.USER_KEY, userkey);
        // addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
        addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
        // 内部请求来源参数清除
        removeHeader(mutate, SecurityConstants.FROM_SOURCE);
        return chain.filter(exchange.mutate().request(mutate.build()).build());
    }

    private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) {
        if (value == null) {
            return;
        }
        String valueStr = value.toString();
        // String valueEncode = ServletUtils.urlEncode(valueStr);
        String valueEncode = null;
        mutate.header(name, valueEncode);
    }

    private void removeHeader(ServerHttpRequest.Builder mutate, String name) {
        mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
    }

    private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) {
        String failAuthMsg = StrUtil.format("鉴权异常:{},请求路径:{}", msg, exchange.getRequest().getPath());
        log.error(failAuthMsg);
        // return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
        throw new CustomRuntimeException(failAuthMsg);
    }

    /**
     * 获取缓存key
     */
    private String getTokenKey(String token)
    {
        return CacheConstants.LOGIN_TOKEN_KEY + token;
    }

    /**
     * 获取请求token
     */
    private String getToken(ServerHttpRequest request) {
        String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);
        // 如果前端设置了令牌前缀,则裁剪掉前缀
        if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX))
        {
            token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
        }
        return token;
    }

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

jwt的相关工具类

package com.allen.study.common.utils.jwt;

import cn.hutool.core.convert.Convert;
import com.allen.study.common.constant.SecurityConstants;
import com.allen.study.common.exception.CustomRuntimeException;
import com.allen.study.common.utils.redis.RedisUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.function.Function;

/**
 * @ClassName: JwtUtils
 * @Author: AllenSun
 * @Date: 2025/3/7 20:35
 *
 * 全局异常处理器(通常使用 @ControllerAdvice 和 @ExceptionHandler 注解)主要处理控制器方法调用过程中抛出的异常。
 * 要是 JWT 异常在过滤器或者拦截器中抛出,且这些组件不在控制器方法调用链里,全局异常处理器就无法捕获这些异常。
 */
@Component
@Slf4j
public class JwtUtils {
    // 秘钥
    private static final String SECRET_KEY = "123456654321";  // 建议存储于环境变量

    // 过期时间
    private static final long TOKEN_EXPIRATION_MS = 86400000;  // 24小时

    // redis缓存key
    private static final String TOKEN_KEY_PREFIX = "auth:token:";

    @Autowired
    private RedisUtils redisUtils;

    // 从token中提取用户名
    public String extractUsername(String token) {
        // 从token中提取声明,并返回用户名
        return extractClaim(token, Claims::getSubject);
    }

    /**
     * 判断 Token 是否过期
     * @param token JWT Token
     * @return 如果过期返回 true,否则返回 false
     */
    public boolean isTokenExpired2(String token) {
        try {
            Claims claims = parseToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            return true;
        }
    }

    /**
     * 解析 JWT Token 并获取 Claims
     * @param token JWT Token
     * @return Claims 对象
     */
    public Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
    }

    // 从token中提取过期时间
    public Date extractExpiration(String token) {
        // 调用extractClaim方法,传入token和Claims::getExpiration方法引用
        return extractClaim(token, Claims::getExpiration);
    }

    // 从token中提取指定类型的声明
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        // 从token中提取所有的claims
        final Claims claims = extractAllClaims(token);
        // 使用claimsResolver函数处理claims,并返回结果
        return claimsResolver.apply(claims);
    }

    // 从token中提取所有声明
    public Claims extractAllClaims(String token) {
        // 使用SECRET_KEY解析token,并返回声明
        Claims claims = null;
        try {
            claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
        } catch (SignatureException e) {
            log.info("无效的JWT签名:{}",e.getMessage());
            throw new CustomRuntimeException("无效的JWT签名,请重新登录");
        } catch (MalformedJwtException e) {
            log.info("无效的JWT签名:{}",e.getMessage());
            throw new CustomRuntimeException("无效的JWT签名,请重新登录");
        }

        return claims;
    }

    // 判断token是否过期
    private Boolean isTokenExpired(String token) {
        // 提取token中的过期时间
        return extractExpiration(token).before(new Date());
    }

    /**
     * 根据令牌获取用户标识
     *
     * @param token 令牌
     * @return 用户ID
     */
    public String getUserKey(String token)
    {
        Claims claims = extractAllClaims(token);
        return getValue(claims, SecurityConstants.USER_KEY);
    }

    /**
     * 根据身份信息获取键值
     *
     * @param claims 身份信息
     * @param key 键
     * @return 值
     */
    public String getValue(Claims claims, String key)
    {
        return Convert.toStr(claims.get(key), "");
    }


    /**
     * 根据令牌获取用户标识
     *
     * @param claims 身份信息
     * @return 用户ID
     */
    public String getUserKey(Claims claims)
    {
        return getValue(claims, SecurityConstants.USER_KEY);
    }

    /**
     * 根据身份信息获取用户ID
     *
     * @param claims 身份信息
     * @return 用户ID
     */
    public String getUserId(Claims claims)
    {
        return getValue(claims, SecurityConstants.DETAILS_USER_ID);
    }

    /**
     * 根据令牌获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUserName(String token)
    {
        Claims claims = extractAllClaims(token);
        return getValue(claims, SecurityConstants.DETAILS_USERNAME);
    }

    /**
     * 根据身份信息获取用户名
     *
     * @param claims 身份信息
     * @return 用户名
     */
    public String getUserName(Claims claims)
    {
        return getValue(claims, SecurityConstants.DETAILS_USERNAME);
    }

}

【7】整合Sentinel实现限流

package com.allen.study.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.allen.study.handler.SentinelFallbackHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

import javax.annotation.PostConstruct;
import java.util.HashSet;
import java.util.Set;

/**
 * @ClassName: GatewaySentinelConfig
 * @Author: AllenSun
 * @Date: 2025/3/15 16:26
 */
@Configuration
public class GatewaySentinelConfig {

    // Sentinel支持自定义异常处理。
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelFallbackHandler sentinelGatewayExceptionHandler()
    {
        return new SentinelFallbackHandler();
    }

    // @Bean
    // @Order(-1)
    // public GlobalFilter sentinelGatewayFilter()
    // {
    //     return new SentinelGatewayFilter();
    // }

    @PostConstruct
    public void doInit()
    {
        // 加载网关限流规则
        initGatewayRules();
    }

    /**
     * 网关限流规则
     */
    private void initGatewayRules()
    {
        Set<GatewayFlowRule> rules = new HashSet<>();
        // 测试验证,一分钟内访问三次系统服务出现异常提示表示限流成功。
        rules.add(new GatewayFlowRule("allen-study-module")
                .setCount(3) // 限流阈值
                .setIntervalSec(10)); // 统计时间窗口,单位是秒,默认是 1 秒
        // 可以配置多个
        rules.add(new GatewayFlowRule("allen_camunda")
                .setCount(3) // 限流阈值
                .setIntervalSec(10));
        // 加载网关限流规则
        GatewayRuleManager.loadRules(rules);
    }
}

实现限流兜底方法

package com.allen.study.handler;

import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
 * @ClassName: SentinelFallbackHandler
 * @Author: AllenSun
 * @Date: 2025/3/15 17:50
 */
public class SentinelFallbackHandler implements WebExceptionHandler
{
    private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange)
    {
        ServerHttpResponse serverHttpResponse = exchange.getResponse();
        serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        byte[] datas = "{\"code\":429,\"msg\":\"请求超过最大数,请稍后再试\"}".getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(datas);
        return serverHttpResponse.writeWith(Mono.just(buffer));
    }

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex)
    {
        if (exchange.getResponse().isCommitted())
        {
            return Mono.error(ex);
        }
        if (!BlockException.isBlockException(ex))
        {
            return Mono.error(ex);
        }
        return handleBlockedRequest(exchange, ex).flatMap(response -> writeResponse(response, exchange));
    }

    private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable)
    {
        return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
    }
}

【8】也可以使用现成redis限流

spring:
  redis:
    host: localhost
    port: 6379
  cloud:
    gateway:
      routes:
        - id: rate_limiter_route
          uri: http://backend-service
          predicates:
            - Path=/public-api/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 100  # 每秒令牌数
                redis-rate-limiter.burstCapacity: 200  # 最大突发流量
                key-resolver: "#{@remoteAddrKeyResolver}"  # 按IP限流

【五】Gateway整合Hystrix实现熔断降级

(1)添加pom依赖,使用的是hystrix
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
(2)修改yml配置,配置需要熔断降级服务

指定过滤器为Hystrix

spring:
  redis:
    host: localhost
    port: 6379
    password: 
  cloud:
    gateway:
      routes:
        # 系统模块
        - id: ruoyi-system
          uri: lb://ruoyi-system
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix=1
            # 降级配置
            - name: Hystrix
              args:
                name: default
                # 降级接口的地址
                fallbackUri: 'forward:/fallback'

上面配置包含了一个Hystrix过滤器,该过滤器会应用Hystrix熔断与降级,会将请求包装成名为fallback的路由指令RouteHystrixCommand,RouteHystrixCommand继承于HystrixObservableCommand,其内包含了Hystrix的断路、资源隔离、降级等诸多断路器核心功能,当网关转发的请求出现问题时,网关能对其进行快速失败,执行特定的失败逻辑,保护网关安全。

配置中有一个可选参数fallbackUri,当前只支持forward模式的URI。如果服务被降级,请求会被转发到该URI对应的控制器。控制器可以是自定义的fallback接口;也可以使自定义的Handler,需要实现接口org.springframework.web.reactive.function.server.HandlerFunction。

(3)实现添加熔断降级处理返回信息
/**
 * 熔断降级处理
 * 
 * @author ruoyi
 */
@Component
public class HystrixFallbackHandler implements HandlerFunction<ServerResponse>
{
    private static final Logger log = LoggerFactory.getLogger(HystrixFallbackHandler.class);

    @Override
    public Mono<ServerResponse> handle(ServerRequest serverRequest)
    {
        Optional<Object> originalUris = serverRequest.attribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
        originalUris.ifPresent(originalUri -> log.error("网关执行请求:{}失败,hystrix服务降级处理", originalUri));
        return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).contentType(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromValue(JSON.toJSONString(R.fail("服务已被降级熔断"))));
    }
}
(4)路由配置信息加一个控制器方法用于处理重定向的/fallback请求
/**
 * 路由配置信息
 * 
 * @author ruoyi
 */
@Configuration
public class RouterFunctionConfiguration
{
    @Autowired
    private HystrixFallbackHandler hystrixFallbackHandler;

    @Autowired
    private ValidateCodeHandler validateCodeHandler;

    @SuppressWarnings("rawtypes")
    @Bean
    public RouterFunction routerFunction()
    {
        return RouterFunctions
                .route(RequestPredicates.path("/fallback").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
                        hystrixFallbackHandler)
                .andRoute(RequestPredicates.GET("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
                        validateCodeHandler);
    }
}
(5)测试服务熔断降级

启动网关服务RuoYiGatewayApplication.java,访问/system/**在进行测试,会发现返回服务已被降级熔断,表示降级成功。

【六】GateWay非阻塞异步模型

【七】(旧)GateWay9527搭建过程(整合Eureka)

【1】第一步:创建子模块cloud-gateway-gateway9527,并且修改pom

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--加入熔断器的依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>

    <!--表示把这个模块注册到注册中心里去-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        <version>2.0.0.RELEASE</version>
    </dependency>

    <!--引入自己定义的API通用包,可以使用Payment支付的Entity-->
    <dependency>
        <groupId>com.atguigu.springcloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>
    <!--devtools-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <!--lombok-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>

</dependencies>

【2】第二步:修改网关服务的yml文件(将Gateway注册进Eureka)

server:
  port: 9527

spring:
  application:
    name: cloud-gateway #把这个微服务注册进7001

eureka:
  instance:
    hostname: cloud-gateway-service #微服务在注册中心的名称为cloud-gateway-service
  client:
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka

【3】第三步:没有业务类,添加启动类

@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
    public static void main(String[] args) {
        SpringApplication.run(GateWayMain9527.class,args);
    }
}

【4】第四步:YML新增网关配置内容

server:
  port: 9527

spring:
  application:
    name: cloud-gateway #把这个微服务注册进7001
  # start
  cloud:
    gateway:
      routes:
        - id: payment_routh  #路由的id,没有固定规则但要求唯一,建议配合服务器
          uri: http://localhost:8001  #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**  #断言,路径相匹配的进行路由

        - id: payment_routh2  #路由的id,没有固定规则但要求唯一,建议配合服务器
          uri: http://localhost:8001  #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**  #断言,路径相匹配的进行路由
eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka

【5】第五步:测试

依次启动7001/8001/9527
先从8001地址测试:http://localhost:8001/payment/get/2
在这里插入图片描述再从9527地址测试:http://localhost:9527/payment/get/2
在这里插入图片描述

【6】分析一下

我们不希望请求直接到达服务,所以让网关挡在服务前面,或者说网关把服务包裹起来,web请求先到达网关,由网关分配服务,这样即使有安全问题也会被网关拦截下来。有了网关就可以不用暴露8001端口,

【八】(旧)GateWay配置路由实现负载均衡

【1】基本介绍

现在使用网关9527,挡在服务前面,对服务进行分配,但是可能会有很多个服务,就需要进行负载均衡,以前是使用Ribbon,现在可以使用网关进行动态路由的配置。默认情况下,Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能

【2】启动eureka7001,服务提供者8001和8002

【3】修改pom

【4】修改yml

server:
  port: 9527

spring:
  application:
    name: cloud-gateway #把这个微服务注册进7001
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心丰台创建路由的功能,利用微服务进行路由
      routes:
        - id: payment_routh  #路由的id,没有固定规则但要求唯一,建议配合服务器
          #uri: http://localhost:8001  #匹配后提供服务的路由地址
          uri: lb://CLOUD-PAYMENT-SERVICE #匹配后提供服务的路由地址,把写死的地址换成微服务的名称
          predicates:
            - Path=/payment/get/**  #断言,路径相匹配的进行路由

        - id: payment_routh2  #路由的id,没有固定规则但要求唯一,建议配合服务器
          #uri: http://localhost:8001  #匹配后提供服务的路由地址
          uri: lb://CLOUD-PAYMENT-SERVICE #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**  #断言,路径相匹配的进行路由
eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka

【5】测试

启动9527,测试地址:http://localhost:9527/payment/lb
以前是从80端口访问8001服务的,现在直接从网关9527开始访问
会在8001服务和8002服务之间切换,达到了动态路由的效果
在这里插入图片描述在这里插入图片描述

03-08
### 网关 Gateway 技术介绍 网关Gateway),特别是指 API 网关,在分布式服务架构、微服务架构中扮演着至关重要的角色[^1]。作为系统的入口,API 网关负责接收所有的客户端请求并将其转发给适当的服务实例。这不仅简化了前端应用与后端多个微服务之间的交互过程,还允许开发者集中处理跨域资源共享(CORS)、身份验证(Authentication)等问题。 对于采用 Spring Cloud 的项目而言,Spring Cloud Gateway 成为了构建 API 网关的理想选择之一。该框架旨在提供一种简单且高效的方式来配置路由规则以及实现诸如安全性、监控/度量等功能,这一切都是通过强大的过滤器机制来完成的[^2]。 具体来说,当一个 HTTP 请求到达 Spring Cloud Gateway 后,它会依次经过三种类型的过滤器:特定于当前路由定义的过滤器、全局默认过滤器(`DefaultFilter`)全局过滤器(`GlobalFilter`)[^4]。这些过滤器可以在请求被发送到目标服务之前或响应返回给客户端之后执行各种操作,比如修改请求头信息、记录日志等。 随着技术的发展趋势,越来越多的企业倾向于使用更现代化的技术栈来进行开发工作;因此,在许多情况下,相较于 Zuul 而言,Spring Cloud Gateway 显示出了更高的性能表现发展潜力,逐渐成为事实上的标准解决方案[^3]。 ### 使用场景 - **统一接入层**:为所有外部调用者提供单一接口,隐藏内部复杂性的同时提高灵活性。 - **流量管理**:支持动态调整不同版本间的流量分配策略,便于灰度发布服务治理。 - **协议转换**:能够将来自不同源的数据格式标准化后再传递给下游服务消费。 - **认证授权**:集成 OAuth2.0 或 JWT 等第三方鉴权组件,保护资源免受未授权访问威胁。 - **熔断降级**:设置合理的超时时间重试次数,防止因单点故障引发连锁反应影响整体稳定性。 ```java // 示例代码展示如何创建简单的 Spring Cloud Gateway 应用程序 @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值