【基于spring-cloud-gateway实现自己的网关过滤器】

ops/2024/10/21 6:04:17/

gateway_0">基于spring-cloud-gateway实现自己的网关过滤器

gateway_custom_starter_1">spring cloud gateway custom starter

自定义非阻塞式反应网关服务,集成鉴权、限流、响应的增强处理等等
  • 环境要求
    <properties><java.version>17</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.target>17</maven.compiler.target><maven.compiler.source>17</maven.compiler.source><java.source.version>17</java.source.version><java.target.version>17</java.target.version><spring-boot.version>3.1.12</spring-boot.version><spring-cloud.version>2022.0.5</spring-cloud.version><commons.pool2.version>2.12.0</commons.pool2.version><redisson.version>3.34.1</redisson.version><com.fastjson.jackson.version>2.17.2</com.fastjson.jackson.version><commons.lang3.version>3.16.0</commons.lang3.version><lombok.version>1.18.32</lombok.version></properties>
    
  • GatewayFilter 路由过滤器
    • TokenFilterGatewayFilterFactory,鉴权处理
    • 配置示例:
    spring:cloud:gateway:routes:- id: testRouteuri: http://127.0.0.1:8080predicates:- Path=/test/**filters:- name: TokenFilterargs:requestHeaderKey: auth
    
    • RlimterGatewayFilterFactory,自定义key,比如:我们想根据不同用户去做自定义限流,那么我们可以在自己的网关过滤工厂里面将limterKey设置为根据请求头自定义的用户标识,来进行自定义的配置。相比重写spring-cloud-gateway里面自定义的keyResolver和RedisLimiter相对容易一些。
      • 配置示例:
      spring:cloud:gateway:routes:- id: route_testuri: http://192.168.1.1:8091predicates:- Path=/from/requestIds/to/appNamesfilters:- name: RLimterargs:limterKey: v1_10 #限流所需要的keyrate: 5  #每秒允许的请求数crust: 0 #每秒令牌桶的填充数
      
      • 更多路由过滤器扩展中。。。
    自定义网关过滤工厂实现,TokenFilterGatewayFilterFactory和RlimterGatewayFilterFactory,以自定义限流器RlimterGatewayFilterFactory为例
  • 首先定义网关过滤工厂功能接口,限流的key,速率和桶的大小,我们是按照spring-cloud-gateway内部限流的实现改编而来的,通过调用lua脚本,采用redis的令牌桶算法做限流
public interface RLimter {Mono<Response> isAllowed(String limitKey, String rate, String crust);@Setter@Getter@NoArgsConstructor@AllArgsConstructorclass Response {private boolean allowed;}
}
  • 限流功能组件的注入和声明,注入我们需要的Bean,注入RedisScript,调用自定义的lua脚本,以及StringRedisTemplate,因为下面这段代码我是将整个网关作为的启动器,所以限流的实现类也一并交给spring管理了,RRedisRateLimiter
@Configuration
public class RLimterAutoConfiguration {@Bean(name = "rredisRequestRateLimiterScript")public RedisScript<?> redisRequestRateLimiterScript() {DefaultRedisScript redisScript = new DefaultRedisScript();redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("script/request_rate_limiter.lua")));redisScript.setResultType(List.class);return redisScript;}@Bean(name = "rredisRateLimiter")public RRedisRateLimiter redisRateLimiter(@Qualifier("rredisRequestRateLimiterScript") RedisScript<List<Long>> redisRequestRateLimiterScript, StringRedisTemplate redisTemplate) {return new RRedisRateLimiter(redisTemplate, redisRequestRateLimiterScript);}@Beanpublic RLimterGatewayFilterFactory rLimterGatewayFilterFactory(@Qualifier("rredisRateLimiter") RRedisRateLimiter redisRateLimiter) {return new RLimterGatewayFilterFactory(redisRateLimiter);}
}
  • 上面代码中提到的new ClassPathResource(“script/request_rate_limiter.lua”),工程resources目录下即可,也是摘抄自spring-cloud-gateway内部的限流脚本,原封不动
