docs: 记录 API Key 修复与中间件安全加固(v2026.05.14)
- [修复] IP 查询失败:assets-ai api_keys 表注册缺失的 key - [优化] 中间件 API Key 验证统一为 ALLOWED_API_KEYS → DB 两级 - [新增] deploy-ai.sh 部署后自动验证 issue→assets 连通性
This commit is contained in:
parent
2697aaaa75
commit
dce8b0b6cc
|
|
@ -13,6 +13,9 @@
|
||||||
- [修复] `reports:write` 全面替换为 `reports:create`
|
- [修复] `reports:write` 全面替换为 `reports:create`
|
||||||
- [修复] 种子数据迁移逻辑不再每次启动覆盖用户自定义权限,仅首次安装时创建默认权限
|
- [修复] 种子数据迁移逻辑不再每次启动覆盖用户自定义权限,仅首次安装时创建默认权限
|
||||||
- [修复] SSO 新用户免登录失败:根页面 `getCurrentUser()` 改为直接 `redirect('/dashboard')`,由中间件统一处理共享 JWT 认证(与 assets-ai 行为一致)
|
- [修复] SSO 新用户免登录失败:根页面 `getCurrentUser()` 改为直接 `redirect('/dashboard')`,由中间件统一处理共享 JWT 认证(与 assets-ai 行为一致)
|
||||||
|
- [修复] 手动建单输入业务 IP 按回车无法查询节点名称和设备序列号:assets-ai `api_keys` 表中缺少 issue-ai 使用的 API Key,服务端调用返回 401,且前端静默吞错无提示
|
||||||
|
- [优化] 中间件 API Key 验证统一为 ALLOWED_API_KEYS(快速路径)→ api_keys 数据库表(回退)两级验证;无效 key 不再被放行,中间件层直接返回 401(安全加固,与 assets-ai 行为一致)
|
||||||
|
- [新增] 部署后自动验证 issue→assets API 连通性(`deploy-ai.sh`),API Key 不匹配时部署立即报错退出
|
||||||
- [修复] `next.config.ts` 添加 `ldapts` 到 `serverExternalPackages`,确保 Next.js standalone 构建包含 LLDAP 客户端模块,避免 `ldapUserExists()` 因模块缺失失败导致 SSO 自动创建用户静默中断
|
- [修复] `next.config.ts` 添加 `ldapts` 到 `serverExternalPackages`,确保 Next.js standalone 构建包含 LLDAP 客户端模块,避免 `ldapUserExists()` 因模块缺失失败导致 SSO 自动创建用户静默中断
|
||||||
- [调整] 全局证书切换:Cloudflare Origin CA → Let's Encrypt(`/etc/letsencrypt/live/www.tlyq.ai/`),覆盖全部 7 个子域名,nginx 8 个站点配置同步更新
|
- [调整] 全局证书切换:Cloudflare Origin CA → Let's Encrypt(`/etc/letsencrypt/live/www.tlyq.ai/`),覆盖全部 7 个子域名,nginx 8 个站点配置同步更新
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ npm run import # 导入工单
|
||||||
|
|
||||||
- **Web UI(v2.1)**:`middleware.ts` 优先检查 `tlyq_session`(共享 JWT,OA 统一签发)→ 回退 `session_issue`(本地 JWT)。`getCurrentUser()` 每次请求时检查 LLDAP 用户是否存在,已删除则清除 cookie 踢出
|
- **Web UI(v2.1)**:`middleware.ts` 优先检查 `tlyq_session`(共享 JWT,OA 统一签发)→ 回退 `session_issue`(本地 JWT)。`getCurrentUser()` 每次请求时检查 LLDAP 用户是否存在,已删除则清除 cookie 踢出
|
||||||
- **localadmin**:纯本地 BCrypt 认证,不依赖 LLDAP,用于 LLDAP 故障时应急登录(DB 预置,admin 角色)
|
- **localadmin**:纯本地 BCrypt 认证,不依赖 LLDAP,用于 LLDAP 故障时应急登录(DB 预置,admin 角色)
|
||||||
- **API Key**:`Bearer ak_<32位十六进制>`,存储时 SHA-256 hash,由 `ALLOWED_API_KEYS` 控制
|
- **API Key(v2.2)**:`middleware.ts` 采用两级验证:① `ALLOWED_API_KEYS` 环境变量快速匹配(逗号分隔明文 key);② 未命中则查 `api_keys` 数据库表(SHA-256 hash)。无效 key 在中间件层直接返回 401,不再放行到 route handler。外部系统(如 assets-ai)调用本系统 API 时,key 可注册在 `ALLOWED_API_KEYS` 或 `api_keys` 表中任意一处即可,推荐走 Web UI 创建(写入 `api_keys` 表,支持权限控制和 `last_used_at` 追踪)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -189,10 +189,14 @@ assets-ai ──→ GET {ISSUE_API_URL}/tickets/by-asset?ip=xxx (Authori
|
||||||
|
|
||||||
Key 格式:`ak_<32位十六进制>`,认证头:`Authorization: Bearer <key>`。每个环境独立创建,互不通用。
|
Key 格式:`ak_<32位十六进制>`,认证头:`Authorization: Bearer <key>`。每个环境独立创建,互不通用。
|
||||||
|
|
||||||
**issue → assets 方向**:在 assets-ai `/settings/api-keys` 创建 Key → 写入 issue-ai 的 `ASSETS_API_KEY`
|
**验证策略(v2.2)**:两边中间件均采用 `ALLOWED_API_KEYS` 环境变量 → `api_keys` 数据库表两级验证。Key 注册在任意一处即可通过认证,推荐走 Web UI 创建(写入 `api_keys` 表,支持权限和追踪)。
|
||||||
|
|
||||||
|
**issue → assets 方向**:在 assets-ai `/settings/api-keys` 创建 Key → 写入 issue-ai 的 `ASSETS_API_KEY`(`.env` 或 `docker-compose.yml` 环境变量)
|
||||||
|
|
||||||
**assets → issue 方向**:在 issue-ai `/settings/api-keys` 创建 Key → 写入 assets-ai 的 `ISSUE_API_KEY` + issue-ai 的 `ALLOWED_API_KEYS`
|
**assets → issue 方向**:在 issue-ai `/settings/api-keys` 创建 Key → 写入 assets-ai 的 `ISSUE_API_KEY` + issue-ai 的 `ALLOWED_API_KEYS`
|
||||||
|
|
||||||
|
> **部署验证**:`deploy-ai.sh` 部署 issue 站点后会自动验证 issue→assets API 连通性(使用配置的 `ASSETS_API_KEY` 调 assets API),若返回 401 则部署失败退出,防止 API Key 配置遗漏上线。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Docker 部署
|
## Docker 部署
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
import { getDb } from '@/lib/db'
|
||||||
|
|
||||||
function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
||||||
try {
|
try {
|
||||||
|
|
@ -15,10 +17,22 @@ function isValidPayload(payload: Record<string, unknown> | null): boolean {
|
||||||
return !(payload.exp && (payload.exp as number) < Math.floor(Date.now() / 1000))
|
return !(payload.exp && (payload.exp as number) < Math.floor(Date.now() / 1000))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API Key 验证:ALLOWED_API_KEYS 环境变量(快速路径)→ api_keys 数据库表(回退)
|
||||||
function verifyApiKey(key: string): boolean {
|
function verifyApiKey(key: string): boolean {
|
||||||
|
if (!key.startsWith('ak_')) return false
|
||||||
|
// 1. 快速路径:ALLOWED_API_KEYS 环境变量(逗号分隔)
|
||||||
const allowedKeys = process.env.ALLOWED_API_KEYS || ''
|
const allowedKeys = process.env.ALLOWED_API_KEYS || ''
|
||||||
if (!allowedKeys) return false
|
if (allowedKeys && allowedKeys.split(',').map(k => k.trim()).includes(key)) {
|
||||||
return allowedKeys.split(',').map(k => k.trim()).includes(key)
|
return true
|
||||||
|
}
|
||||||
|
// 2. 回退:查 api_keys 数据库表(SHA-256 hash)
|
||||||
|
try {
|
||||||
|
const db = getDb()
|
||||||
|
const keyHash = crypto.createHash('sha256').update(key).digest('hex')
|
||||||
|
const row = db.prepare('SELECT id FROM api_keys WHERE key_hash = ? AND is_active = 1').get(keyHash)
|
||||||
|
if (row) return true
|
||||||
|
} catch { /* DB 不可用时忽略,走后续 cookie 认证 */ }
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLoginRedirect(request: NextRequest) {
|
function buildLoginRedirect(request: NextRequest) {
|
||||||
|
|
@ -44,7 +58,10 @@ export function middleware(request: NextRequest) {
|
||||||
if (authHeader?.startsWith('Bearer ak_')) {
|
if (authHeader?.startsWith('Bearer ak_')) {
|
||||||
const key = authHeader.slice(7)
|
const key = authHeader.slice(7)
|
||||||
if (verifyApiKey(key)) return NextResponse.next()
|
if (verifyApiKey(key)) return NextResponse.next()
|
||||||
if (pathname.startsWith('/api/')) return NextResponse.next()
|
// 无效 key:API 路由返回 401
|
||||||
|
if (pathname.startsWith('/api/')) {
|
||||||
|
return NextResponse.json({ error: '未授权' }, { status: 401 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先检查 tlyq_session(共享 JWT)
|
// 优先检查 tlyq_session(共享 JWT)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue