Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 18a24e0

Browse files
feat: support rate limit
1 parent 6071f22 commit 18a24e0

File tree

8 files changed

+98
-15
lines changed

8 files changed

+98
-15
lines changed

‎front/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
"less": "^2.7.3",
7777
"less-loader": "^4.0.5",
7878
"lint-staged": "10.5.4",
79-
"lru-cache": "^4.1.1",
79+
"lru-cache": "^7.5.1",
8080
"md5": "^2.2.1",
8181
"mongoose": "^4.13.9",
8282
"morgan": "^1.9.0",

‎front/server.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const express = require('express')
22
const path = require('path')
3-
const LRU = require('lru-cache')
3+
const lruCache = require('lru-cache')
44
const fs = require('fs')
55
const cookieParser = require('cookie-parser')
66
const bodyParser = require('body-parser')
@@ -12,10 +12,14 @@ const compression = require('compression')
1212
const { createBundleRenderer } = require('vue-server-renderer')
1313
const { startSchedule } = require('./server/utils/schedule')
1414
const template = fs.readFileSync('./src/index.template.html', 'utf-8')
15+
const ratelimit = require('./server/middleware/ratelimit')
1516
const isProd = process.env.NODE_ENV === 'production'
1617
const server = express()
1718
const resolve = (file) => path.resolve(__dirname, file)
18-
server.use(logger('dev')) //日志记录中间件,将请求信息打印在控制台
19+
// 开启流控
20+
server.use('/api', ratelimit)
21+
// 日志记录中间件
22+
server.use(logger('dev'))
1923
server.use(bodyParser.json())
2024
server.use(bodyParser.urlencoded({ extended: true }))
2125
server.use(cookieParser())
@@ -26,15 +30,18 @@ server.set('views', [path.join(__dirname, 'dist'), path.join(__dirname, 'static'
2630
server.engine('.html', ejs.__express)
2731
server.set('view engine', 'ejs')
2832
route(server)
33+
34+
const LRU = new lruCache({
35+
max: 1000,
36+
ttl: 1000 * 60 * 15
37+
})
38+
2939
function createRenderer(bundle, options) {
3040
return createBundleRenderer(
3141
bundle,
3242
Object.assign(options, {
3343
template: template,
34-
cache: LRU({
35-
max: 1000,
36-
maxAge: 1000 * 60 * 15
37-
}),
44+
cache: LRU,
3845
basedir: resolve('./dist'),
3946
runInNewContext: false
4047
})

‎front/server/api/visitor.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
/**
22
* @desc 访客信息处理
3+
* @author justJokee
34
*/
45

56
const express = require('express')
67
const { Octokit } = require('@octokit/core')
78
const api = require('../http/server-api')
89
const router = express.Router()
910
const db = require('../db/')
10-
const secret = require('../db/secret')
11+
const {db: secret} = require('../db/secret')
1112

1213
// 存储访客信息
1314
router.post('/api/front/visitor/save', async (req, res) => {
@@ -81,13 +82,13 @@ router.get('/login_github', (req, res) => {
8182
}
8283
api
8384
.post('https://github.com/login/oauth/access_token', JSON.parse(JSON.stringify(params)))
84-
.then(fullData => {
85+
.then((fullData) => {
8586
const arr1 = fullData.split('&')
8687
const arr2 = arr1[0].split('=')
8788
const token = arr2[1]
8889
return token
8990
})
90-
.then(async token => {
91+
.then(async (token) => {
9192
let userInfo = {}
9293
const octokit = new Octokit({ auth: `${token}` })
9394
const info = await octokit.request('GET /user')

‎front/server/middleware/ratelimit.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* @desc 限流中间件(简单实现)
3+
* @author justJokee
4+
*/
5+
6+
const lruCache = require('lru-cache')
7+
const getIp = require('../utils/getIp')
8+
const { highLimitApis, HEIGHLIMIT, HEIGHLIMITTL } = require('../utils/highLimitApis')
9+
// 每个ip一分钟最大限制100次请求
10+
const LIMIT = 100
11+
const LRU = new lruCache({
12+
max: 6000,
13+
// 默认时间窗口1分钟
14+
ttl: 1000 * 60
15+
// 过期后立即删除 Note that this may significantly degrade performance
16+
// ttlAutopurge: true
17+
})
18+
19+
module.exports = function ratelimit(req, res, next) {
20+
const ip = getIp(req)
21+
const url = `${req.baseUrl}${req.url}`
22+
const key = `${ip}:${url}`
23+
const blackKey = `black:${ip}:${url}`
24+
const value = LRU.get(key)
25+
const isHeighLimitApi = highLimitApis.includes(url)
26+
const limit = isHeighLimitApi ? HEIGHLIMIT : LIMIT
27+
const option = isHeighLimitApi ? { ttl: HEIGHLIMITTL } : {}
28+
29+
// cache中存在当前ip对应的接口信息
30+
if (value) {
31+
if (value < limit) {
32+
LRU.set(key, value + 1, option)
33+
next()
34+
}
35+
// 请求次数超限
36+
else {
37+
LRU.delete(key)
38+
LRU.set(blackKey, Date.now())
39+
// 返回限流状态
40+
response(res)
41+
}
42+
}
43+
// cache中不存在当前请求ip信息
44+
else {
45+
// ip已经在黑名单
46+
if (LRU.get(blackKey)) {
47+
// 返回限流状态
48+
response(res)
49+
} else {
50+
LRU.set(key, 1, option)
51+
next()
52+
}
53+
}
54+
}
55+
56+
function response(res) {
57+
res.json({
58+
status: 429,
59+
info: '访问次数超限,请稍后再试'
60+
})
61+
}

‎front/server/utils/highLimitApis.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @desc 限制高频访问的api名单
3+
* 1. 时间窗口 10s
4+
* 2. 限制次数 3
5+
* @author justJokee
6+
*/
7+
8+
exports.HEIGHLIMIT = 3
9+
exports.HEIGHLIMITTL = 1000 * 10
10+
11+
exports.highLimitApis = ['/api/front/comments/save', '/api/front/messageBoard/save']

‎front/src/entry-client.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ router.onReady(() => {
6363
activated.map(async (Component) => {
6464
if (Component.asyncData) {
6565
const res = await Component.asyncData({ store, route: to })
66-
Component.__COMPONENT_ASYNCDATA__ = res
66+
Component.__COMPONENT_ASYNCDATA__ = res||{}
6767
}
6868
})
6969
)

‎front/src/entry-server.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createApp } from './app'
22

3-
export default context => {
3+
export default (context) => {
44
const { app, router, store } = createApp()
55
const meta = app.$meta()
66
return new Promise((resolve, reject) => {
@@ -14,7 +14,7 @@ export default context => {
1414
// 对所有匹配的路由组件调用 `asyncData()`
1515
try {
1616
const componentRes = await Promise.all(
17-
matchedComponents.map(async Component => {
17+
matchedComponents.map(async (Component) => {
1818
if (Component.asyncData) {
1919
const res = await Component.asyncData({
2020
store,
@@ -25,13 +25,13 @@ export default context => {
2525
}
2626
})
2727
)
28-
const __COMPONENT_ASYNCDATA__ = componentRes.map(eRes => {
28+
const __COMPONENT_ASYNCDATA__ = componentRes.map((eRes) => {
2929
if (eRes) {
3030
const { res, Component } = eRes
3131
// 将路由组件的 asyncData 返回值挂载到组件实例的构造项上
3232
// 用于 data & asyncData 的合并策略
3333
Component.__COMPONENT_ASYNCDATA__ = res
34-
return res
34+
return res||{}
3535
}
3636
})
3737

‎front/src/utils/errorCode.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export function errorCode(code) {
1313
case 101:
1414
_message('warning', '您已经点过赞了 ~')
1515
break
16+
case 429:
17+
_message('warning', '访问次数超限,请稍后再试 ~')
18+
break
1619
}
1720
}
1821

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /