fix security and import validation

This commit is contained in:
lizhilun
2026-07-01 16:13:47 +08:00
parent 6467048f38
commit e69930799c
14 changed files with 498 additions and 163 deletions
+80
View File
@@ -7,6 +7,43 @@ const logger = require('../utils/logger');
const router = express.Router();
const loginAttempts = new Map();
const LOGIN_WINDOW_MS = 15 * 60 * 1000;
const LOGIN_MAX_FAILURES = 5;
function normalizeLoginKey(req, username) {
return `${req.ip || req.socket.remoteAddress || 'unknown'}:${String(username || '').trim().toLowerCase()}`;
}
function getLoginAttempt(key) {
const now = Date.now();
const attempt = loginAttempts.get(key);
if (!attempt || attempt.resetAt <= now) {
const fresh = { failures: 0, resetAt: now + LOGIN_WINDOW_MS };
loginAttempts.set(key, fresh);
return fresh;
}
return attempt;
}
function clearExpiredLoginAttempts() {
const now = Date.now();
for (const [key, attempt] of loginAttempts) {
if (attempt.resetAt <= now) loginAttempts.delete(key);
}
}
function recordFailedLogin(key) {
const attempt = getLoginAttempt(key);
attempt.failures += 1;
if (loginAttempts.size > 1000) clearExpiredLoginAttempts();
return attempt;
}
function resetLoginAttempts(key) {
loginAttempts.delete(key);
}
// JWT 中间件
function auth(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
@@ -27,15 +64,58 @@ router.post('/login', (req, res) => {
return res.status(400).json({ error: '用户名和密码不能为空' });
}
const loginKey = normalizeLoginKey(req, username);
const attempt = getLoginAttempt(loginKey);
if (attempt.failures >= LOGIN_MAX_FAILURES) {
const retryAfter = Math.ceil((attempt.resetAt - Date.now()) / 1000);
res.setHeader('Retry-After', String(Math.max(retryAfter, 1)));
return res.status(429).json({ error: '登录失败次数过多,请稍后再试' });
}
const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);
if (!user || !bcrypt.compareSync(password, user.password)) {
recordFailedLogin(loginKey);
logger.warn(`登录失败: ${username}`);
return res.status(401).json({ error: '用户名或密码错误' });
}
resetLoginAttempts(loginKey);
const token = jwt.sign({ id: user.id, username: user.username }, config.jwtSecret, { expiresIn: config.jwtExpiresIn });
logger.info(`用户登录: ${username}`);
res.json({ token, username: user.username });
});
// 注册
router.post('/register', (req, res) => {
if (!config.enableRegistration) {
return res.status(403).json({ error: '注册已关闭' });
}
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: '用户名和密码不能为空' });
}
if (username.length < 2 || username.length > 20) {
return res.status(400).json({ error: '用户名长度需在2-20个字符之间' });
}
if (password.length < 6) {
return res.status(400).json({ error: '密码长度不能少于6个字符' });
}
const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username);
if (existing) {
return res.status(409).json({ error: '用户名已存在' });
}
const hash = bcrypt.hashSync(password, 10);
const result = db.prepare('INSERT INTO users (username, password) VALUES (?, ?)').run(username, hash);
logger.info(`新用户注册: ${username}`);
const token = jwt.sign({ id: result.lastInsertRowid, username }, config.jwtSecret, { expiresIn: config.jwtExpiresIn });
res.json({ token, username });
});
module.exports = { router, auth };