212 lines
8.4 KiB
TypeScript
212 lines
8.4 KiB
TypeScript
|
|
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
|
import { settingsService } from './settings.service';
|
|
import { NotificationService } from './notification.service';
|
|
|
|
|
|
const notificationService = new NotificationService(); // 实例化 NotificationService
|
|
|
|
// 黑名单相关设置的 Key
|
|
const MAX_LOGIN_ATTEMPTS_KEY = 'maxLoginAttempts';
|
|
const LOGIN_BAN_DURATION_KEY = 'loginBanDuration'; // 单位:秒
|
|
|
|
// 与 ipWhitelist.middleware.ts 保持一致
|
|
const LOCAL_IPS = [
|
|
'127.0.0.1', // IPv4 本地回环
|
|
'::1', // IPv6 本地回环
|
|
'localhost' // 本地主机名 (虽然通常解析为上面两者,但也包含以防万一)
|
|
];
|
|
|
|
// 黑名单条目接口
|
|
interface IpBlacklistEntry {
|
|
ip: string;
|
|
attempts: number;
|
|
last_attempt_at: number;
|
|
blocked_until: number | null;
|
|
}
|
|
|
|
|
|
type DbIpBlacklistRow = IpBlacklistEntry;
|
|
|
|
export class IpBlacklistService {
|
|
|
|
/**
|
|
* 获取指定 IP 的黑名单记录
|
|
* @param ip IP 地址
|
|
* @returns 黑名单记录或 undefined
|
|
*/
|
|
private async getEntry(ip: string): Promise<IpBlacklistEntry | undefined> {
|
|
try {
|
|
const db = await getDbInstance();
|
|
const row = await getDbRow<DbIpBlacklistRow>(db, 'SELECT * FROM ip_blacklist WHERE ip = ?', [ip]);
|
|
return row; // Returns undefined if not found
|
|
} catch (err: any) {
|
|
console.error(`[IP Blacklist] 查询 IP ${ip} 时出错:`, err.message);
|
|
throw new Error('数据库查询失败'); // Re-throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 检查 IP 是否当前被封禁
|
|
* @param ip IP 地址
|
|
* @returns 如果被封禁则返回 true,否则返回 false
|
|
*/
|
|
async isBlocked(ip: string): Promise<boolean> {
|
|
try {
|
|
const entry = await this.getEntry(ip);
|
|
if (!entry) {
|
|
return false; // 不在黑名单中
|
|
}
|
|
// 检查封禁时间是否已过
|
|
if (entry.blocked_until && entry.blocked_until > Math.floor(Date.now() / 1000)) {
|
|
console.log(`[IP Blacklist] IP ${ip} 当前被封禁,直到 ${new Date(entry.blocked_until * 1000).toISOString()}`);
|
|
return true; // 仍在封禁期内
|
|
}
|
|
return false;
|
|
} catch (error: any) { // Catch errors from getEntry
|
|
console.error(`[IP Blacklist] 检查 IP ${ip} 封禁状态时出错:`, error.message);
|
|
return false; // 出错时默认不封禁
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 记录一次登录失败尝试
|
|
* 如果达到阈值,则封禁该 IP
|
|
* @param ip IP 地址
|
|
*/
|
|
async recordFailedAttempt(ip: string): Promise<void> {
|
|
if (LOCAL_IPS.includes(ip)) {
|
|
console.log(`[IP Blacklist] 检测到本地 IP ${ip} 登录失败,跳过黑名单处理。`);
|
|
return;
|
|
}
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
|
try {
|
|
const db = await getDbInstance();
|
|
const maxAttemptsStr = await settingsService.getSetting(MAX_LOGIN_ATTEMPTS_KEY);
|
|
const banDurationStr = await settingsService.getSetting(LOGIN_BAN_DURATION_KEY);
|
|
|
|
const maxAttempts = parseInt(maxAttemptsStr || '5', 10) || 5;
|
|
const banDuration = parseInt(banDurationStr || '300', 10) || 300;
|
|
|
|
const entry = await this.getEntry(ip);
|
|
|
|
if (entry) {
|
|
const newAttempts = entry.attempts + 1;
|
|
let blockedUntil = entry.blocked_until;
|
|
let shouldNotify = false;
|
|
|
|
if (newAttempts >= maxAttempts && !entry.blocked_until) {
|
|
blockedUntil = now + banDuration;
|
|
shouldNotify = true;
|
|
console.warn(`[IP Blacklist] IP ${ip} 登录失败次数达到 ${newAttempts} 次 (阈值 ${maxAttempts}),将被封禁 ${banDuration} 秒。`);
|
|
} else if (newAttempts >= maxAttempts && entry.blocked_until) {
|
|
console.log(`[IP Blacklist] IP ${ip} 再次登录失败,当前已处于封禁状态。`);
|
|
}
|
|
|
|
await runDb(db,
|
|
'UPDATE ip_blacklist SET attempts = ?, last_attempt_at = ?, blocked_until = ? WHERE ip = ?',
|
|
[newAttempts, now, blockedUntil, ip]
|
|
);
|
|
|
|
if (shouldNotify && blockedUntil) {
|
|
notificationService.sendNotification('IP_BLOCKED', {
|
|
ip: ip,
|
|
attempts: newAttempts,
|
|
duration: banDuration,
|
|
blockedUntil: new Date(blockedUntil * 1000).toISOString()
|
|
}).catch(err => console.error(`[IP Blacklist] 发送 IP_BLACKLISTED 通知失败 for IP ${ip}:`, err));
|
|
}
|
|
|
|
} else {
|
|
// Insert new record
|
|
let blockedUntil: number | null = null;
|
|
const attempts = 1;
|
|
let shouldNotify = false;
|
|
|
|
if (attempts >= maxAttempts) {
|
|
blockedUntil = now + banDuration;
|
|
shouldNotify = true;
|
|
console.warn(`[IP Blacklist] IP ${ip} 首次登录失败即达到阈值 ${maxAttempts},将被封禁 ${banDuration} 秒。`);
|
|
}
|
|
|
|
await runDb(db,
|
|
'INSERT INTO ip_blacklist (ip, attempts, last_attempt_at, blocked_until) VALUES (?, ?, ?, ?)',
|
|
[ip, attempts, now, blockedUntil]
|
|
);
|
|
|
|
if (shouldNotify && blockedUntil) {
|
|
notificationService.sendNotification('IP_BLOCKED', {
|
|
ip: ip,
|
|
attempts: attempts,
|
|
duration: banDuration,
|
|
blockedUntil: new Date(blockedUntil * 1000).toISOString()
|
|
}).catch(err => console.error(`[IP Blacklist] 发送 IP_BLACKLISTED 通知失败 for IP ${ip}:`, err));
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
console.error(`[IP Blacklist] 记录 IP ${ip} 失败尝试时出错:`, error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 重置指定 IP 的失败尝试次数和封禁状态 (例如登录成功后调用)
|
|
* @param ip IP 地址
|
|
*/
|
|
async resetAttempts(ip: string): Promise<void> {
|
|
try {
|
|
const db = await getDbInstance();
|
|
await runDb(db, 'DELETE FROM ip_blacklist WHERE ip = ?', [ip]);
|
|
console.log(`[IP Blacklist] 已重置 IP ${ip} 的失败尝试记录。`);
|
|
} catch (error: any) {
|
|
console.error(`[IP Blacklist] 重置 IP ${ip} 尝试次数时出错:`, error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取所有黑名单记录 (用于管理界面)
|
|
* @param limit 每页数量
|
|
* @param offset 偏移量
|
|
*/
|
|
async getBlacklist(limit: number = 50, offset: number = 0): Promise<{ entries: IpBlacklistEntry[], total: number }> {
|
|
try {
|
|
const db = await getDbInstance();
|
|
const entries = await allDb<DbIpBlacklistRow>(db,
|
|
'SELECT * FROM ip_blacklist ORDER BY last_attempt_at DESC LIMIT ? OFFSET ?',
|
|
[limit, offset]
|
|
);
|
|
const countRow = await getDbRow<{ count: number }>(db, 'SELECT COUNT(*) as count FROM ip_blacklist');
|
|
const total = countRow?.count ?? 0;
|
|
return { entries, total };
|
|
} catch (error: any) {
|
|
console.error('[IP Blacklist] 获取黑名单列表时出错:', error.message);
|
|
return { entries: [], total: 0 };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 从黑名单中删除一个 IP (解除封禁)
|
|
* @param ip IP 地址
|
|
* @returns Promise<boolean> 是否成功删除
|
|
*/
|
|
async removeFromBlacklist(ip: string): Promise<boolean> {
|
|
try {
|
|
const db = await getDbInstance();
|
|
const result = await runDb(db, 'DELETE FROM ip_blacklist WHERE ip = ?', [ip]);
|
|
if (result.changes > 0) {
|
|
console.log(`[IP Blacklist] 已从黑名单中删除 IP ${ip}。`);
|
|
return true;
|
|
} else {
|
|
console.warn(`[IP Blacklist] 尝试删除 IP ${ip},但该 IP 不在黑名单中。`);
|
|
return false;
|
|
}
|
|
} catch (error: any) {
|
|
console.error(`[IP Blacklist] 从黑名单删除 IP ${ip} 时出错:`, error.message);
|
|
throw new Error(`从黑名单删除 IP ${ip} 时出错`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 导出单例
|
|
export const ipBlacklistService = new IpBlacklistService();
|