API限频可以保护和提高API的服务的可用性;如果某个IP在一个时间段进行大量的访问请求(例如典型的DDos攻击),不但会影响其他用户的访问,严重的还有可能直接拖垮整个服务;
针对API限流有多种策略,Node.js可以使用Koa现成的限流模块koa-ratelimit,Java也有对应的限频实现方式(通常通过注解+AOP的方式即可实现);
本文使用Redis+Node,以相当轻量级的方式实现了针对IP的访问限频,起到了抛砖引玉的作用;
源代码:
Node+Redis实现基于IP的限流策略
废话不多说,直接来写!
建立Node项目
① 初始化Node项目
首先初始化一个node项目:
npm init --yes
添加 --yes
标志来使用默认选项;
编辑package.json文件:
{
"name": "redis_rate_limit",
"version": "1.0.0",
"author": "Jasonkay",
"license": "MIT",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js"
},
"dependencies": {
"express": "^4.17.1",
"ioredis": "^4.17.3"
}
}
项目使用到了express和ioredis;
② 创建index.js并安装依赖
创建index.js文件,并安装依赖:
npm i
③ 编辑index.js
在index.js
中创建一个/
路径的路由:
const express = require('express')
const app = express()
const port = process.env.PORT || 3000
app.post('/', (req, res) => {
res.send('Post has no rate limit!')
})
app.get('/', async (req, res) => {
res.send("Accessed precious resources!")
})
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
此时启动服务器,可以使用Get或者Post请求到页面信息;
下面我们使用Redis来添加接口限频;
使用Redis进行接口限频
首先在index.js中初始化redis连接,以及连接成功后的log输出:
const Redis = require('ioredis')
const client = new Redis({
port: process.env.REDIS_PORT || 6379,
host: process.env.REDIS_HOST || 'localhost',
password: "admin",
family: 4, // 4 (IPv4) or 6 (IPv6)
db: 0,
})
client.on('connect', () => {
console.log('Redis connected!');
});
为了方便演示限流,我们在Get请求中进行限流操作;
修改app.get
:
app.get('/', async (req, res) => {
// Check rate limit
if (!await checkPermission(req.ip)) {
res.status(429).send('Too many requests - try again later')
return
}
// allow access to resources
res.send("Accessed precious resources!")
})
处理get请求时,首先在req中取得请求的ip地址,并通过checkPermission
函数判断是否已经限频;
下面重点来看checkPermission
函数;
async function checkPermission(ip) {
// Step 1: Check ip is in block-list
if (await isBlocked(ip)) {
return false
}
// Step 2: Check rate limit
if (await isOverLimit(ip)) {
try {
await doBlock(ip);
} catch (err) {
console.error("execute doBlock err:", err)
}
return false
}
return true
}
在checkPermission
函数中:
- 首先判断IP是否已经在黑名单:在黑名单则直接返回false拒绝访问;
- 此后判断是否超过了单位时间访问次数,如果超过了
- 如果上述两个检验均通过,则返回true;
下面来分别查看isBlocked
、isOverLimit
和doBlock
函数:
const EXPIRE_SECOND = 5
const LIMIT_RATE = 20
const BLOCK_SECOND = 86400
const BLOCK_SUFFIX = '-blocked'
async function isBlocked(ip) {
return (await client.get(ip + BLOCK_SUFFIX)) > 0;
}
async function isOverLimit(ip) {
let res
try {
res = await client.incr(ip)
} catch (err) {
console.error('isOverLimit: could not increment key')
throw err
}
console.log(`${ip} has value: ${res}`)
if (res > LIMIT_RATE) {
return true
}
client.expire(ip, EXPIRE_SECOND)
}
async function doBlock(ip) {
let res;
try {
res = await client.set(ip + BLOCK_SUFFIX, 1, 'ex', BLOCK_SECOND)
} catch (err) {
console.error('doBlock: could not set key')
throw err
}
console.log(`${ip} has bend blocked: ${res}`)
}
在doBlock函数中,判断是否有IP+BLOCK_SUFFIX
对应的过期Key存在,如果存在,则说明这个IP在黑名单中;
在isOverLimit中每次访问将访问的IP对应的Key的值加一并设置过期时间窗口为EXPIRE_SECOND
,如果在EXPIRE_SECOND
时间内key的值超过了设定的LIMIT_RATE
,则直接调用doBlock
函数将IP加入黑名单;
在doBlock函数中,插入IP+BLOCK_SUFFIX
的Key,并设置过期时间(黑名单保留时间)为BLOCK_SECOND
;
至此,使用Redis进行IP限流的操作完成,下面进行测试;
接口访问测试
正确配置并安装依赖后,使用下面的命令启动项目:
npm start
显示如下:
在控制台输出:
::1 has value: 1
说明在Redis中创建了对应窗口的Key:
下面快速刷新,直到达到了窗口限频;
此时窗口显示为:
控制台显示:
::1 has value: 1
::1 has value: 2
::1 has value: 3
::1 has value: 4
::1 has value: 5
::1 has value: 6
::1 has value: 7
::1 has value: 8
::1 has value: 9
::1 has value: 10
::1 has value: 11
::1 has value: 12
::1 has value: 13
::1 has value: 14
::1 has value: 15
::1 has value: 16
::1 has value: 17
::1 has value: 18
::1 has value: 19
::1 has value: 20
::1 has value: 21
::1 has bend blocked: OK
此时Redis中可以看到::1
已经被加入黑名单:
此后再访问页面都将会显示Too many requests - try again later
;
最后,将Key::1-blocked
删除;
访问恢复正常;
总结
本文讲述了如何使用Redis实现一个基于IP的限流策略,起到了抛砖引玉的作用;
在实际项目中建议使用例如Koa框架中提供的限流模块koa-ratelimit等;
并且本例中还有许多值得优化的点,比如:
- 在响应正文或
Retry-after
Header中添加block时长,让用户知道在重试之前应该等待多少时间; - 记录达到速率限制的请求,以了解用户行为并警告恶意攻击;
- 使用其他速率限制算法或其他中间件;
附录
源代码: