diff --git a/napcat.webui/src/controllers/webui_manager.ts b/napcat.webui/src/controllers/webui_manager.ts index 5a0c3150..bb31f761 100644 --- a/napcat.webui/src/controllers/webui_manager.ts +++ b/napcat.webui/src/controllers/webui_manager.ts @@ -33,21 +33,6 @@ export default class WebUIManager { return data.data } - public static async changePasswordFromDefault(newToken: string) { - const { data } = await serverRequest.post>( - '/auth/update_token', - { newToken, fromDefault: true } - ) - return data.data - } - - public static async checkUsingDefaultToken() { - const { data } = await serverRequest.get>( - '/auth/check_using_default_token' - ) - return data.data - } - public static async proxy(url = '') { const data = await serverRequest.get>( '/base/proxy?url=' + encodeURIComponent(url) diff --git a/napcat.webui/src/pages/dashboard/config/change_password.tsx b/napcat.webui/src/pages/dashboard/config/change_password.tsx index c5d18f0d..f4afc4b0 100644 --- a/napcat.webui/src/pages/dashboard/config/change_password.tsx +++ b/napcat.webui/src/pages/dashboard/config/change_password.tsx @@ -1,6 +1,5 @@ import { Input } from '@heroui/input' import { useLocalStorage } from '@uidotdev/usehooks' -import { useEffect, useState } from 'react' import { Controller, useForm } from 'react-hook-form' import toast from 'react-hot-toast' import { useNavigate } from 'react-router-dom' @@ -12,9 +11,6 @@ import SaveButtons from '@/components/button/save_buttons' import WebUIManager from '@/controllers/webui_manager' const ChangePasswordCard = () => { - const [isDefaultToken, setIsDefaultToken] = useState(false) - const [isLoadingCheck, setIsLoadingCheck] = useState(true) - const { control, handleSubmit: handleWebuiSubmit, @@ -33,31 +29,10 @@ const ChangePasswordCard = () => { const navigate = useNavigate() const [_, setToken] = useLocalStorage(key.token, '') - // 检查是否使用默认密码 - useEffect(() => { - const checkDefaultToken = async () => { - try { - const isDefault = await WebUIManager.checkUsingDefaultToken() - setIsDefaultToken(isDefault) - } catch (error) { - console.error('检查默认密码状态失败:', error) - } finally { - setIsLoadingCheck(false) - } - } - - checkDefaultToken() - }, []) - const onSubmit = handleWebuiSubmit(async (data) => { try { - if (isDefaultToken) { - // 从默认密码更新 - await WebUIManager.changePasswordFromDefault(data.newToken) - } else { - // 正常密码更新 - await WebUIManager.changePassword(data.oldToken, data.newToken) - } + // 使用正常密码更新流程 + await WebUIManager.changePassword(data.oldToken, data.newToken) toast.success('修改成功') setToken('') @@ -69,43 +44,22 @@ const ChangePasswordCard = () => { } }) - if (isLoadingCheck) { - return ( - <> - 修改密码 - NapCat WebUI -
-
加载中...
-
- - ) - } - return ( <> 修改密码 - NapCat WebUI - {isDefaultToken && ( -
-

- 检测到您正在使用默认密码,为了安全起见,请立即设置新密码。 -

-
- )} - - {!isDefaultToken && ( - ( - - )} - /> - )} + ( + + )} + /> { render={({ field }) => ( )} diff --git a/napcat.webui/src/pages/index.tsx b/napcat.webui/src/pages/index.tsx index b31427d7..ebe4cd66 100644 --- a/napcat.webui/src/pages/index.tsx +++ b/napcat.webui/src/pages/index.tsx @@ -1,52 +1,15 @@ import { Spinner } from '@heroui/spinner' import { AnimatePresence, motion } from 'motion/react' -import { Suspense, useEffect } from 'react' -import { Outlet, useLocation, useNavigate } from 'react-router-dom' +import { Suspense } from 'react' +import { Outlet, useLocation } from 'react-router-dom' -import useAuth from '@/hooks/auth' -import useDialog from '@/hooks/use-dialog' - -import WebUIManager from '@/controllers/webui_manager' import DefaultLayout from '@/layouts/default' -const CheckDefaultPassword = () => { - const { isAuth } = useAuth() - const dialog = useDialog() - const navigate = useNavigate() - const checkDefaultPassword = async () => { - const data = await WebUIManager.checkUsingDefaultToken() - if (data) { - dialog.confirm({ - title: '修改默认密码', - content: '检测到当前密码为默认密码,为了您的安全,必须立即修改密码。', - confirmText: '前往修改', - onConfirm: () => { - navigate('/config?tab=token') - }, - onCancel: () => { - navigate('/config?tab=token') - }, - onClose() { - navigate('/config?tab=token') - }, - }) - } - } - - useEffect(() => { - if (isAuth) { - checkDefaultPassword() - } - }, [isAuth]) - return null -} - export default function IndexPage() { const location = useLocation() return ( - diff --git a/src/onebot/index.ts b/src/onebot/index.ts index 247659e2..02945ced 100644 --- a/src/onebot/index.ts +++ b/src/onebot/index.ts @@ -17,6 +17,7 @@ import { NTMsgAtType, } from '@/core'; import { OB11ConfigLoader } from '@/onebot/config'; +import { pendingTokenToSend } from '@/webui/index'; import { OB11HttpClientAdapter, OB11WebSocketClientAdapter, @@ -64,8 +65,8 @@ export class NapCatOneBot11Adapter { networkManager: OB11NetworkManager; actions: ActionMap; private readonly bootTime = Date.now() / 1000; - recallEventCache = new Map(); - constructor(core: NapCatCore, context: InstanceContext, pathWrapper: NapCatPathWrapper) { + recallEventCache = new Map(); + constructor (core: NapCatCore, context: InstanceContext, pathWrapper: NapCatPathWrapper) { this.core = core; this.context = context; this.configLoader = new OB11ConfigLoader(core, pathWrapper.configPath, OneBotConfigSchema); @@ -79,7 +80,7 @@ export class NapCatOneBot11Adapter { this.actions = createActionMap(this, core); this.networkManager = new OB11NetworkManager(); } - async creatOneBotLog(ob11Config: OneBotConfig) { + async creatOneBotLog (ob11Config: OneBotConfig) { let log = '[network] 配置加载\n'; for (const key of ob11Config.network.httpServers) { log += `HTTP服务: ${key.host}:${key.port}, : ${key.enable ? '已启动' : '未启动'}\n`; @@ -98,15 +99,44 @@ export class NapCatOneBot11Adapter { } return log; } - async InitOneBot() { + async InitOneBot () { const selfInfo = this.core.selfInfo; const ob11Config = this.configLoader.configData; this.core.apis.UserApi.getUserDetailInfo(selfInfo.uid, false) - .then((user) => { + .then(async (user) => { selfInfo.nick = user.nick; this.context.logger.setLogSelfInfo(selfInfo); - WebUiDataRuntime.getQQLoginCallback()(true); + + // 检查是否有待发送的token + if (pendingTokenToSend) { + this.context.logger.log('[NapCat] [OneBot] 🔐 检测到待发送的WebUI Token,开始发送'); + try { + await this.core.apis.MsgApi.sendMsg( + { chatType: ChatType.KCHATTYPEC2C, peerUid: selfInfo.uid, guildId: '' }, + [{ + elementType: ElementType.TEXT, + elementId: '', + textElement: { + content: + '[NapCat] 温馨提示:\n'+ + 'WebUI密码为默认密码,已进行强制修改\n'+ + '新密码: ' +pendingTokenToSend, + atType: NTMsgAtType.ATTYPEUNKNOWN, + atUid: '', + atTinyId: '', + atNtUid: '', + } + }], + 5000 + ); + this.context.logger.log('[NapCat] [OneBot] ✅ WebUI Token 消息发送成功'); + } catch (error) { + this.context.logger.logError('[NapCat] [OneBot] ❌ WebUI Token 消息发送失败:', error); + } + } + + WebUiDataRuntime.getQQLoginCallback()(true); }) .catch(e => this.context.logger.logError(e)); @@ -120,7 +150,7 @@ export class NapCatOneBot11Adapter { // new OB11PluginAdapter('myPlugin', this.core, this,this.actions) // ); if (existsSync(this.context.pathWrapper.pluginPath)) { - this.context.logger.log(`[Plugins] 插件目录存在,开始加载插件`); + this.context.logger.log('[Plugins] 插件目录存在,开始加载插件'); this.networkManager.registerAdapter( new OB11PluginMangerAdapter('plugin_manager', this.core, this, this.actions) ); @@ -181,25 +211,6 @@ export class NapCatOneBot11Adapter { WebUiDataRuntime.setQQVersion(this.core.context.basicInfoWrapper.getFullQQVersion()); WebUiDataRuntime.setQQLoginInfo(selfInfo); WebUiDataRuntime.setQQLoginStatus(true); - - let sendWebUiToken = async (token: string) => { - await this.core.apis.MsgApi.sendMsg( - { chatType: ChatType.KCHATTYPEC2C, peerUid: selfInfo.uid, guildId: '' }, - [{ - elementType: ElementType.TEXT, - elementId: '', - textElement: { - content: 'Update WebUi Token: ' + token, - atType: NTMsgAtType.ATTYPEUNKNOWN, - atUid: '', - atTinyId: '', - atNtUid: '', - } - }], - 5000 - ) - }; - WebUiDataRuntime.setWebUiTokenChangeCallback(sendWebUiToken); WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig) => { const prev = this.configLoader.configData; this.configLoader.save(newConfig); @@ -209,7 +220,7 @@ export class NapCatOneBot11Adapter { } - private async reloadNetwork(prev: OneBotConfig, now: OneBotConfig): Promise { + private async reloadNetwork (prev: OneBotConfig, now: OneBotConfig): Promise { const prevLog = await this.creatOneBotLog(prev); const newLog = await this.creatOneBotLog(now); this.context.logger.log(`[Notice] [OneBot11] 配置变更前:\n${prevLog}`); @@ -222,7 +233,7 @@ export class NapCatOneBot11Adapter { await this.handleConfigChange(prev.network.websocketClients, now.network.websocketClients, OB11WebSocketClientAdapter); } - private async handleConfigChange( + private async handleConfigChange ( prevConfig: NetworkAdapterConfig[], nowConfig: NetworkAdapterConfig[], adapterClass: new ( @@ -254,7 +265,7 @@ export class NapCatOneBot11Adapter { } } - private initMsgListener() { + private initMsgListener () { const msgListener = new NodeIKernelMsgListener(); msgListener.onRecvSysMsg = (msg) => { this.apis.MsgApi.parseSysMessage(msg) @@ -368,7 +379,7 @@ export class NapCatOneBot11Adapter { this.context.session.getMsgService().addKernelMsgListener(proxiedListenerOf(msgListener, this.context.logger)); } - private initBuddyListener() { + private initBuddyListener () { const buddyListener = new NodeIKernelBuddyListener(); buddyListener.onBuddyReqChange = async (reqs) => { @@ -399,7 +410,7 @@ export class NapCatOneBot11Adapter { .addKernelBuddyListener(proxiedListenerOf(buddyListener, this.context.logger)); } - private initGroupListener() { + private initGroupListener () { const groupListener = new NodeIKernelGroupListener(); groupListener.onGroupNotifiesUpdated = async (_, notifies) => { @@ -492,7 +503,7 @@ export class NapCatOneBot11Adapter { .addKernelGroupListener(proxiedListenerOf(groupListener, this.context.logger)); } - private async emitMsg(message: RawMessage) { + private async emitMsg (message: RawMessage) { const network = await this.networkManager.getAllConfig(); this.context.logger.logDebug('收到新消息 RawMessage', message); await Promise.allSettled([ @@ -501,7 +512,7 @@ export class NapCatOneBot11Adapter { ]); } - private async handleMsg(message: RawMessage, network: Array) { + private async handleMsg (message: RawMessage, network: Array) { // 过滤无效消息 if (message.msgType === NTMsgType.KMSGTYPENULL) { return; @@ -522,17 +533,17 @@ export class NapCatOneBot11Adapter { } } - private isSelfMessage(ob11Msg: { - stringMsg: OB11Message; - arrayMsg: OB11Message; + private isSelfMessage (ob11Msg: { + stringMsg: OB11Message + arrayMsg: OB11Message }): boolean { return ob11Msg.stringMsg.user_id.toString() == this.core.selfInfo.uin || ob11Msg.arrayMsg.user_id.toString() == this.core.selfInfo.uin; } - private createMsgMap(network: Array, ob11Msg: { - stringMsg: OB11Message; - arrayMsg: OB11Message; + private createMsgMap (network: Array, ob11Msg: { + stringMsg: OB11Message + arrayMsg: OB11Message }, isSelfMsg: boolean, message: RawMessage): Map { const msgMap: Map = new Map(); network.filter(e => e.enable).forEach(e => { @@ -550,7 +561,7 @@ export class NapCatOneBot11Adapter { return msgMap; } - private handleDebugNetwork(network: Array, msgMap: Map, message: RawMessage) { + private handleDebugNetwork (network: Array, msgMap: Map, message: RawMessage) { const debugNetwork = network.filter(e => e.enable && e.debug); if (debugNetwork.length > 0) { debugNetwork.forEach(adapter => { @@ -564,7 +575,7 @@ export class NapCatOneBot11Adapter { } } - private handleNotReportSelfNetwork(network: Array, msgMap: Map, isSelfMsg: boolean) { + private handleNotReportSelfNetwork (network: Array, msgMap: Map, isSelfMsg: boolean) { if (isSelfMsg) { const notReportSelfNetwork = network.filter(e => e.enable && (('reportSelfMessage' in e && !e.reportSelfMessage) || !('reportSelfMessage' in e))); notReportSelfNetwork.forEach(adapter => { @@ -573,7 +584,7 @@ export class NapCatOneBot11Adapter { } } - private async handleGroupEvent(message: RawMessage) { + private async handleGroupEvent (message: RawMessage) { try { // 群名片修改事件解析 任何都该判断 if (message.senderUin && message.senderUin !== '0') { @@ -606,7 +617,7 @@ export class NapCatOneBot11Adapter { } } - private async handlePrivateMsgEvent(message: RawMessage) { + private async handlePrivateMsgEvent (message: RawMessage) { try { if (message.msgType === NTMsgType.KMSGTYPEGRAYTIPS) { // 灰条为单元素消息 @@ -624,7 +635,7 @@ export class NapCatOneBot11Adapter { } } - private async emitRecallMsg(message: RawMessage, element: MessageElement) { + private async emitRecallMsg (message: RawMessage, element: MessageElement) { const peer: Peer = { chatType: message.chatType, peerUid: message.peerUid, guildId: '' }; const oriMessageId = MessageUnique.getShortIdByMsgId(message.msgId) ?? MessageUnique.createUniqueMsgId(peer, message.msgId); if (message.chatType == ChatType.KCHATTYPEC2C) { @@ -635,7 +646,7 @@ export class NapCatOneBot11Adapter { return; } - private async emitFriendRecallMsg(message: RawMessage, oriMessageId: number, element: MessageElement) { + private async emitFriendRecallMsg (message: RawMessage, oriMessageId: number, element: MessageElement) { const operatorUid = element.grayTipElement?.revokeElement.operatorUid; if (!operatorUid) return undefined; return new OB11FriendRecallNoticeEvent( @@ -645,7 +656,7 @@ export class NapCatOneBot11Adapter { ); } - private async emitGroupRecallMsg(message: RawMessage, oriMessageId: number, element: MessageElement) { + private async emitGroupRecallMsg (message: RawMessage, oriMessageId: number, element: MessageElement) { const operatorUid = element.grayTipElement?.revokeElement.operatorUid; if (!operatorUid) return undefined; const operatorId = await this.core.apis.UserApi.getUinByUidV2(operatorUid); diff --git a/src/webui/index.ts b/src/webui/index.ts index 5fcb56ec..e1f2e576 100644 --- a/src/webui/index.ts +++ b/src/webui/index.ts @@ -4,6 +4,7 @@ import express from 'express'; import { createServer } from 'http'; +import { randomUUID } from 'node:crypto' import { createServer as createHttpsServer } from 'https'; import { LogWrapper } from '@/common/log'; import { NapCatPathWrapper } from '@/common/path'; @@ -30,17 +31,42 @@ const MAX_PORT_TRY = 100; import * as net from 'node:net'; import { WebUiDataRuntime } from './src/helper/Data'; import { existsSync, readFileSync } from 'node:fs'; + export let webUiRuntimePort = 6099; -export async function InitPort(parsedConfig: WebUiConfigType): Promise<[string, number, string]> { +// 全局变量:存储需要在QQ登录成功后发送的新token +export let pendingTokenToSend: string | null = null; + +/** + * 存储WebUI启动时的初始token,用于鉴权 + * - 无论是否在运行时修改密码,都应该使用此token进行鉴权 + * - 运行时手动修改的密码将会在下次napcat重启后生效 + * - 如果需要在运行时修改密码并立即生效,则需要在前端调用路由进行修改 + */ +let initialWebUiToken: string = ''; + +export function setInitialWebUiToken(token: string) { + initialWebUiToken = token; +} + +export function getInitialWebUiToken(): string { + return initialWebUiToken; +} + +export function setPendingTokenToSend(token: string | null) { + pendingTokenToSend = token; +} + +export async function InitPort(parsedConfig: WebUiConfigType): Promise<[string, number,string]> { try { await tryUseHost(parsedConfig.host); const port = await tryUsePort(parsedConfig.port, parsedConfig.host); return [parsedConfig.host, port, parsedConfig.token]; } catch (error) { console.log('host或port不可用', error); - return ['', 0, '']; + return ['', 0, randomUUID()]; } } + async function checkCertificates(logger: LogWrapper): Promise<{ key: string, cert: string } | null> { try { const certPath = join(webUiPathWrapper.configPath, 'cert.pem'); @@ -61,7 +87,27 @@ async function checkCertificates(logger: LogWrapper): Promise<{ key: string, cer export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapper) { webUiPathWrapper = pathWrapper; WebUiConfig = new WebUiConfigWrapper(); - const config = await WebUiConfig.GetWebUIConfig(); + let config = await WebUiConfig.GetWebUIConfig(); + + // 检查并更新默认密码 - 最高优先级 + if (config.defaultToken || config.token === 'napcat' || !config.token) { + const randomToken = Math.random().toString(36).slice(-8); + await WebUiConfig.UpdateWebUIConfig({ token: randomToken, defaultToken: false }); + logger.log(`[NapCat] [WebUi] 🔐 检测到默认密码,已自动更新为安全密码: ${randomToken}`); + + // 存储token到全局变量,等待QQ登录成功后发送 + setPendingTokenToSend(randomToken); + logger.log(`[NapCat] [WebUi] 📤 新密码将在QQ登录成功后发送给用户`); + + // 重新获取更新后的配置 + config = await WebUiConfig.GetWebUIConfig(); + } else { + logger.log(`[NapCat] [WebUi] ✅ 当前使用安全密码: ${config.token}`); + } + + // 存储启动时的初始token用于鉴权 + setInitialWebUiToken(config.token); + logger.log(`[NapCat] [WebUi] 🔑 已缓存启动时的token用于鉴权,运行时手动修改配置文件密码将不会生效`); // 检查是否禁用WebUI if (config.disableWebUI) { @@ -90,19 +136,6 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp } } }); - WebUiDataRuntime.setQQLoginCallback(async (_status: boolean) => { - try { - if ((await WebUiConfig.GetWebUIConfig()).defaultToken) { - let randomToken = Math.random().toString(36).slice(-8); - await WebUiConfig.UpdateWebUIConfig({ token: randomToken }); - console.log(`[NapCat] [WebUi] Update WebUi Token: ${randomToken}`); - await WebUiDataRuntime.getWebUiTokenChangeCallback()(randomToken); - } - } catch (error) { - console.log(`[NapCat] [WebUi] Update WebUi Token failed.` + error); - } - - }); // ------------注册中间件------------ // 使用express的json中间件 app.use(express.json()); @@ -182,7 +215,6 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp // ------------启动服务------------ server.listen(port, host, async () => { - // 启动后打印出相关地址 let searchParams = { token: token }; logger.log( `[NapCat] [WebUi] WebUi User Panel Url: ${createUrl('127.0.0.1', port.toString(), '/webui', searchParams)}` diff --git a/src/webui/src/api/Auth.ts b/src/webui/src/api/Auth.ts index 0022eaca..de244d83 100644 --- a/src/webui/src/api/Auth.ts +++ b/src/webui/src/api/Auth.ts @@ -1,6 +1,6 @@ import { RequestHandler } from 'express'; -import { WebUiConfig } from '@/webui'; +import { WebUiConfig, getInitialWebUiToken, setInitialWebUiToken } from '@/webui'; import { AuthHelper } from '@webapi/helper/SignToken'; import { WebUiDataRuntime } from '@webapi/helper/Data'; @@ -9,10 +9,7 @@ import { isEmpty } from '@webapi/utils/check'; // 检查是否使用默认Token export const CheckDefaultTokenHandler: RequestHandler = async (_, res) => { - const webuiToken = await WebUiConfig.GetWebUIConfig(); - if (webuiToken.defaultToken) { - return sendSuccess(res, true); - } + // 由于密码在WebUI启动时已经确保不是默认密码,这里总是返回false return sendSuccess(res, false); }; @@ -33,8 +30,13 @@ export const LoginHandler: RequestHandler = async (req, res) => { if (!WebUiDataRuntime.checkLoginRate(clientIP, WebUiConfigData.loginRate)) { return sendError(res, 'login rate limit'); } - //验证config.token hash是否等于token hash - if (!AuthHelper.comparePasswordHash(WebUiConfigData.token, hash)) { + // 使用启动时缓存的token进行验证,而不是动态读取配置文件 + const initialToken = getInitialWebUiToken(); + if (!initialToken) { + return sendError(res, 'Server token not initialized'); + } + //验证初始token hash是否等于提交的token hash + if (!AuthHelper.comparePasswordHash(initialToken, hash)) { return sendError(res, 'token is invalid'); } @@ -63,8 +65,6 @@ export const LogoutHandler: RequestHandler = async (req, res) => { // 检查登录状态 export const checkHandler: RequestHandler = async (req, res) => { - // 获取WebUI配置 - const WebUiConfigData = await WebUiConfig.GetWebUIConfig(); // 获取请求头中的Authorization const authorization = req.headers.authorization; // 检查凭证 @@ -79,8 +79,13 @@ export const checkHandler: RequestHandler = async (req, res) => { return sendError(res, 'Token has been revoked'); } + // 使用启动时缓存的token进行验证 + const initialToken = getInitialWebUiToken(); + if (!initialToken) { + return sendError(res, 'Server token not initialized'); + } // 验证凭证是否在一小时内有效 - const valid = AuthHelper.validateCredentialWithinOneHour(WebUiConfigData.token, Credential); + const valid = AuthHelper.validateCredentialWithinOneHour(initialToken, Credential); // 返回成功信息 if (valid) return sendSuccess(res, null); // 返回错误信息 @@ -120,9 +125,21 @@ export const UpdateTokenHandler: RequestHandler = async (req, res) => { return sendError(res, 'Current password is not default password'); } await WebUiConfig.UpdateWebUIConfig({ token: newToken, defaultToken: false }); + // 更新内存中的缓存token,使新密码立即生效 + setInitialWebUiToken(newToken); } else { - // 正常的密码更新流程 - await WebUiConfig.UpdateToken(oldToken, newToken); + // 正常的密码更新流程 - 使用启动时缓存的token进行验证 + const initialToken = getInitialWebUiToken(); + if (!initialToken) { + return sendError(res, 'Server token not initialized'); + } + if (initialToken !== oldToken) { + return sendError(res, '旧 token 不匹配'); + } + // 直接更新配置文件中的token,不需要通过WebUiConfig.UpdateToken方法 + await WebUiConfig.UpdateWebUIConfig({ token: newToken, defaultToken: false }); + // 更新内存中的缓存token,使新密码立即生效 + setInitialWebUiToken(newToken); } return sendSuccess(res, 'Token updated successfully'); diff --git a/src/webui/src/helper/config.ts b/src/webui/src/helper/config.ts index c3e70355..bfbdffad 100644 --- a/src/webui/src/helper/config.ts +++ b/src/webui/src/helper/config.ts @@ -1,4 +1,4 @@ -import { webUiPathWrapper } from '@/webui'; +import { webUiPathWrapper, getInitialWebUiToken } from '@/webui'; import { Type, Static } from '@sinclair/typebox'; import Ajv from 'ajv'; import fs, { constants } from 'node:fs/promises'; @@ -63,6 +63,46 @@ export class WebUiConfigWrapper { } async GetWebUIConfig(): Promise { + if (this.WebUiConfigData) { + return this.WebUiConfigData + } + + try { + const configPath = resolve(webUiPathWrapper.configPath, './webui.json'); + await this.ensureConfigFileExists(configPath); + const parsedConfig = await this.readAndValidateConfig(configPath); + // 使用内存中缓存的token进行覆盖,确保强兼容性 + this.WebUiConfigData = { + ...parsedConfig, + // 首次读取内存中是没有token的,需要进行一层兜底 + token: getInitialWebUiToken() || parsedConfig.token + }; + return this.WebUiConfigData; + } catch (e) { + console.log('读取配置文件失败', e); + const defaultConfig = this.validateAndApplyDefaults({}); + return { + ...defaultConfig, + token: defaultConfig.token + }; + } + } + + async UpdateWebUIConfig(newConfig: Partial): Promise { + const configPath = resolve(webUiPathWrapper.configPath, './webui.json'); + // 使用原始配置进行合并,避免内存token覆盖影响配置更新 + const currentConfig = await this.GetRawWebUIConfig(); + const mergedConfig = deepMerge({ ...currentConfig }, newConfig); + const updatedConfig = this.validateAndApplyDefaults(mergedConfig); + await this.writeConfig(configPath, updatedConfig); + this.WebUiConfigData = updatedConfig; + } + + /** + * 获取配置文件中实际存储的配置(不被内存token覆盖) + * 主要用于配置更新和特殊场景 + */ + async GetRawWebUIConfig(): Promise { if (this.WebUiConfigData) { return this.WebUiConfigData; } @@ -78,18 +118,12 @@ export class WebUiConfigWrapper { } } - async UpdateWebUIConfig(newConfig: Partial): Promise { - const configPath = resolve(webUiPathWrapper.configPath, './webui.json'); - const currentConfig = await this.GetWebUIConfig(); - const mergedConfig = deepMerge({ ...currentConfig }, newConfig); - const updatedConfig = this.validateAndApplyDefaults(mergedConfig); - await this.writeConfig(configPath, updatedConfig); - this.WebUiConfigData = updatedConfig; - } - async UpdateToken(oldToken: string, newToken: string): Promise { - const currentConfig = await this.GetWebUIConfig(); - if (currentConfig.token !== oldToken) { + // 使用内存中缓存的token进行验证,确保强兼容性 + const cachedToken = getInitialWebUiToken(); + const tokenToCheck = cachedToken || (await this.GetWebUIConfig()).token; + + if (tokenToCheck !== oldToken) { throw new Error('旧 token 不匹配'); } await this.UpdateWebUIConfig({ token: newToken, defaultToken: false }); diff --git a/src/webui/src/middleware/auth.ts b/src/webui/src/middleware/auth.ts index 8e2d756c..fd799568 100644 --- a/src/webui/src/middleware/auth.ts +++ b/src/webui/src/middleware/auth.ts @@ -1,6 +1,6 @@ import { NextFunction, Request, Response } from 'express'; -import { WebUiConfig } from '@/webui'; +import { getInitialWebUiToken } from '@/webui'; import { AuthHelper } from '@webapi/helper/SignToken'; import { sendError } from '@webapi/utils/response'; @@ -30,10 +30,13 @@ export async function auth(req: Request, res: Response, next: NextFunction) { } catch (e) { return sendError(res, 'Unauthorized'); } - // 获取配置 - const config = await WebUiConfig.GetWebUIConfig(); + // 使用启动时缓存的token进行验证,而不是动态读取配置文件 因为有可能运行时手动修改了密码 + const initialToken = getInitialWebUiToken(); + if (!initialToken) { + return sendError(res, 'Server token not initialized'); + } // 验证凭证在1小时内有效 - const credentialJson = AuthHelper.validateCredentialWithinOneHour(config.token, Credential); + const credentialJson = AuthHelper.validateCredentialWithinOneHour(initialToken, Credential); if (credentialJson) { // 通过验证 return next();