122 lines
3.8 KiB
JavaScript
122 lines
3.8 KiB
JavaScript
const express = require('express');
|
|
const jwt = require('jsonwebtoken');
|
|
const bcrypt = require('bcryptjs');
|
|
const db = require('../db');
|
|
const config = require('../config');
|
|
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];
|
|
if (!token) return res.status(401).json({ error: '未登录' });
|
|
try {
|
|
req.user = jwt.verify(token, config.jwtSecret);
|
|
next();
|
|
} catch {
|
|
res.status(401).json({ error: '登录已过期' });
|
|
}
|
|
}
|
|
|
|
// 登录
|
|
router.post('/login', (req, res) => {
|
|
const { username, password } = req.body;
|
|
|
|
if (!username || !password) {
|
|
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 };
|