输入访问 token
管理台需要有效 token 才能继续访问。
import express from 'express'; import crypto from 'crypto'; import path from 'path'; import { fileURLToPath } from 'url'; import logger from './logger.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); class APIServer { constructor(config, modem, smsProcessor) { this.config = config; this.modem = modem; this.smsProcessor = smsProcessor; this.app = express(); this.publicDir = path.join(__dirname, '../public'); this.setupMiddleware(); this.setupRoutes(); } setupMiddleware() { // JSON解析 this.app.use(express.json()); // Web Token认证,API保留Basic Auth兼容 this.app.use((req, res, next) => { this.authenticateRequest(req, res, next); }); // 请求日志 this.app.use((req, res, next) => { logger.info(`${req.method} ${req.path}`); next(); }); this.app.use('/assets', express.static(path.join(this.publicDir, 'assets'))); } authenticateRequest(req, res, next) { if (this.hasValidWebToken(req)) { this.persistWebToken(req, res); if (this.shouldCleanTokenFromUrl(req)) { return res.redirect(302, this.getCleanUrl(req)); } return next(); } if (this.isWebRoute(req) && this.getConfiguredWebToken()) { return this.sendTokenGate(res); } if (this.hasValidBasicAuth(req)) { return next(); } if (req.path.startsWith('/api/')) { return res.status(401).json({ success: false, error: this.getConfiguredWebToken() ? '需要有效token或Basic Auth' : '需要Basic Auth' }); } return this.sendBasicAuthChallenge(res); } getConfiguredWebToken() { return String(this.config.api.webToken || '').trim(); } hasValidWebToken(req) { const expected = this.getConfiguredWebToken(); if (!expected) { return false; } const token = this.getRequestToken(req); return this.safeEqual(token, expected); } getRequestToken(req) { const bearer = req.get('authorization')?.match(/^Bearer\s+(.+)$/i)?.[1]; return req.query.token || req.get('x-web-token') || bearer || this.getCookie(req, 'sms_gateway_token') || ''; } getCookie(req, name) { const cookieHeader = req.get('cookie') || ''; const cookies = cookieHeader.split(';').map(item => item.trim()); const prefix = `${name}=`; const cookie = cookies.find(item => item.startsWith(prefix)); return cookie ? decodeURIComponent(cookie.slice(prefix.length)) : ''; } persistWebToken(req, res) { if (!req.query.token) { return; } res.cookie('sms_gateway_token', req.query.token, { httpOnly: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000, path: '/' }); } shouldCleanTokenFromUrl(req) { return this.isWebRoute(req) && Boolean(req.query.token); } getCleanUrl(req) { const url = new URL(req.originalUrl, 'http://localhost'); url.searchParams.delete('token'); return `${url.pathname}${url.search}`; } hasValidBasicAuth(req) { const auth = req.get('authorization') || ''; if (!auth.startsWith('Basic ')) { return false; } const decoded = Buffer.from(auth.slice(6), 'base64').toString('utf8'); const separatorIndex = decoded.indexOf(':'); if (separatorIndex === -1) { return false; } const username = decoded.slice(0, separatorIndex); const password = decoded.slice(separatorIndex + 1); return this.safeEqual(username, this.config.api.auth.username) && this.safeEqual(password, this.config.api.auth.password); } safeEqual(actual, expected) { const actualBuffer = Buffer.from(String(actual)); const expectedBuffer = Buffer.from(String(expected)); if (actualBuffer.length !== expectedBuffer.length) { return false; } return crypto.timingSafeEqual(actualBuffer, expectedBuffer); } isWebRoute(req) { return req.path === '/' || req.path === '/admin' || req.path.startsWith('/assets/'); } sendBasicAuthChallenge(res) { res.set('WWW-Authenticate', 'Basic realm="SMS Gateway"'); return res.status(401).send('Authentication required'); } sendTokenGate(res) { res.set('Cache-Control', 'no-store'); return res.status(401).type('html').send(`
管理台需要有效 token 才能继续访问。