From f31f7c9385e7c9c5c0c001daa703be17e14230fb Mon Sep 17 00:00:00 2001 From: iRubbish Date: Mon, 25 Aug 2025 18:19:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=8E=86=E5=8F=B2API?= =?UTF-8?q?=20Key=E8=87=AA=E5=8A=A8=E5=85=B3=E8=81=94=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心功能: - AD用户登录时自动关联已存在的历史API Key - 关联规则: API Key name字段与用户displayName完全匹配 - 自动设置owner字段完成关联,避免用户重新创建Key 实现逻辑: 1. 优先匹配owner字段(已关联的Key) 2. 如无owner匹配,尝试匹配name与displayName 3. 找到匹配历史Key后,自动设置owner完成关联 技术特性: - 详细日志记录关联过程 - 支持JWT token中完整用户信息传递 - Redis数据自动更新owner字段 - 系统迁移兼容性处理 测试验证: - 创建测试历史Key验证自动关联 - JWT token正确解析displayName字段 - Redis数据正确更新owner关联关系 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- package-lock.json | 73 +++++++++++++++++++++++++++++++--------- package.json | 1 + src/routes/ldapRoutes.js | 47 ++++++++++++++++++++++++-- 3 files changed, 103 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 72fbe9fc..c428539e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "jsonwebtoken": "^9.0.2", "ldapjs": "^3.0.7", "morgan": "^1.10.0", + "node-fetch": "^2.7.0", "ora": "^5.4.1", "rate-limiter-flexible": "^5.0.5", "socks-proxy-agent": "^8.0.2", @@ -4094,7 +4095,7 @@ }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "license": "MIT", "engines": { @@ -4868,7 +4869,7 @@ }, "node_modules/fetch-blob": { "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/fetch-blob/-/fetch-blob-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", "funding": [ { @@ -5049,7 +5050,7 @@ }, "node_modules/formdata-polyfill": { "version": "4.0.10", - "resolved": "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "license": "MIT", "dependencies": { @@ -5138,6 +5139,24 @@ "node": ">=18" } }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/gcp-metadata": { "version": "7.0.1", "resolved": "https://registry.npmmirror.com/gcp-metadata/-/gcp-metadata-7.0.1.tgz", @@ -7143,7 +7162,7 @@ }, "node_modules/node-domexception": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", "deprecated": "Use your platform's native DOMException instead", "funding": [ @@ -7162,21 +7181,23 @@ } }, "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" + "whatwg-url": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "4.x || >=6.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, "node_modules/node-int64": { @@ -8855,6 +8876,12 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmmirror.com/triple-beam/-/triple-beam-1.4.1.tgz", @@ -9096,13 +9123,29 @@ }, "node_modules/web-streams-polyfill": { "version": "3.3.3", - "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "license": "MIT", "engines": { "node": ">= 8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index bdfaf284..3c08d7da 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "jsonwebtoken": "^9.0.2", "ldapjs": "^3.0.7", "morgan": "^1.10.0", + "node-fetch": "^2.7.0", "ora": "^5.4.1", "rate-limiter-flexible": "^5.0.5", "socks-proxy-agent": "^8.0.2", diff --git a/src/routes/ldapRoutes.js b/src/routes/ldapRoutes.js index 4608bb8d..133a558b 100644 --- a/src/routes/ldapRoutes.js +++ b/src/routes/ldapRoutes.js @@ -384,24 +384,41 @@ const authenticateUser = (req, res, next) => { /** * 获取用户的API Keys + * + * 自动关联逻辑说明: + * 系统迁移过程中存在历史API Key,这些Key是在AD集成前手动创建的 + * 创建时使用的name字段恰好与AD用户的displayName一致 + * 例如: AD用户displayName为"测试用户",对应的API Key name也是"测试用户" + * 为了避免用户重复创建Key,系统会自动关联这些历史Key + * 关联规则: + * 1. 优先匹配owner字段(新建的Key) + * 2. 如果没有owner匹配,则尝试匹配name字段与displayName + * 3. 找到匹配的历史Key后,自动将owner设置为当前用户,完成关联 */ router.get('/user/api-keys', authenticateUser, async (req, res) => { try { const redis = require('../models/redis') - const { username } = req.user + const { username, displayName } = req.user - logger.info(`获取用户API Keys: ${username}`) + logger.info(`获取用户API Keys: ${username}, displayName: ${displayName}`) + logger.info(`用户完整信息: ${JSON.stringify(req.user)}`) // 获取所有API Keys const allKeysPattern = 'api_key:*' const keys = await redis.getClient().keys(allKeysPattern) const userKeys = [] + let foundHistoricalKey = false // 筛选属于该用户的API Keys for (const key of keys) { const apiKeyData = await redis.getClient().hgetall(key) - if (apiKeyData && apiKeyData.owner === username) { + if (!apiKeyData) { + continue + } + + // 规则1: 直接owner匹配(已关联的Key) + if (apiKeyData.owner === username) { userKeys.push({ id: apiKeyData.id, name: apiKeyData.name || '未命名', @@ -412,6 +429,30 @@ router.get('/user/api-keys', authenticateUser, async (req, res) => { status: apiKeyData.status || 'active' }) } + // 规则2: 历史Key自动关联(name字段匹配displayName且无owner) + else if (displayName && apiKeyData.name === displayName && !apiKeyData.owner) { + logger.info(`发现历史API Key需要关联: name=${apiKeyData.name}, displayName=${displayName}`) + + // 自动关联: 设置owner为当前用户 + await redis.getClient().hset(key, 'owner', username) + foundHistoricalKey = true + + userKeys.push({ + id: apiKeyData.id, + name: apiKeyData.name || '未命名', + key: apiKeyData.key, + limit: parseInt(apiKeyData.limit) || 1000000, + used: parseInt(apiKeyData.used) || 0, + createdAt: apiKeyData.createdAt, + status: apiKeyData.status || 'active' + }) + + logger.info(`历史API Key关联成功: ${apiKeyData.id} -> ${username}`) + } + } + + if (foundHistoricalKey) { + logger.info(`用户 ${username} 自动关联了历史API Key`) } res.json({