fix security and import validation
This commit is contained in:
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user