From 7f3e570d8ab8dcb48a156c12a9f862405ecf4ae1 Mon Sep 17 00:00:00 2001 From: HenryDu8133 <813367384@qq.com> Date: Sat, 12 Sep 2026 23:07:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20aggregate=20OAuth=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20auxx.cn=20=E8=87=AA=E5=AE=9A=E4=B9=89=E8=A7=84?= =?UTF-8?q?=E6=A0=BC=EF=BC=88=E6=8C=89=20endpoint=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=88=86=E6=B4=BE=EF=BC=89\n\n-=20oauth-strategies.ts:=20endpo?= =?UTF-8?q?int=20=E5=91=BD=E4=B8=AD=20auxx.cn=20=E6=97=B6=E8=B5=B0?= =?UTF-8?q?=E6=96=B0=E7=9A=84=20auxxStrategy\n=20=20-=20getAuthorizeUrl:?= =?UTF-8?q?=20GET=20{endpoint}/connect.php=3Fact=3Dlogin=20=E6=8B=BF=20{ur?= =?UTF-8?q?l}=20=E5=90=8E=E8=BD=AC=E5=8F=91=EF=BC=88=E9=99=84=20state?= =?UTF-8?q?=EF=BC=89\n=20=20-=20exchangeToken:=20=E7=9B=B4=E6=8E=A5=20GET?= =?UTF-8?q?=20{endpoint}/connect.php=3Fact=3Dcallback=20=E6=8B=BF=20social?= =?UTF-8?q?=5Fuid/access=5Ftoken/nickname/faceimg=EF=BC=8C10=20=E5=88=86?= =?UTF-8?q?=E9=92=9F=E5=86=85=E5=AD=98=E7=BC=93=E5=AD=98=E4=BE=9B=20getUse?= =?UTF-8?q?rInfo=20=E8=AF=BB=E5=8F=96\n=20=20-=20getUserInfo:=20=E5=91=BD?= =?UTF-8?q?=E4=B8=AD=E7=BC=93=E5=AD=98=E8=BF=94=E5=9B=9E=20OAuthUserInfo?= =?UTF-8?q?=EF=BC=88id=3Dsocial=5Fuid,=20username=3Dnickname,=20avatar=3Df?= =?UTF-8?q?aceimg=EF=BC=89\n-=20=E6=A0=87=E5=87=86=20OAuth2=20/oauth/...?= =?UTF-8?q?=20=E8=A7=84=E6=A0=BC=E4=BB=8D=E7=94=B1=20standardAggregateStra?= =?UTF-8?q?tegy=20=E4=BF=9D=E7=95=99=EF=BC=8C=E9=9D=9E=20auxx=20=E5=9F=9F?= =?UTF-8?q?=E5=90=8D=E4=B8=8D=E5=8F=97=E5=BD=B1=E5=93=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/utils/oauth-strategies.ts | 117 ++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/server/utils/oauth-strategies.ts b/server/utils/oauth-strategies.ts index f69e010..cf3b7d7 100644 --- a/server/utils/oauth-strategies.ts +++ b/server/utils/oauth-strategies.ts @@ -312,7 +312,98 @@ function aggregateSign(params: Record, appKey: string): string { return createHmac('sha256', appKey).update(sorted).digest('hex') } -const aggregateStrategy: OAuthStrategy = { +const auxxUserInfoCache = new Map() + +function isAuxxEndpoint(endpoint: string): boolean { + return /(^|\.)auxx\.cn(\/|$)/i.test(endpoint) +} + +const auxxStrategy: OAuthStrategy = { + async getAuthorizeUrl(redirectUri: string, state: string) { + const settings = await loadAggregateSettings() + if (!settings?.aggregateOAuthEnabled || !settings.aggregateOAuthAppId || !settings.aggregateOAuthAppKey || !settings.aggregateOAuthEndpoint) { + throw createError({ statusCode: 500, message: '聚合登录未配置' }) + } + if (!isSafeAggregateOAuthUrl(settings.aggregateOAuthEndpoint)) { + throw createError({ statusCode: 500, message: '聚合登录端点不安全或格式错误' }) + } + const loginTypes = getAggregateOAuthLoginTypesOrDefault(settings.aggregateOAuthLoginType) + const loginType = loginTypes[0] || 'qq' + const endpoint = normalizeEndpoint(settings.aggregateOAuthEndpoint) + const loginQuery = new URLSearchParams({ + act: 'login', + appid: settings.aggregateOAuthAppId, + appkey: settings.aggregateOAuthAppKey, + type: loginType, + redirect_uri: redirectUri + }).toString() + try { + const resp: any = await $fetch(`${endpoint}/connect.php?${loginQuery}`, { + headers: { Accept: 'application/json' } + }) + if (resp.code !== 0 || !resp.url) { + throw new Error(resp.msg || '获取登录跳转地址失败') + } + const sep = resp.url.includes('?') ? '&' : '?' + return `${resp.url}${sep}state=${encodeURIComponent(state)}` + } catch (e: any) { + console.error('Auxx OAuth authorize URL failed', e) + throw new Error(e.message || '获取登录跳转地址失败') + } + }, + async exchangeToken(code: string, redirectUri: string) { + const settings = await loadAggregateSettings() + if (!settings?.aggregateOAuthEnabled || !settings.aggregateOAuthAppId || !settings.aggregateOAuthAppKey || !settings.aggregateOAuthEndpoint) { + throw createError({ statusCode: 500, message: '聚合登录未配置' }) + } + const loginTypes = getAggregateOAuthLoginTypesOrDefault(settings.aggregateOAuthLoginType) + const loginType = loginTypes[0] || 'qq' + const endpoint = normalizeEndpoint(settings.aggregateOAuthEndpoint) + const cbQuery = new URLSearchParams({ + act: 'callback', + appid: settings.aggregateOAuthAppId, + appkey: settings.aggregateOAuthAppKey, + type: loginType, + code + }).toString() + try { + const resp: any = await $fetch(`${endpoint}/connect.php?${cbQuery}`, { + headers: { Accept: 'application/json' } + }) + if (resp.code !== 0 || !resp.access_token || !resp.social_uid) { + throw new Error(resp.msg || '回调换取用户信息失败') + } + const cacheKey = `${settings.aggregateOAuthAppId}:${resp.access_token}` + auxxUserInfoCache.set(cacheKey, resp) + setTimeout(() => auxxUserInfoCache.delete(cacheKey), 10 * 60 * 1000) + return String(resp.access_token) + } catch (e: any) { + console.error('Auxx OAuth token exchange failed', e) + throw new Error(e.message || '回调换取用户信息失败') + } + }, + async getUserInfo(accessToken: string) { + const settings = await loadAggregateSettings() + const appId = settings?.aggregateOAuthAppId || '' + const cacheKey = `${appId}:${accessToken}` + let resp = auxxUserInfoCache.get(cacheKey) + if (!resp) { + resp = Array.from(auxxUserInfoCache.values()).find((r: any) => r.access_token === accessToken) + } + if (!resp || resp.code !== 0 || !resp.social_uid) { + throw new Error('获取用户信息失败') + } + return { + id: String(resp.social_uid), + username: resp.nickname || `wx_${String(resp.social_uid).slice(0, 8)}`, + name: resp.nickname || undefined, + email: undefined, + avatar: resp.faceimg || undefined + } + } +} + +const standardAggregateStrategy: OAuthStrategy = { async getAuthorizeUrl(redirectUri: string, state: string) { const settings = await loadAggregateSettings() if (!settings?.aggregateOAuthEnabled || !settings.aggregateOAuthAppId || !settings.aggregateOAuthAppKey || !settings.aggregateOAuthEndpoint) { @@ -415,6 +506,30 @@ const aggregateStrategy: OAuthStrategy = { } } +const aggregateStrategy: OAuthStrategy = { + async getAuthorizeUrl(redirectUri: string, state: string) { + const settings = await loadAggregateSettings() + const endpoint = settings?.aggregateOAuthEndpoint || '' + return isAuxxEndpoint(endpoint) + ? auxxStrategy.getAuthorizeUrl(redirectUri, state) + : standardAggregateStrategy.getAuthorizeUrl(redirectUri, state) + }, + async exchangeToken(code: string, redirectUri: string) { + const settings = await loadAggregateSettings() + const endpoint = settings?.aggregateOAuthEndpoint || '' + return isAuxxEndpoint(endpoint) + ? auxxStrategy.exchangeToken(code, redirectUri) + : standardAggregateStrategy.exchangeToken(code, redirectUri) + }, + async getUserInfo(accessToken: string) { + const settings = await loadAggregateSettings() + const endpoint = settings?.aggregateOAuthEndpoint || '' + return isAuxxEndpoint(endpoint) + ? auxxStrategy.getUserInfo(accessToken) + : standardAggregateStrategy.getUserInfo(accessToken) + } +} + strategies.aggregate = aggregateStrategy export const getOAuthStrategy = (provider: string): OAuthStrategy => {