From 2628de051d5aefe0f9f8d1063d29b28d2cc456da Mon Sep 17 00:00:00 2001 From: MarSeventh <1193267292@qq.com> Date: Tue, 10 Mar 2026 10:52:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20API=20Token=20?= =?UTF-8?q?=E8=BF=87=E6=9C=9F=E6=97=B6=E9=97=B4=E5=90=8E=E7=AB=AF=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 tokenExpiration.js 工具模块(过期判定、自动删除过滤) - apiTokens.js 支持 expiresAt/autoDelete 字段的创建、更新、自动清理 - 新增 getTokenData 导出函数 - tokenValidator.js 增加过期检查逻辑 --- functions/api/manage/apiTokens.js | 86 +++++++++++++++++++++++++----- functions/utils/tokenExpiration.js | 40 ++++++++++++++ functions/utils/tokenValidator.js | 23 +++++--- package.json | 2 +- 4 files changed, 131 insertions(+), 20 deletions(-) create mode 100644 functions/utils/tokenExpiration.js diff --git a/functions/api/manage/apiTokens.js b/functions/api/manage/apiTokens.js index 34b6c62b..9f25e9b7 100644 --- a/functions/api/manage/apiTokens.js +++ b/functions/api/manage/apiTokens.js @@ -1,4 +1,5 @@ import { getDatabase } from '../../utils/databaseAdapter.js'; +import { filterAutoDeleteTokens } from '../../utils/tokenExpiration.js'; export async function onRequest(context) { // API Token管理,支持创建、删除、列出Token @@ -24,7 +25,7 @@ export async function onRequest(context) { // POST - 创建新Token if (method === 'POST') { const body = await request.json() - const { name, permissions, owner } = body + const { name, permissions, owner, expiresAt = null, autoDelete = false } = body if (!name || !permissions || !owner) { return new Response(JSON.stringify({ error: '缺少必要参数' }), { @@ -35,7 +36,7 @@ export async function onRequest(context) { }) } - const token = await createApiToken(db, name, permissions, owner) + const token = await createApiToken(db, name, permissions, owner, expiresAt, autoDelete) return new Response(JSON.stringify(token), { headers: { 'content-type': 'application/json', @@ -67,7 +68,7 @@ export async function onRequest(context) { // PUT - 更新Token权限 if (method === 'PUT') { const body = await request.json() - const { tokenId, permissions } = body + const { tokenId, permissions, expiresAt = null, autoDelete = false } = body if (!tokenId || !permissions) { return new Response(JSON.stringify({ error: '缺少必要参数' }), { @@ -78,7 +79,7 @@ export async function onRequest(context) { }) } - const result = await updateApiToken(db, tokenId, permissions) + const result = await updateApiToken(db, tokenId, permissions, expiresAt, autoDelete) return new Response(JSON.stringify(result), { headers: { 'content-type': 'application/json', @@ -95,8 +96,8 @@ async function getApiTokens(db) { const settings = settingsStr ? JSON.parse(settingsStr) : {} const tokens = settings.apiTokens?.tokens || {} - // 返回时不包含实际token值,只返回基本信息 - const tokenList = Object.keys(tokens).map(id => { + // 将 tokens 对象转为数组,并应用向后兼容默认值 + const tokenArray = Object.keys(tokens).map(id => { const token = tokens[id] return { id, @@ -105,15 +106,41 @@ async function getApiTokens(db) { permissions: token.permissions, createdAt: token.createdAt, updatedAt: token.updatedAt, - token: token.token.substr(0, 15) + '...' // 只显示前15位 + token: token.token, + expiresAt: token.expiresAt ?? null, + autoDelete: token.autoDelete ?? false } }) + // 使用 filterAutoDeleteTokens 识别需要自动删除的 Token + const { toDelete, toKeep } = filterAutoDeleteTokens(tokenArray) + + // 从数据库中删除符合自动删除条件的 Token + if (toDelete.length > 0) { + for (const t of toDelete) { + delete settings.apiTokens.tokens[t.id] + } + await db.put('manage@sysConfig@security', JSON.stringify(settings)) + } + + // 返回时不包含实际token值,只返回基本信息 + const tokenList = toKeep.map(t => ({ + id: t.id, + name: t.name, + owner: t.owner, + permissions: t.permissions, + createdAt: t.createdAt, + updatedAt: t.updatedAt, + token: t.token.substr(0, 15) + '...', // 只显示前15位 + expiresAt: t.expiresAt, + autoDelete: t.autoDelete + })) + return { tokens: tokenList } } // 创建新的API Token -async function createApiToken(db, name, permissions, owner) { +async function createApiToken(db, name, permissions, owner, expiresAt = null, autoDelete = false) { const settingsStr = await db.get('manage@sysConfig@security') const settings = settingsStr ? JSON.parse(settingsStr) : {} @@ -132,7 +159,9 @@ async function createApiToken(db, name, permissions, owner) { owner, permissions, createdAt: now, - updatedAt: now + updatedAt: now, + expiresAt: expiresAt ?? null, + autoDelete: autoDelete === true } settings.apiTokens.tokens[tokenId] = tokenData @@ -147,7 +176,9 @@ async function createApiToken(db, name, permissions, owner) { owner, permissions, createdAt: now, - updatedAt: now + updatedAt: now, + expiresAt: tokenData.expiresAt, + autoDelete: tokenData.autoDelete } } @@ -168,8 +199,8 @@ async function deleteApiToken(db, tokenId) { return { success: true, message: 'Token 已删除' } } -// 更新API Token权限 -async function updateApiToken(db, tokenId, permissions) { +// 更新API Token +async function updateApiToken(db, tokenId, permissions, expiresAt = null, autoDelete = false) { const settingsStr = await db.get('manage@sysConfig@security') const settings = settingsStr ? JSON.parse(settingsStr) : {} @@ -179,13 +210,15 @@ async function updateApiToken(db, tokenId, permissions) { settings.apiTokens.tokens[tokenId].permissions = permissions settings.apiTokens.tokens[tokenId].updatedAt = new Date().toISOString() + settings.apiTokens.tokens[tokenId].expiresAt = expiresAt ?? null + settings.apiTokens.tokens[tokenId].autoDelete = autoDelete === true // 保存到数据库 await db.put('manage@sysConfig@security', JSON.stringify(settings)) return { success: true, - message: 'Token 权限已更新', + message: 'Token 已更新', token: settings.apiTokens.tokens[tokenId] } } @@ -220,3 +253,30 @@ export async function getTokenPermissions(db, token) { return null } + +// 根据Token获取完整数据对象(供tokenValidator使用) +export async function getTokenData(db, token) { + const settingsStr = await db.get('manage@sysConfig@security') + const settings = settingsStr ? JSON.parse(settingsStr) : {} + const tokens = settings.apiTokens?.tokens || {} + + // 查找匹配的token + for (const tokenId in tokens) { + if (tokens[tokenId].token === token) { + const t = tokens[tokenId] + return { + id: t.id, + name: t.name, + token: t.token, + owner: t.owner, + permissions: t.permissions, + createdAt: t.createdAt, + updatedAt: t.updatedAt, + expiresAt: t.expiresAt ?? null, + autoDelete: t.autoDelete ?? false + } + } + } + + return null +} diff --git a/functions/utils/tokenExpiration.js b/functions/utils/tokenExpiration.js new file mode 100644 index 00000000..0c8a95d9 --- /dev/null +++ b/functions/utils/tokenExpiration.js @@ -0,0 +1,40 @@ +/** + * Token 过期时间工具模块(后端) + * 提供 Token 过期判定和自动删除过滤功能 + */ + +/** + * 判定 Token 是否已过期 + * @param {string|null} expiresAt - 过期时间 ISO 8601 字符串,null 表示永不过期 + * @param {Date} [now=new Date()] - 当前时间 + * @returns {boolean} 是否已过期 + */ +export function isExpired(expiresAt, now = new Date()) { + if (expiresAt === null || expiresAt === undefined) { + return false; + } + const expiresDate = new Date(expiresAt); + const currentTime = now instanceof Date ? now : new Date(now); + return currentTime.getTime() > expiresDate.getTime(); +} + +/** + * 过滤出需要自动删除的 Token 并返回保留的 Token 列表 + * @param {Array} tokens - Token 数组 + * @param {Date} [now=new Date()] - 当前时间 + * @returns {{ toDelete: Array, toKeep: Array }} + */ +export function filterAutoDeleteTokens(tokens, now = new Date()) { + const toDelete = []; + const toKeep = []; + + for (const token of tokens) { + if (isExpired(token.expiresAt, now) && token.autoDelete === true) { + toDelete.push(token); + } else { + toKeep.push(token); + } + } + + return { toDelete, toKeep }; +} diff --git a/functions/utils/tokenValidator.js b/functions/utils/tokenValidator.js index 0efa185f..cae47b95 100644 --- a/functions/utils/tokenValidator.js +++ b/functions/utils/tokenValidator.js @@ -1,5 +1,6 @@ // API Token权限验证工具函数 -import { getTokenPermissions } from '../api/manage/apiTokens.js'; +import { getTokenData } from '../api/manage/apiTokens.js'; +import { isExpired } from './tokenExpiration.js'; /** * 验证API Token权限 @@ -28,15 +29,20 @@ export async function validateApiToken(request, db, requiredPermission) { return { valid: false, error: '无效的Token格式' }; } - // 获取Token权限 - const permissions = await getTokenPermissions(db, token); + // 获取完整Token数据 + const tokenData = await getTokenData(db, token); - if (!permissions) { + if (!tokenData) { return { valid: false, error: '无效的Token' }; } + // 检查Token是否已过期 + if (isExpired(tokenData.expiresAt)) { + return { valid: false, error: 'Token 已过期' }; + } + // 检查权限,如果不需要特定权限(requiredPermission为null),则只要token有效就通过 - if (requiredPermission !== null && !permissions.includes(requiredPermission)) { + if (requiredPermission !== null && !tokenData.permissions.includes(requiredPermission)) { return { valid: false, error: `缺少${requiredPermission}权限` }; } @@ -76,7 +82,12 @@ export async function getTokenInfo(request, kv) { // 查找匹配的token for (const tokenId in tokens) { if (tokens[tokenId].token === token) { - return tokens[tokenId]; + const t = tokens[tokenId]; + return { + ...t, + expiresAt: t.expiresAt ?? null, + autoDelete: t.autoDelete ?? false + }; } } diff --git a/package.json b/package.json index 50b6a23e..2e3f7d40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cloudflare-imgbed", - "version": "2.6.2", + "version": "2.6.3", "type": "module", "scripts": { "ci-test": "concurrently --kill-others \"npm start\" \"wait-on http://localhost:8080 && mocha\"",