redis.replicate_commands()local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
-- redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])local fill_time = capacity / rate
local ttl = math.floor(fill_time * 2)-- for testing, it should use redis system time in production
if now == nil thennow = redis.call('TIME')[1]
end--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
--redis.log(redis.LOG_WARNING, "now " .. now)
--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
--redis.log(redis.LOG_WARNING, "ttl " .. ttl)local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil thenlast_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil thenlast_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed thennew_tokens = filled_tokens - requestedallowed_num = 1
end--redis.log(redis.LOG_WARNING, "delta " .. delta)
--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)if ttl > 0 thenredis.call("setex", tokens_key, ttl, new_tokens)redis.call("setex", timestamp_key, ttl, now)
end-- return { allowed_num, new_tokens, capacity, filled_tokens, requested, new_tokens }
return { allowed_num, new_tokens }
  • 然后是限流实现类,通过传入上面lua脚本需要的四个参数即可,lua脚本中requested为每次从桶里面取出的令牌数,这个默认为1,此处不关注这个,默认值即可。
public class RRedisRateLimiter implements RLimter {private static final Logger logger = LoggerFactory.getLogger(RedisRateLimiter.class);private final StringRedisTemplate redisTemplate;private final RedisScript<List<Long>> script;public RRedisRateLimiter(StringRedisTemplate redisTemplate, RedisScript<List<Long>> script) {this.redisTemplate = redisTemplate;this.script = script;}static List<String> getKeys(String id) {String prefix = "request_rate_limiter.{" + id;String tokenKey = prefix + "}.tokens";String timestampKey = prefix + "}.timestamp";return Arrays.asList(tokenKey, timestampKey);}@Overridepublic Mono<Response> isAllowed(String key, String rate, String crust) {List<String> keys = getKeys(key);List<Long> exec;try {exec = this.redisTemplate.execute(this.script, keys, rate, crust, "", "1");} catch (Throwable throwable) {logger.error("Error calling rate limiter lua", throwable);exec = Arrays.asList(1L, -1L);}assert exec != null;boolean allowed = exec.get(0) == 1L;return Mono.just(new Response(allowed));}
}
  • 最后就是定义我们自己的自定义限流网关工厂了,通过继承spring-cloud-gateway的一个父类,帮助我们加载自定义的网关过滤工厂,AbstractGatewayFilterFactory,父类支持传入我们的自定义配置参数,Config,通过泛型参数定义自己的配置类,并在构造中传入。
public class RLimterGatewayFilterFactory extends AbstractGatewayFilterFactory<RLimterGatewayFilterFactory.Config2> {private final RRedisRateLimiter redisRateLimiter;public RLimterGatewayFilterFactory(RRedisRateLimiter redisRateLimiter) {super(Config2.class);this.redisRateLimiter = redisRateLimiter;}@Overridepublic GatewayFilter apply(Config2 config) {return (exchange, chain) -> redisRateLimiter.isAllowed(config.limterKey, config.rate, config.crust).flatMap(response -> {if (response.isAllowed()) {return chain.filter(exchange);} else {ServerWebExchangeUtils.setResponseStatus(exchange, config.getStatusCode());return exchange.getResponse().setComplete();}});}@Setter@Getterpublic static class Config2 implements HasRouteId {private String routeId;private String limterKey = "default";private String rate = "1";private String crust = "1";private HttpStatus statusCode = HttpStatus.TOO_MANY_REQUESTS;@Overridepublic String getRouteId() {return routeId;}@Overridepublic void setRouteId(String routeId) {this.routeId = routeId;}}
}
  • 这里我只是写了个简单的实例,如果实现自定义的限流,还需要在网关工厂里面,通过exchange拿到请求体来进行相应的key的解析,结合业务实现自定义的限流。配置可以参考一开始的yaml配置示例。可以测试当把令牌桶设置为0时,会给出TOO MANY REQUEST的429状态码。
全局过滤器案例
  • 首先定义自己的上下文对象
@Setter
@Getter
public class RequestContext {private String requestId;private long requestStartTime;private String requestIp;
}
  • 全局过滤器实现
