docs: 记录 API Key 修复与中间件安全加固(v2026.05.14)

- [修复] api_keys 表补注册 issue-ai 使用的 API Key
- [优化] 中间件 API Key 验证统一为 ALLOWED_API_KEYS → DB 两级
This commit is contained in:
gitadmin 2026-05-15 09:13:51 +08:00
parent 5d841a56a6
commit 69694d3fe9
3 changed files with 27 additions and 3 deletions

View File

@ -9,6 +9,8 @@
- [修复] 新增/导入/编辑资产页面无权限守卫:无权限用户直接访问会被重定向到资产列表
- [修复] 资产列表页模板按钮链接指向 404`/assets/template` → `/api/assets/template`
- [调整] editor/viewer 角色权限允许自定义编辑(`db-schema.ts` 仅强制同步 admin三个内置角色均不可删除
- [修复] issue-ai 调用 assets API 返回 401云端 `api_keys` 表中注册了 issue-ai 使用的 API Key统一 Key此前只存在 issue-ai 的 ALLOWED_API_KEYS 环境变量中assets-ai 的 api_keys 表缺失该记录)
- [优化] 中间件 API Key 验证统一为 ALLOWED_API_KEYS快速路径→ api_keys 数据库表(回退)两级验证;无效 key 不再被放行,中间件层直接返回 401安全加固与 issue-ai 行为一致)
## 2026-05-12

View File

@ -136,7 +136,7 @@ npm run import # 导入设备数据
- **Web UIv2.1**`middleware.ts` 优先检查 `tlyq_session`(共享 JWTOA 统一签发)→ 回退 `session_assets`(本地 JWT。`getSession()` 每次请求时检查 LLDAP 用户是否存在,已删除则清除 cookie 踢出
- **localadmin**:纯本地 BCrypt 认证,不依赖 LLDAP用于 LLDAP 故障时应急登录DB 预置admin 角色)
- **API Key**`Bearer ak_<32位十六进制>`,存储时 SHA-256 hash验证时更新 `last_used_at`,权限由创建时指定的 `permissions` 数组控制
- **API Keyv2.2**`middleware.ts` 采用两级验证:① `ALLOWED_API_KEYS` 环境变量快速匹配(逗号分隔明文 key② 未命中则查 `api_keys` 数据库表SHA-256 hash。无效 key 在中间件层直接返回 401此前仅检查 `Bearer ak_` 前缀就放行,实际验证延后到 route handler。外部系统如 issue-ai调用本系统 API 时key 可注册在 `ALLOWED_API_KEYS``api_keys` 表中任意一处即可
---

View File

@ -1,5 +1,24 @@
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import crypto from 'crypto'
import db from '@/lib/db'
// API Key 验证ALLOWED_API_KEYS 环境变量(快速路径)→ api_keys 数据库表(回退)
function verifyApiKey(key: string): boolean {
if (!key.startsWith('ak_')) return false
// 1. 快速路径ALLOWED_API_KEYS 环境变量
const allowedKeys = process.env.ALLOWED_API_KEYS || ''
if (allowedKeys && allowedKeys.split(',').map(k => k.trim()).includes(key)) {
return true
}
// 2. 回退:查 api_keys 数据库表
try {
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 不可用时忽略 */ }
return false
}
function decodeJwtPayload(token: string): Record<string, unknown> | null {
try {
@ -24,10 +43,13 @@ export function middleware(request: NextRequest) {
return NextResponse.next()
}
// API 路由:检查 session_assets cookie 或 Bearer API Key
// API 路由:检查 Bearer API Key 或 session cookie
if (pathname.startsWith('/api/')) {
const authHeader = request.headers.get('authorization')
if (authHeader?.startsWith('Bearer ak_')) return NextResponse.next()
if (authHeader?.startsWith('Bearer ak_')) {
if (verifyApiKey(authHeader.slice(7))) return NextResponse.next()
return NextResponse.json({ error: '未授权' }, { status: 401 })
}
const sharedToken = request.cookies.get('tlyq_session')?.value
const sharedPayload = sharedToken ? decodeJwtPayload(sharedToken) : null