/** * 原型站轻量登录网关 * * 功能: * - 所有静态文件访问前必须通过 Cookie 认证 * - 未登录自动跳转到自定义登录页 * - 密码写死在本文件中,按需修改 * * 启动:node server.js * 默认端口:3000(可通过环境变量 PORT 修改) */ const http = require('http'); const fs = require('fs'); const path = require('path'); const url = require('url'); const crypto = require('crypto'); const querystring = require('querystring'); /* ========== 配置区 ========== */ /** 访问密码(按需修改) */ const PASSWORD = 'JKCQ_2026!'; /** Cookie 签名密钥(部署时建议替换为随机字符串) */ const SECRET = 'hc-prototype-secret-key-2026'; /** Cookie 名称 */ const COOKIE_NAME = 'prototype_auth'; /** Cookie 有效期(秒),默认 7 天 */ const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; /** 服务端口 */ const PORT = process.env.PORT || 3010; /** 静态文件根目录(当前目录) */ const STATIC_ROOT = __dirname; /** 不需要登录即可访问的路径 */ const PUBLIC_PATHS = ['/login', '/login.html', '/favicon.ico']; /* ========== MIME 类型映射 ========== */ const MIME_TYPES = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.md': 'text/plain; charset=utf-8', }; /* ========== 工具函数 ========== */ /** 生成签名 Cookie 值 */ function signToken() { var expires = Date.now() + COOKIE_MAX_AGE * 1000; var payload = 'ok|' + expires; var sig = crypto.createHmac('sha256', SECRET).update(payload).digest('hex').slice(0, 16); return payload + '|' + sig; } /** 校验签名 Cookie 值 */ function verifyToken(token) { if (!token) return false; var parts = token.split('|'); if (parts.length !== 3) return false; var status = parts[0]; var expires = parseInt(parts[1], 10); var sig = parts[2]; if (status !== 'ok' || isNaN(expires)) return false; if (Date.now() > expires) return false; var expected = crypto.createHmac('sha256', SECRET).update(status + '|' + expires).digest('hex').slice(0, 16); return sig === expected; } /** 从请求头解析指定 Cookie */ function getCookie(req, name) { var header = req.headers.cookie || ''; var cookies = header.split(';'); for (var i = 0; i < cookies.length; i++) { var pair = cookies[i].trim().split('='); if (pair[0] === name) return decodeURIComponent(pair.slice(1).join('=')); } return null; } /** 读取 POST 请求体 */ function readBody(req, callback) { var chunks = []; req.on('data', function(chunk) { chunks.push(chunk); }); req.on('end', function() { callback(Buffer.concat(chunks).toString()); }); } /** 安全校验回跳地址:只允许站内相对路径 */ function safeRedirect(target) { if (!target || typeof target !== 'string') return '/'; if (target.charAt(0) !== '/') return '/'; if (target.indexOf('//') === 0) return '/'; return target; } /** 发送静态文件 */ function serveFile(res, filePath) { /* 防止路径穿越攻击 */ var resolved = path.resolve(filePath); if (resolved.indexOf(path.resolve(STATIC_ROOT)) !== 0) { res.writeHead(403); res.end('Forbidden'); return; } /* 禁止访问敏感文件 */ var basename = path.basename(resolved); if (basename === 'server.js' || basename === '.env' || basename === '.gitignore') { res.writeHead(403); res.end('Forbidden'); return; } fs.stat(resolved, function(err, stats) { if (err || !stats.isFile()) { /* 目录请求尝试补 index-v2.html */ if (!err && stats.isDirectory()) { var indexFile = path.join(resolved, 'index-v2.html'); serveFile(res, indexFile); return; } res.writeHead(404); res.end('Not Found'); return; } var ext = path.extname(resolved).toLowerCase(); var contentType = MIME_TYPES[ext] || 'application/octet-stream'; res.writeHead(200, { 'Content-Type': contentType, 'Content-Length': stats.size }); fs.createReadStream(resolved).pipe(res); }); } /* ========== 请求处理 ========== */ var server = http.createServer(function(req, res) { var parsed = url.parse(req.url, true); var pathname = decodeURIComponent(parsed.pathname); /* --- POST /simple-login:校验密码 --- */ if (req.method === 'POST' && pathname === '/simple-login') { readBody(req, function(body) { var data = querystring.parse(body); var password = data.password || ''; var redirect = safeRedirect(data.redirect); if (password !== PASSWORD) { res.writeHead(302, { 'Location': '/login.html?error=1&redirect=' + encodeURIComponent(redirect) }); res.end(); return; } var token = signToken(); var cookie = COOKIE_NAME + '=' + encodeURIComponent(token) + '; Path=/; HttpOnly; SameSite=Lax; Max-Age=' + COOKIE_MAX_AGE; res.writeHead(302, { 'Set-Cookie': cookie, 'Location': redirect }); res.end(); }); return; } /* --- POST /simple-logout:退出登录 --- */ if (req.method === 'POST' && pathname === '/simple-logout') { var clearCookie = COOKIE_NAME + '=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0'; res.writeHead(302, { 'Set-Cookie': clearCookie, 'Location': '/login.html' }); res.end(); return; } /* --- GET /simple-logout:也支持 GET 方式退出 --- */ if (req.method === 'GET' && pathname === '/simple-logout') { var clearCookie2 = COOKIE_NAME + '=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0'; res.writeHead(302, { 'Set-Cookie': clearCookie2, 'Location': '/login.html' }); res.end(); return; } /* --- 公开路径放行 --- */ var isPublic = PUBLIC_PATHS.some(function(p) { return pathname === p; }); if (isPublic) { var loginFile = pathname === '/login' ? '/login.html' : pathname; var fullPath = path.join(STATIC_ROOT, loginFile); serveFile(res, fullPath); return; } /* --- 鉴权:检查 Cookie --- */ var token = getCookie(req, COOKIE_NAME); if (!verifyToken(token)) { res.writeHead(302, { 'Location': '/login.html?redirect=' + encodeURIComponent(pathname) }); res.end(); return; } /* --- 已登录:返回静态文件 --- */ if (pathname === '/') pathname = '/index-v2.html'; var filePath = path.join(STATIC_ROOT, pathname); serveFile(res, filePath); }); server.listen(PORT, function() { console.log('[原型网关] 已启动 -> http://localhost:' + PORT); console.log('[原型网关] 访问密码: ' + PASSWORD); console.log('[原型网关] 按 Ctrl+C 停止'); });