const express = require('express'); const https = require('https'); const cors = require('cors'); const path = require('path'); const fs = require('fs'); const morgan = require('morgan'); const rateLimit = require('express-rate-limit'); const logger = require('./utils/logger'); const auditLogger = require('./middleware/auditLogger'); const { checkDiskSpace } = require('./utils/diskMonitor'); require('dotenv').config(); const app = express(); const PORT = process.env.PORT || 3000; // 信任反向代理 (Nginx) // 必须设置此项,否则在 Nginx 后端运行时,req.ip 永远是 127.0.0.1,导致限流策略对所有用户生效(误伤) app.set('trust proxy', 1); // 中间件 app.use(cors({ origin: process.env.CORS_ORIGIN || 'http://localhost:3000', credentials: true })); app.use(express.json()); // 定义脱敏的 URL token morgan.token('safe-url', (req) => { let url = req.originalUrl || req.url; try { // 隐藏 token, password, code 等敏感参数 url = url.replace(/([?&])(token|password|code)=([^&]+)/gi, '$1$2=***'); } catch (e) { // 忽略错误 } return url; }); // HTTP 请求日志 (使用自定义格式,替换 :url 为 :safe-url) app.use(morgan(':remote-addr - :remote-user [:date[clf]] ":method :safe-url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"', { stream: logger.stream })); // 全局审计日志 app.use(auditLogger); // 限流策略配置 // 1. 全局限流:每个IP每分钟最多100次请求 const globalLimiter = rateLimit({ windowMs: 1 * 60 * 1000, // 1分钟 max: 100, // 限制每个IP 100次请求 standardHeaders: true, // 返回 `RateLimit-*` 头信息 legacyHeaders: false, // 禁用 `X-RateLimit-*` 头信息 message: { message: '请求过于频繁,请稍后再试' } }); // 2. 认证接口限流:每个IP每分钟最多10次请求 (防暴力破解) const authLimiter = rateLimit({ windowMs: 1 * 60 * 1000, // 1分钟 max: 10, // 限制每个IP 10次请求 standardHeaders: true, legacyHeaders: false, message: { message: '登录/注册尝试过于频繁,请1分钟后再试' } }); // 应用全局限流 app.use('/api/', globalLimiter); // 安全策略:禁止移动端访问 app.use((req, res, next) => { const userAgent = req.headers['user-agent'] || ''; // 匹配常见的移动设备标识 const isMobile = /mobile|android|iphone|ipad|phone/i.test(userAgent); if (isMobile) { res.status(403).send(` 访问被拒绝

🚫 访问被拒绝

根据本单位安全规定,本系统禁止使用移动设备访问。

请使用办公电脑访问。

`); return; } next(); }); // 确保上传目录存在 const uploadDir = process.env.UPLOAD_DIR || 'uploads'; let absoluteUploadDir; if (path.isAbsolute(uploadDir)) { absoluteUploadDir = uploadDir; } else { absoluteUploadDir = path.join(__dirname, uploadDir); } if (!fs.existsSync(absoluteUploadDir)) { fs.mkdirSync(absoluteUploadDir, { recursive: true }); } const DISK_MONITOR_ENABLED = String(process.env.DISK_MONITOR_ENABLED || 'true').toLowerCase() !== 'false'; const DISK_CHECK_INTERVAL_MINUTES = Number(process.env.DISK_CHECK_INTERVAL_MINUTES || 60); const DISK_CHECK_INTERVAL_MS = Number.isFinite(DISK_CHECK_INTERVAL_MINUTES) && DISK_CHECK_INTERVAL_MINUTES > 0 ? DISK_CHECK_INTERVAL_MINUTES * 60 * 1000 : 60 * 60 * 1000; // 启动磁盘监控(默认每 60 分钟检查一次) if (DISK_MONITOR_ENABLED) { setInterval(() => { checkDiskSpace(); }, DISK_CHECK_INTERVAL_MS); // 启动时立即检查一次 checkDiskSpace(); } // 导入路由 const authRoutes = require('./routes/auth'); const achievementRoutes = require('./routes/achievements'); const userRoutes = require('./routes/users'); const statisticsRoutes = require('./routes/statistics'); const dictionaryRoutes = require('./routes/dictionaries'); const logRoutes = require('./routes/logs'); const notificationRoutes = require('./routes/notifications'); const aiAnalysisRoutes = require('./routes/aiAnalysis'); // 对认证路由应用更严格的限流 app.use('/api/auth', authLimiter, authRoutes); app.use('/api/achievements', achievementRoutes); app.use('/api/users', userRoutes); app.use('/api/statistics', statisticsRoutes); app.use('/api/dictionaries', dictionaryRoutes); app.use('/api/logs', logRoutes); app.use('/api/notifications', notificationRoutes); app.use('/api/ai-analysis', aiAnalysisRoutes); // 静态文件服务 (用于开发环境查看附件,生产环境建议用 Nginx) app.use('/uploads', express.static(absoluteUploadDir, { setHeaders: (res, path) => { // 强制下载,防止浏览器直接预览 HTML/SVG 等可能包含脚本的文件 res.setHeader('Content-Disposition', 'attachment'); } })); // 托管前端静态文件 (生产环境) const clientBuildPath = path.join(__dirname, '../client/build'); if (fs.existsSync(clientBuildPath)) { app.use(express.static(clientBuildPath)); // 使用正则表达式排除以 /api/ 开头的路径 app.get(/^(?!\/api\/).*$/, (req, res) => { res.sendFile(path.join(clientBuildPath, 'index.html')); }); } // 尝试读取 SSL 证书 (用于 HTTPS) const keyPath = path.join(__dirname, 'server.key'); const certPath = path.join(__dirname, 'server.cert'); if (fs.existsSync(keyPath) && fs.existsSync(certPath)) { // 如果存在证书,启动 HTTPS 服务器 const options = { key: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath) }; https.createServer(options, app).listen(PORT, () => { const msg = `HTTPS 安全服务器运行在 https://localhost:${PORT}`; console.log(msg); logger.info(msg); console.log(`局域网访问请使用: https://<你的IP>:${PORT}`); }); } else { // 否则启动标准 HTTP 服务器 app.listen(PORT, () => { const msg = `HTTP 服务器运行在 http://localhost:${PORT}`; console.log(msg); logger.info(msg); console.log(`提示: 未检测到 server.key 和 server.cert,已降级为 HTTP 模式。`); }); }