47da1967d3
- 新增IP标准化、请求IP获取、本地请求判断等工具函数 - 新增requireLocalOnly和requireInternalOnly权限中间件 - 将路由内的鉴权逻辑抽离为中间件,简化路由代码 - 更新模块与接口的注释描述 - 修复管理接口未授权访问的安全问题 - 优化内部查询接口权限,支持本地直连或内部token验证
139 lines
4.2 KiB
JavaScript
139 lines
4.2 KiB
JavaScript
// ============================================================
|
|
// data_gateway/auth.js - 数据网关鉴权与内网守卫模块
|
|
// 职责:
|
|
// - 校验 X-API-Id + X-API-Key
|
|
// - 校验内部读取 token
|
|
// - 统一提供 localOnly / internalOnly 语义
|
|
// ============================================================
|
|
const crypto = require('crypto')
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
const SKILLS_DIR = path.join(__dirname, 'skills')
|
|
|
|
// 缓存已加载的 skill 配置
|
|
const skillConfigCache = new Map()
|
|
|
|
const loadSkillConfig = (skillId) => {
|
|
if (skillConfigCache.has(skillId)) return skillConfigCache.get(skillId)
|
|
const cfgPath = path.join(SKILLS_DIR, skillId, 'config.json')
|
|
if (!fs.existsSync(cfgPath)) return null
|
|
try {
|
|
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'))
|
|
skillConfigCache.set(skillId, cfg)
|
|
return cfg
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
// 刷新缓存(用于配置热更新)
|
|
const refreshCache = (skillId) => {
|
|
skillConfigCache.delete(skillId)
|
|
return loadSkillConfig(skillId)
|
|
}
|
|
|
|
// 枚举所有已启用的 skill
|
|
const listEnabledSkills = () => {
|
|
const result = []
|
|
if (!fs.existsSync(SKILLS_DIR)) return result
|
|
const dirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
for (const d of dirs) {
|
|
if (!d.isDirectory()) continue
|
|
const cfg = loadSkillConfig(d.name)
|
|
if (cfg && cfg.enabled !== false) {
|
|
result.push({ id: cfg.id, name: cfg.name })
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
const normalizeIp = (value) => {
|
|
const raw = String(value || '').trim()
|
|
if (!raw) return ''
|
|
if (raw.startsWith('::ffff:')) return raw.slice(7)
|
|
return raw
|
|
}
|
|
|
|
const getRequestIp = (req) => {
|
|
const forwarded = String(req.headers['x-forwarded-for'] || '')
|
|
.split(',')
|
|
.map(item => normalizeIp(item))
|
|
.find(Boolean)
|
|
if (forwarded) return forwarded
|
|
return normalizeIp(
|
|
req.ip ||
|
|
req.socket?.remoteAddress ||
|
|
req.connection?.remoteAddress ||
|
|
req.connection?.socket?.remoteAddress
|
|
)
|
|
}
|
|
|
|
const isLoopbackIp = (ip) => {
|
|
const normalized = normalizeIp(ip)
|
|
return normalized === '127.0.0.1' || normalized === '::1'
|
|
}
|
|
|
|
const isLocalRequest = (req) => isLoopbackIp(getRequestIp(req))
|
|
|
|
// 校验外部 API 请求
|
|
const verifyApiKey = (req) => {
|
|
const apiId = String(req.headers['x-api-id'] || '').trim()
|
|
const apiKey = String(req.headers['x-api-key'] || '').trim()
|
|
if (!apiId || !apiKey) return { ok: false, error: 'missing api id or key' }
|
|
|
|
const cfg = loadSkillConfig(apiId)
|
|
if (!cfg) return { ok: false, error: 'skill not found' }
|
|
if (cfg.enabled === false) return { ok: false, error: 'skill disabled' }
|
|
|
|
const expectedHash = String(cfg.api_key_hash || '')
|
|
if (!expectedHash) return { ok: false, error: 'skill not configured' }
|
|
|
|
const actualHash = 'sha256:' + crypto.createHash('sha256').update(apiKey).digest('hex')
|
|
if (actualHash !== expectedHash) return { ok: false, error: 'invalid api key' }
|
|
|
|
return { ok: true, skill: cfg }
|
|
}
|
|
|
|
// 校验内部 Tool 读取请求
|
|
const verifyInternalToken = (req) => {
|
|
const token = String(req.headers['x-internal-token'] || '').trim()
|
|
const skillId = String(req.headers['x-skill-id'] || req.params?.skillId || '').trim()
|
|
if (!token || !skillId) return { ok: false, error: 'missing token or skill id' }
|
|
|
|
const cfg = loadSkillConfig(skillId)
|
|
if (!cfg) return { ok: false, error: 'skill not found' }
|
|
|
|
const expectedHash = String(cfg.read_token_hash || '')
|
|
if (!expectedHash) return { ok: false, error: 'skill not configured for internal read' }
|
|
|
|
const actualHash = 'sha256:' + crypto.createHash('sha256').update(token).digest('hex')
|
|
if (actualHash !== expectedHash) return { ok: false, error: 'invalid internal token' }
|
|
|
|
return { ok: true, skill: cfg }
|
|
}
|
|
|
|
const requireLocalOnly = (req, res, next) => {
|
|
if (isLocalRequest(req)) return next()
|
|
return res.status(403).json({ ok: false, error: 'local_only' })
|
|
}
|
|
|
|
const requireInternalOnly = (req, res, next) => {
|
|
if (isLocalRequest(req)) return next()
|
|
const auth = verifyInternalToken(req)
|
|
if (auth.ok) return next()
|
|
return res.status(401).json({ ok: false, error: auth.error })
|
|
}
|
|
|
|
module.exports = {
|
|
loadSkillConfig,
|
|
refreshCache,
|
|
listEnabledSkills,
|
|
verifyApiKey,
|
|
verifyInternalToken,
|
|
getRequestIp,
|
|
isLocalRequest,
|
|
requireLocalOnly,
|
|
requireInternalOnly
|
|
}
|