@Component
public class RequestContextFilter implements WebFilter, Ordered {private static final Logger logger = LoggerFactory.getLogger(RequestContextFilter.class);@Overridepublic int getOrder() {return OrderConstant.REQUEST_CONTEXT_ORDER;}@Overridepublic Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {long requestStartTime = SystemClock.now();String requestId = generateRequestId();ServerHttpRequest request = exchange.getRequest();String uri = request.getURI().getRawPath();String requestIp = IpUtils.tryGetRealIp(request);exchange.getResponse().getHeaders().add("requestId", requestId);logger.info("request start,requestId:{}, requestUri:{},ip:{},requestStartTime:{}", requestId, uri, requestIp, requestStartTime);RequestContext context = new RequestContext();context.setRequestId(requestId);context.setRequestStartTime(requestStartTime);context.setRequestIp(requestIp);return chain.filter(exchange).contextWrite(Context.of(RequestContext.class, context)).doOnEach(signal -> {long requestEndTime = SystemClock.now();if (signal.isOnComplete()) {logger.info("request end,requestId:{},response:{},requestEndTime:{},耗时ms:{}", requestId, exchange.getResponse().getStatusCode(), requestEndTime, (requestEndTime - requestStartTime));}if (signal.isOnError()) {logger.info("request end,requestId:{},error:{},requestEndTime:{},耗时ms:{}", requestId, signal.getThrowable(), requestEndTime, (requestEndTime - requestStartTime));}});}private String generateRequestId() {return UUID.randomUUID().toString().replaceAll("-", "");}
}

http://www.ppmy.cn/ops/119791.html

相关文章

Spring Boot入门到精通:网上购物商城系统

第3章 系统分析 3.1 可行性分析 在系统开发之初要进行系统可行分析&#xff0c;这样做的目的就是使用最小成本解决最大问题&#xff0c;一旦程序开发满足用户需要&#xff0c;带来的好处也是很多的。下面我们将从技术上、操作上、经济上等方面来考虑这个系统到底值不值得开发。…

java 抽奖程序结合数据库,redis实现

数据库脚本&#xff1a; /*** SET NAMES utf8mb4;* SET FOREIGN_KEY_CHECKS 0;** -- ----------------------------* -- Table structure for prizes* -- ----------------------------* DROP TABLE IF EXISTS prizes;* CREATE TABLE prizes (* id int NOT NULL AUTO_INCRE…

RLHF 的启示:微调 LSTM 能更好预测股票?

作者:老余捞鱼 原创不易,转载请标明出处及原作者。 写在前面的话: 在财务预测领域,准确预测股票价格是一项具有挑战性但至关重要的任务。传统方法通常难以应对股票市场固有的波动性和复杂性。这篇文章介绍了一种创新方法,该方法将长短期记忆 (LSTM) 网络与基于评…

DRF实操学习——文章和评论的设计

DRF实操学习——文章和评论的设计 1.文章表的设计2.文章表接口演示基础权限创建文章修改文章删除文章浏览所有文章 3.评论表的设计4.评论表接口演示1. 查询指定文章下的所有评论 1.文章表的设计 创建一个community的app 在settings中 完成注册 定义模型 创建文章表 from djan…

【Unity服务】如何使用Unity Version Control

Unity上的线上服务有很多&#xff0c;我们接触到的第一个一般就是Version Control&#xff0c;用于对项目资源的版本管理。 本文介绍如何为项目添加Version Control&#xff0c;并如何使用&#xff0c;以及如何将项目与Version Control断开链接。 其实如果仅仅是对项目资源进…

Clickhouse分布式表初体验

ClickHouse的分布式表是一种特殊类型的表&#xff0c;它允许你跨多个节点进行数据的查询和写入操作。以下是创建分布式表的步骤和案例&#xff1a; 1. 创建本地表&#xff1a; 在集群的每个节点上创建一个本地表&#xff0c;可以使用ReplicatedMergeTree系列引擎来实现数据…

电脑录屏怎么录视频和声音?苹果macOS、windows10都可以用的原神录屏工具来啦

在当今数字化时代&#xff0c;电脑录屏已经成为一项非常实用的技能&#xff0c;无论是制作教学视频、记录游戏精彩瞬间&#xff0c;还是进行线上会议演示&#xff0c;都离不开高质量的录屏。那么&#xff0c;电脑录屏怎么录视频和声音呢&#xff1f;今天就为大家详细介绍一下&a…

【数据库】 MongoDB 查看当前用户的角色和权限

在 MongoDB 中&#xff0c;可以通过一些简单的命令查看当前用户的角色和权限。这对于理解用户的访问能力和管理用户权限至关重要。 1. 使用 MongoDB Shell 查看角色和权限 1.1 查看当前数据库用户 要查看当前数据库中的所有用户及其角色&#xff0c;可以使用以下命令&#x…