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:
+10
-1
@@ -30,6 +30,15 @@ const deriveKey = () => {
|
||||
}
|
||||
return _key
|
||||
}
|
||||
|
||||
const getLocalCredsToken = (req) => {
|
||||
const headerToken = String((req.headers && req.headers['x-local-creds-token']) || '').trim()
|
||||
if (headerToken) return headerToken
|
||||
if (req.method !== 'GET' && req.body && typeof req.body === 'object') {
|
||||
return String(req.body.token || '').trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
const enc = (plain) => {
|
||||
const key = deriveKey()
|
||||
const iv = crypto.randomBytes(12)
|
||||
@@ -431,7 +440,7 @@ const bindRoutes = (app) => {
|
||||
})
|
||||
const serverToken = String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '')
|
||||
if (serverToken) {
|
||||
const clientToken = String(req.query.token || '')
|
||||
const clientToken = getLocalCredsToken(req)
|
||||
if (clientToken !== serverToken) return res.json({ ok: true, creds: null })
|
||||
}
|
||||
const u = creds.AILIB_USERNAME || ''
|
||||
|
||||
Binary file not shown.
+76
-13
@@ -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' })
|
||||
|
||||
@@ -2,6 +2,7 @@ const fs = require('fs')
|
||||
const path = require('path')
|
||||
const iconv = require('iconv-lite')
|
||||
const { logJSON } = require('./logger')
|
||||
const INTERNAL_ERROR = 'internal_error'
|
||||
|
||||
const readTextWithFallback = filePath => {
|
||||
const buf = fs.readFileSync(filePath)
|
||||
@@ -249,7 +250,11 @@ const summarizeEvents = events => {
|
||||
}
|
||||
}
|
||||
|
||||
const bindRoutes = app => {
|
||||
const sendError = (res, status, error) => {
|
||||
res.status(status).json({ ok: false, error })
|
||||
}
|
||||
|
||||
const bindRoutes = (app, options = {}) => {
|
||||
app.use('/api/investment_ledger', (req, res, next) => {
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate')
|
||||
res.set('Pragma', 'no-cache')
|
||||
@@ -257,6 +262,13 @@ const bindRoutes = app => {
|
||||
next()
|
||||
})
|
||||
|
||||
app.use('/api/investment_ledger', (req, res, next) => {
|
||||
if (typeof options.requireAuth === 'function') {
|
||||
return options.requireAuth(req, res, next)
|
||||
}
|
||||
return next()
|
||||
})
|
||||
|
||||
app.get('/api/investment_ledger/ping', (req, res) => {
|
||||
res.json({ ok: true, ts: Date.now() })
|
||||
})
|
||||
@@ -265,24 +277,20 @@ const bindRoutes = app => {
|
||||
try {
|
||||
const symbol = String((req.body && req.body.symbol) || '').trim()
|
||||
const rawText = String((req.body && req.body.raw_text) || '')
|
||||
const filePath = String((req.body && req.body.file_path) || '').trim()
|
||||
if (!rawText) return sendError(res, 400, 'empty_input')
|
||||
if (String((req.body && req.body.file_path) || '').trim()) {
|
||||
return sendError(res, 400, 'file_path_not_allowed')
|
||||
}
|
||||
let text = rawText
|
||||
let encoding = 'utf8'
|
||||
if (!text && filePath) {
|
||||
const abs = path.resolve(filePath)
|
||||
if (!fs.existsSync(abs)) return res.status(400).json({ ok: false, error: 'file_not_found' })
|
||||
const decoded = readTextWithFallback(abs)
|
||||
text = decoded.text
|
||||
encoding = decoded.encoding
|
||||
}
|
||||
if (!text) return res.status(400).json({ ok: false, error: 'empty_input' })
|
||||
const rows = parseCsvText(text)
|
||||
const events = normalizeRowsToEvents(rows, symbol)
|
||||
const summary = summarizeEvents(events)
|
||||
logJSON('investment_ledger.parse', { symbol, encoding, events: events.length }, 'investment_ledger')
|
||||
res.json({ ok: true, encoding, summary, rows: events })
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: String(e.message || e) })
|
||||
logJSON('investment_ledger.parse.error', { error: String(e.message || e) }, 'investment_ledger')
|
||||
sendError(res, 500, INTERNAL_ERROR)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -118,6 +118,15 @@ const requireAuth = (req, res, next) => {
|
||||
next()
|
||||
}
|
||||
|
||||
const getLocalCredsToken = (req) => {
|
||||
const headerToken = String((req.headers && req.headers['x-local-creds-token']) || '').trim()
|
||||
if (headerToken) return headerToken
|
||||
if (req.method !== 'GET' && req.body && typeof req.body === 'object') {
|
||||
return String(req.body.token || '').trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const bindRoutes = app => {
|
||||
ensureDefaultUser()
|
||||
|
||||
@@ -151,7 +160,7 @@ const bindRoutes = app => {
|
||||
})
|
||||
const serverToken = String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '')
|
||||
if (serverToken) {
|
||||
const clientToken = String(req.query.token || '')
|
||||
const clientToken = getLocalCredsToken(req)
|
||||
if (clientToken !== serverToken) return res.json({ ok: true, creds: null })
|
||||
}
|
||||
const u = creds.LBL_USERNAME || ''
|
||||
|
||||
@@ -139,6 +139,14 @@ const getExpectedToken = () => {
|
||||
const creds = readLocalCreds()
|
||||
return String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '').trim()
|
||||
}
|
||||
const getLocalCredsToken = (req) => {
|
||||
const headerToken = String((req.headers && req.headers['x-local-creds-token']) || '').trim()
|
||||
if (headerToken) return headerToken
|
||||
if (req.method !== 'GET' && req.body && typeof req.body === 'object') {
|
||||
return String(req.body.token || '').trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const signAuthPayload = (username) => {
|
||||
const payload = { sub: username, iat: Date.now() }
|
||||
@@ -495,7 +503,7 @@ const createRouter = () => {
|
||||
|
||||
router.get('/auth/local_creds', (req, res) => {
|
||||
try {
|
||||
const token = String((req.query && req.query.token) || '').trim()
|
||||
const token = getLocalCredsToken(req)
|
||||
const expectedToken = getExpectedToken()
|
||||
if (!expectedToken || token !== expectedToken) return res.status(401).json({ ok: false, error: '令牌无效' })
|
||||
const username = getExpectedUsername()
|
||||
@@ -1093,4 +1101,4 @@ const bindRoutes = (app) => {
|
||||
app.use('/api/oss_file_cabinet', router)
|
||||
}
|
||||
|
||||
module.exports = { bindRoutes, getDb, humanSize, getExt, isImage }
|
||||
module.exports = { bindRoutes, getDb, humanSize, getExt, isImage }
|
||||
|
||||
@@ -320,6 +320,14 @@ const getExpectedToken = () => {
|
||||
const creds = readLocalCreds()
|
||||
return String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '').trim()
|
||||
}
|
||||
const getLocalCredsToken = (req) => {
|
||||
const headerToken = String((req.headers && req.headers['x-local-creds-token']) || '').trim()
|
||||
if (headerToken) return headerToken
|
||||
if (req.method !== 'GET' && req.body && typeof req.body === 'object') {
|
||||
return String(req.body.token || '').trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const createRouter = () => {
|
||||
const router = express.Router()
|
||||
@@ -374,7 +382,7 @@ const createRouter = () => {
|
||||
|
||||
router.get('/auth/local_creds', (req, res) => {
|
||||
try {
|
||||
const token = String((req.query && req.query.token) || '').trim()
|
||||
const token = getLocalCredsToken(req)
|
||||
const expectedToken = getExpectedToken()
|
||||
if (!expectedToken || token !== expectedToken) {
|
||||
return res.status(401).json({ ok: false, error: 'invalid_token' })
|
||||
|
||||
@@ -118,6 +118,15 @@ const requireAuth = (req, res, next) => {
|
||||
next()
|
||||
}
|
||||
|
||||
const getLocalCredsToken = (req) => {
|
||||
const headerToken = String((req.headers && req.headers['x-local-creds-token']) || '').trim()
|
||||
if (headerToken) return headerToken
|
||||
if (req.method !== 'GET' && req.body && typeof req.body === 'object') {
|
||||
return String(req.body.token || '').trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const bindRoutes = app => {
|
||||
ensureDefaultUser()
|
||||
|
||||
@@ -151,7 +160,7 @@ const bindRoutes = app => {
|
||||
})
|
||||
const serverToken = String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '')
|
||||
if (serverToken) {
|
||||
const clientToken = String(req.query.token || '')
|
||||
const clientToken = getLocalCredsToken(req)
|
||||
if (clientToken !== serverToken) return res.json({ ok: true, creds: null })
|
||||
}
|
||||
const u = creds.RL_USERNAME || ''
|
||||
|
||||
+10
-1
@@ -29,6 +29,15 @@ const sealPath = path.join(dataDir, 'weekly.enc')
|
||||
initDB(dbPath, sealPath)
|
||||
core.ensureDefaultUser()
|
||||
|
||||
const getLocalCredsToken = (req) => {
|
||||
const headerToken = String((req.headers && req.headers['x-local-creds-token']) || '').trim()
|
||||
if (headerToken) return headerToken
|
||||
if (req.method !== 'GET' && req.body && typeof req.body === 'object') {
|
||||
return String(req.body.token || '').trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 3. Helper Middleware
|
||||
const upload = multer({ limits: { fileSize: 50 * 1024 * 1024 } })
|
||||
|
||||
@@ -145,7 +154,7 @@ const bindRoutes = (app) => {
|
||||
})
|
||||
const serverToken = String(creds.TOOLBOX_LOCAL_CREDS_TOKEN || '')
|
||||
if (serverToken) {
|
||||
const clientToken = String(req.query.token || '')
|
||||
const clientToken = getLocalCredsToken(req)
|
||||
if (clientToken !== serverToken) return res.json({ ok: true, creds: null })
|
||||
}
|
||||
const username = creds.WEEKLY_USERNAME || ''
|
||||
|
||||
Reference in New Issue
Block a user