# Limitation
**Repository Path**: bruce6213/limitation
## Basic Information
- **Project Name**: Limitation
- **Description**: 接口请求限流以及限制客户端IP
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2024-12-30
- **Last Updated**: 2024-12-30
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 限制请求IP
## 直连服务器
```
request.getRemoteAddr();
```
## 存在中间代理服务器
```
request.getHeader("X-Forwarded-For");
```
但请求头中的X-Forwarded-For字段非常容易被伪造(Postman),故还不能直接使用。
### 原理
一般的客户端(例如浏览器)发送HTTP请求是没有`X-Forwarded-For`头的,当请求到达第一个代理服务器时,代理服务器会加上`X-Forwarded-For`请求头,并将值设为客户端的IP地址(也就是最左边第一个值),后面如果还有多个代理,会依次将IP追加到`X-Forwarded-For`头最右边,最终请求到达Web应用服务器,应用通过获取`X-Forwarded-For`头取左边第一个IP即为客户端真实IP。但是如果客户端在发起请求时,请求头上带上一个伪造的`X-Forwarded-For`,由于后续每层代理只会追加而不会覆盖,那么最终到达应用服务器时,获取的左边第一个IP地址将会是客户端伪造的IP。
### 核心步骤
- 找到最外层代理服务器,添加如下配置:
```
// $remote_addr是获取的是直接TCP连接的客户端IP(类似于Java中的request.getRemoteAddr()),这个是无法伪造的
// 即使客户端伪造也会被覆盖掉,而不是追加
proxy_set_header X-Forwarded-For $remote_addr;
```
- 在其他代理服务器上添加如下配置:
```
// 追加字段
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
```
- 后端代码相关逻辑判断
```
public String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
// 处理多个代理服务器的情况
return ip.split(",")[0].trim();
}
return request.getRemoteAddr();
}
```
## 参考链接
[博客园](https://www.cnblogs.com/irvin-chen/p/14210508.html)
# 接口限流
## 固定窗口
**缺点:**存在边界问题;例如时间窗口为5秒,限流200个请求,如果前四秒没有请求,在第四秒到第六秒之间就有可能来了400个请求,导致违反了5秒内只允许200个请求的限制。

## 滑动窗口
只是表面解决了

## 漏桶算法
**本质:**将外部请求比作注入漏桶的水,漏桶会存储一定水量并以固定速率出水,即匀速通过请求,如果请求量超过漏桶容量则会被丢弃,消息中间件就是采用漏桶算法的思想。
**缺点:**虽然可以用漏桶出口的固定速率平滑突增流量,但也正是由于固定速率,使得在流量较小的时候也无法更快的处理请求。

## 令牌桶算法
Guava的RateLimiter
```
com.google.guava
guava
31.0.1-jre
```

## 方案一
从第一次请求开始,通过redis 对请求进行计数并开始设置过期时间;以后的每一次请求都会去判断是否超过了请求次数。但该方案存在明显存在以下**两个不足**:
1. 在更新redis的value时无法同步过期时间,即存在一定的时间间隔,导致实际过期时间比预定过期时间要长。
2. 过期时间何时开始倒计时取决于第一次请求的到达时间:即**静态限流**,只能限定固定区间内的访问次数。
```
if (redisUtil.hasKey(key.toString())) {
// 当前已访问次数
int sum = (int) redisUtil.getCacheObject(key.toString());
if (sum >= count) {
log.error("超过请求次数,请稍后再试, 过期时间:{}", redisUtil.getExpire(key.toString()));
throw new LimitationException("超过请求次数,请稍后再试");
} else {
// 修改redis的vaule,但不修改过期时间
// redisUtil.getExpire获取的是分钟
Date date = new Date(System.currentTimeMillis() + redisUtil.getExpire(key.toString()) * 1000);
redisUtil.setCacheObject(key.toString(), sum + 1);
// 为什么一直是当前时间: 因为你在上一步刚没有设置过期时间
redisUtil.expireAt(key.toString(), date);
log.info("key剩余过期时间:{}", redisUtil.getExpire(key.toString()));
}
} else {
// 第一次访问
redisUtil.setCacheObject(key.toString(), 1, time, TimeUnit.MILLISECONDS);
}
```
## 方案二
对每一次请求都设置一个key,并且设置过期时间;通过计算当前时间内redis中的key的个数(模糊查询)来判断该请求是否被接受。这样既解决了过期时间的同步问题,也实现了区间内访问次数的控制,达到了正在意义上的限流措施(**动态限流**:过期key不会计算在内)。
但该方案有**一个明显的缺陷**是会导致key过多,即如果间隔时间可能会很长,导致key一直没有过期。
```
Collection keys = redisUtil.keys("*" + key.toString() + "*");
int size = keys.size() + 1;
if (size > count) {
log.error("超过请求次数,请稍后再试; 请求方法:{}", methodName);
throw new LimitationException("超过请求次数,请稍后再试");
} else {
/**
* 区间范围内限流
*/
redisUtil.setCacheObject(key.toString()+size, size, time, TimeUnit.MILLISECONDS);
}
```
## 方案三
使用redis自带的INCR命令:value自增但不影响过期时间;**但该方案能是静态限流**
```
if (redisUtil.hasKey(key.toString())){
// 不是第一次访问
int sum = (int) redisUtil.getCacheObject(key.toString());
if (sum >= count){
log.error("超过请求次数,请稍后再试, 过期时间:{}", redisUtil.getExpire(key.toString()));
throw new LimitationException("超过请求次数,请稍后再试");
}else{
redisUtil.incr(key.toString());
}
}else{
// 第一次访问
redisUtil.setCacheObject(key.toString(), 1, time, TimeUnit.MILLISECONDS);
}
```
## 方案四(推荐)
使用redis的列表存储每次访问的时间戳,每次请求到达先清理“过期”的元素(**滑动窗口**)
```
public boolean checkLimitation(String key, long time, int count, Integer interval){
List