feat(data_gateway/auth): 重构鉴权模块,新增权限控制中间件

- 新增IP标准化、请求IP获取、本地请求判断等工具函数
- 新增requireLocalOnly和requireInternalOnly权限中间件
- 将路由内的鉴权逻辑抽离为中间件,简化路由代码
- 更新模块与接口的注释描述
- 修复管理接口未授权访问的安全问题
- 优化内部查询接口权限,支持本地直连或内部token验证
This commit is contained in:
yangxiangyuan
2026-08-02 07:24:54 +08:00
parent a6f763ffd8
commit 47da1967d3
2 changed files with 62 additions and 12 deletions
+56 -3
View File
@@ -1,6 +1,9 @@
// ============================================================
// data_gateway/auth.js - 对外 API 鉴权模块
// 职责:校验 X-API-Id + X-API-Key,匹配 skill config.json 中的哈希
// data_gateway/auth.js - 数据网关鉴权与内网守卫模块
// 职责:
// - 校验 X-API-Id + X-API-Key
// - 校验内部读取 token
// - 统一提供 localOnly / internalOnly 语义
// ============================================================
const crypto = require('crypto')
const fs = require('fs')
@@ -45,6 +48,34 @@ const listEnabledSkills = () => {
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()
@@ -82,4 +113,26 @@ const verifyInternalToken = (req) => {
return { ok: true, skill: cfg }
}
module.exports = { loadSkillConfig, refreshCache, listEnabledSkills, verifyApiKey, verifyInternalToken }
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
}
+6 -9
View File
@@ -2,10 +2,10 @@
// data_gateway/index.js - 对外 API 数据网关主入口
// 暴露两个路由:
// POST /api/v1/ingest/:skillId 外部数据写入(公网,需 API Key)
// GET /api/v1/data/:skillId/query 内部数据读取(仅 localhost,需 Internal Token)
// GET /api/v1/data/:skillId/query 内部数据读取(localhost 或 Internal Token)
// ============================================================
const { logJSON } = require('../logger')
const { verifyApiKey, verifyInternalToken, listEnabledSkills } = require('./auth')
const { verifyApiKey, listEnabledSkills, requireLocalOnly, requireInternalOnly } = require('./auth')
const { insert, upsertLatest, queryLatest, queryList, queryByTimeRange, count } = require('./store')
const { loadSkillConfig } = require('./auth')
const { bindRoutes: bindExternalStorageRoutes } = require('./skills/external_storage')
@@ -165,18 +165,15 @@ const bindRoutes = (app) => {
})
// ============================================================
// 内部读取接口(仅 localhost)
// 内部读取接口(本机直连,或带 Internal Token 的受保护读取)
// GET /api/v1/data/:skillId/query?mode=latest|list&limit=100&offset=0
// Headers: X-Internal-Token, X-Skill-Id
// ============================================================
app.get('/api/v1/data/:skillId/query', (req, res) => {
app.get('/api/v1/data/:skillId/query', requireInternalOnly, (req, res) => {
setNoCache(res)
const skillId = String(req.params.skillId || '').trim()
if (!skillId) return res.status(400).json({ ok: false, error: 'missing skill id' })
const auth = verifyInternalToken(req)
if (!auth.ok) return res.status(401).json({ ok: false, error: auth.error })
const mode = String(req.query.mode || 'latest').trim()
const limit = parseInt(req.query.limit, 10) || 100
const offset = parseInt(req.query.offset, 10) || 0
@@ -208,7 +205,7 @@ const bindRoutes = (app) => {
// 管理接口:列出所有已启用的 skill(仅 localhost)
// GET /api/v1/data/skills
// ============================================================
app.get('/api/v1/data/skills', (req, res) => {
app.get('/api/v1/data/skills', requireLocalOnly, (req, res) => {
setNoCache(res)
const skills = listEnabledSkills()
const result = skills.map(s => {
@@ -227,4 +224,4 @@ const bindRoutes = (app) => {
app.use(errorHandler)
}
module.exports = { bindRoutes, listEnabledSkills, loadSkillConfig }
module.exports = { bindRoutes, listEnabledSkills, loadSkillConfig }