feat: 重构认证流程,升级工具功能并优化后端配置

- 修复token泄露风险:改用X-Local-Creds-Token请求头传递token,从URL Hash读取set_token避免日志记录
- 重构全站认证逻辑:提取公用工具函数统一各服务的认证校验流程
- 升级投资账本工具:支持本地上传CSV和粘贴文本,移除硬编码路径,适配新参数格式
- 新增403/404/500标准错误响应文件,统一API错误返回格式
- 优化后端配置:关闭ETag,重构CORS中间件,新增全局错误捕获,限制敏感文件访问
- 优化web zen box的Service Worker,支持音频分片请求与断点续传
- 清理冗余前端代码,调整页面格式与权限控制
This commit is contained in:
yangxiangyuan
2026-08-01 21:50:05 +08:00
parent 2143adb3bb
commit a6f763ffd8
26 changed files with 304 additions and 72 deletions
+76 -13
View File
@@ -96,6 +96,7 @@ app.use((req, res, next) => {
// 移除 X-Powered-By 响应头(避免泄露 Express 版本信息)
app.disable('x-powered-by')
app.set('etag', false)
// CORS 白名单限制 — 仅允许指定域名跨域访问 API
const allowedOrigins = [
@@ -106,18 +107,33 @@ const allowedOrigins = [
'http://localhost:8081',
'http://127.0.0.1:8081'
]
app.use(cors({
origin: function (origin, callback) {
// 允许无 origin 的请求(如 curl、服务器端调用)
if (!origin) return callback(null, true)
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true)
} else {
callback(new Error('Not allowed by CORS'))
const isAllowedCorsOrigin = (origin) => !origin || allowedOrigins.includes(origin)
app.use((req, res, next) => {
const origin = String(req.headers.origin || '').trim()
const isAllowed = isAllowedCorsOrigin(origin)
if (origin && isAllowed) {
res.set('Access-Control-Allow-Origin', origin)
res.vary('Origin')
res.set('Access-Control-Allow-Credentials', 'true')
}
if (req.method === 'OPTIONS') {
if (!origin || isAllowed) {
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-Local-Creds-Token')
res.set('Access-Control-Allow-Credentials', 'true')
return res.status(204).end()
}
},
credentials: true
}))
return res.status(403).json({ ok: false, error: 'cors_origin_denied' })
}
if (origin && !isAllowed) {
return res.status(403).json({ ok: false, error: 'cors_origin_denied' })
}
return next()
})
// 强制 HTTPS:HTTP 请求返回 301 跳转到 HTTPS
app.use((req, res, next) => {
@@ -165,10 +181,23 @@ const readLocalCreds = () => {
} catch { return {} }
}
const getLocalCredsClientToken = (req) => {
try {
const headerToken = String(req.headers['x-local-creds-token'] || req.headers['x-toolbox-creds-token'] || '').trim()
if (headerToken) return headerToken
const authHeader = String(req.headers.authorization || '').trim()
if (authHeader.startsWith('Bearer ')) return authHeader.slice(7).trim()
if (req.method !== 'GET' && req.body && typeof req.body === 'object') {
return String(req.body.token || '').trim()
}
} catch {}
return ''
}
const checkLocalCredsToken = (req, localCreds) => {
const serverToken = String(localCreds.TOOLBOX_LOCAL_CREDS_TOKEN || '')
if (!serverToken) return true
const clientToken = String(req.query.token || '')
const clientToken = getLocalCredsClientToken(req)
return clientToken === serverToken
}
@@ -3350,6 +3379,10 @@ app.get('/api/doc_cloud_keeper/auth/local_creds', (req, res) => {
res.json({ ok: true, creds: { username: u, password: p } })
} catch { sendNoCreds(res) }
})
app.use('/tools/doc_cloud_keeper/DocHelper', (req, res, next) => {
if (hasDocCloudKeeperAuth(req)) return next()
return res.status(404).send('Not Found')
})
app.get('/tools/doc_cloud_keeper', (req, res, next) => {
try {
const token = String(req.query.token || '').trim()
@@ -6162,6 +6195,16 @@ app.use('/tools/web_order_box', (req, res, next) => {
next()
})
app.use('/tools/web_order_box', express.static(path.join(process.cwd(), 'public', 'tools', 'web_order_box')))
app.use('/tools/investment_ledger', (req, res, next) => {
try {
if (hasNavAuthRoot(req)) return next()
return res.status(401).send('未授权')
} catch { return res.status(401).send('未授权') }
})
app.get(['/package.json', '/package-lock.json', '/pnpm-lock.yaml', '/yarn.lock', '/robots.txt'], (_req, res) => {
res.status(404).send('Not Found')
})
app.get('/api/zen_box/playlists', (req, res) => {
try {
@@ -6623,7 +6666,12 @@ ensureWeeklyEmbedLoaded()
ensurePscLoaded()
aiLib.bindRoutes(app)
expense.bindRoutes(app)
investmentLedger.bindRoutes(app)
investmentLedger.bindRoutes(app, {
requireAuth: (req, res, next) => {
if (hasNavAuthRoot(req)) return next()
return res.status(401).json({ ok: false, error: 'unauthorized' })
}
})
styleCheck.bindRoutes(app)
globalNews.bindRoutes(app)
dataGateway.bindRoutes(app)
@@ -6731,6 +6779,21 @@ app.get('/api/yuanzhupai/sessions/:id', (req, res) => {
}
})
app.use((err, req, res, next) => {
try {
logJSON('http.unhandled.error', {
path: req && req.path,
method: req && req.method,
error: String(err && err.message ? err.message : err)
}, 'server')
} catch {}
if (res.headersSent) return next(err)
if (req && req.path && req.path.startsWith('/api/')) {
return res.status(500).json({ ok: false, error: 'internal_error' })
}
return res.status(500).send('服务器开小差了')
})
// 404 处理:覆盖 Express 默认 "Cannot GET" 错误页,避免泄露技术栈特征
app.use((req, res) => {
res.status(404).json({ ok: false, error: 'not_found' })