feat(auth): aggregate OAuth 支持 auxx.cn 自定义规格(按 endpoint 自动分派)\n\n- oauth-strategies.ts: endpoint 命中 auxx.cn 时走新的 auxxStrategy\n - getAuthorizeUrl: GET {endpoint}/connect.php?act=login 拿 {url} 后转发(附 state)\n - exchangeToken: 直接 GET {endpoint}/connect.php?act=callback 拿 social_uid/access_token/nickname/faceimg,10 分钟内存缓存供 getUserInfo 读取\n - getUserInfo: 命中缓存返回 OAuthUserInfo(id=social_uid, username=nickname, avatar=faceimg)\n- 标准 OAuth2 /oauth/... 规格仍由 standardAggregateStrategy 保留,非 auxx 域名不受影响
This commit is contained in:
@@ -312,7 +312,98 @@ function aggregateSign(params: Record<string, string>, appKey: string): string {
|
|||||||
return createHmac('sha256', appKey).update(sorted).digest('hex')
|
return createHmac('sha256', appKey).update(sorted).digest('hex')
|
||||||
}
|
}
|
||||||
|
|
||||||
const aggregateStrategy: OAuthStrategy = {
|
const auxxUserInfoCache = new Map<string, any>()
|
||||||
|
|
||||||
|
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) {
|
async getAuthorizeUrl(redirectUri: string, state: string) {
|
||||||
const settings = await loadAggregateSettings()
|
const settings = await loadAggregateSettings()
|
||||||
if (!settings?.aggregateOAuthEnabled || !settings.aggregateOAuthAppId || !settings.aggregateOAuthAppKey || !settings.aggregateOAuthEndpoint) {
|
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
|
strategies.aggregate = aggregateStrategy
|
||||||
|
|
||||||
export const getOAuthStrategy = (provider: string): OAuthStrategy => {
|
export const getOAuthStrategy = (provider: string): OAuthStrategy => {
|
||||||
|
|||||||
Reference in New Issue
Block a user