ZhaoShanGeng commited on
Commit
c04dce9
·
1 Parent(s): 620492b

security: 添加登录速率限制防止暴力破解

Browse files

- 添加 IP 级别的登录尝试追踪
- 5次失败后封禁 IP 5分钟
- 15分钟窗口期内计数
- 验证输入类型和长度防止 DoS
- 记录可疑登录尝试日志

Files changed (1) hide show
  1. src/routes/admin.js +86 -0
src/routes/admin.js CHANGED
@@ -16,14 +16,100 @@ const envPath = getEnvPath();
16
 
17
  const router = express.Router();
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  // 登录接口
20
  router.post('/login', (req, res) => {
 
 
 
 
 
 
 
 
 
 
 
 
21
  const { username, password } = req.body;
22
 
 
 
 
 
 
 
 
 
 
 
23
  if (username === config.admin.username && password === config.admin.password) {
 
24
  const token = generateToken({ username, role: 'admin' });
25
  res.json({ success: true, token });
26
  } else {
 
27
  res.status(401).json({ success: false, message: '用户名或密码错误' });
28
  }
29
  });
 
16
 
17
  const router = express.Router();
18
 
19
+ // 登录速率限制 - 防止暴力破解
20
+ const loginAttempts = new Map(); // IP -> { count, lastAttempt, blockedUntil }
21
+ const MAX_LOGIN_ATTEMPTS = 5;
22
+ const BLOCK_DURATION = 5 * 60 * 1000; // 5分钟
23
+ const ATTEMPT_WINDOW = 15 * 60 * 1000; // 15分钟窗口
24
+
25
+ function getClientIP(req) {
26
+ return req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
27
+ req.headers['x-real-ip'] ||
28
+ req.connection?.remoteAddress ||
29
+ req.ip ||
30
+ 'unknown';
31
+ }
32
+
33
+ function checkLoginRateLimit(ip) {
34
+ const now = Date.now();
35
+ const attempt = loginAttempts.get(ip);
36
+
37
+ if (!attempt) return { allowed: true };
38
+
39
+ // 检查是否被封禁
40
+ if (attempt.blockedUntil && now < attempt.blockedUntil) {
41
+ const remainingSeconds = Math.ceil((attempt.blockedUntil - now) / 1000);
42
+ return {
43
+ allowed: false,
44
+ message: `登录尝试过多,请 ${remainingSeconds} 秒后重试`,
45
+ remainingSeconds
46
+ };
47
+ }
48
+
49
+ // 清理过期的尝试记录
50
+ if (now - attempt.lastAttempt > ATTEMPT_WINDOW) {
51
+ loginAttempts.delete(ip);
52
+ return { allowed: true };
53
+ }
54
+
55
+ return { allowed: true };
56
+ }
57
+
58
+ function recordLoginAttempt(ip, success) {
59
+ const now = Date.now();
60
+
61
+ if (success) {
62
+ // 登录成功,清除记录
63
+ loginAttempts.delete(ip);
64
+ return;
65
+ }
66
+
67
+ // 登录失败,记录尝试
68
+ const attempt = loginAttempts.get(ip) || { count: 0, lastAttempt: now };
69
+ attempt.count++;
70
+ attempt.lastAttempt = now;
71
+
72
+ // 超过最大尝试次数,封禁
73
+ if (attempt.count >= MAX_LOGIN_ATTEMPTS) {
74
+ attempt.blockedUntil = now + BLOCK_DURATION;
75
+ logger.warn(`IP ${ip} 因登录失败次数过多被暂时封禁`);
76
+ }
77
+
78
+ loginAttempts.set(ip, attempt);
79
+ }
80
+
81
  // 登录接口
82
  router.post('/login', (req, res) => {
83
+ const clientIP = getClientIP(req);
84
+
85
+ // 检查速率限制
86
+ const rateCheck = checkLoginRateLimit(clientIP);
87
+ if (!rateCheck.allowed) {
88
+ return res.status(429).json({
89
+ success: false,
90
+ message: rateCheck.message,
91
+ retryAfter: rateCheck.remainingSeconds
92
+ });
93
+ }
94
+
95
  const { username, password } = req.body;
96
 
97
+ // 验证输入
98
+ if (!username || !password || typeof username !== 'string' || typeof password !== 'string') {
99
+ return res.status(400).json({ success: false, message: '用户名和密码必填' });
100
+ }
101
+
102
+ // 限制输入长度防止 DoS
103
+ if (username.length > 100 || password.length > 100) {
104
+ return res.status(400).json({ success: false, message: '输入过长' });
105
+ }
106
+
107
  if (username === config.admin.username && password === config.admin.password) {
108
+ recordLoginAttempt(clientIP, true);
109
  const token = generateToken({ username, role: 'admin' });
110
  res.json({ success: true, token });
111
  } else {
112
+ recordLoginAttempt(clientIP, false);
113
  res.status(401).json({ success: false, message: '用户名或密码错误' });
114
  }
115
  });