From a80b36896dc35882b09e167f40c111d11beef4fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E7=BE=BD?= Date: Mon, 4 Aug 2025 00:59:57 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20Gemini=20OAuth=20?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E6=94=AF=E6=8C=81=E6=96=B0=E7=9A=84=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 codeassist.google.com 作为新的回调地址 - 实现 PKCE 认证流程增强安全性 - 更新前端授权流程指引 - 简化授权码输入流程 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/routes/admin.js | 30 ++++++++++----- src/services/geminiAccountService.js | 38 +++++++++++++++---- .../src/components/accounts/OAuthFlow.vue | 33 +++++++--------- 3 files changed, 63 insertions(+), 38 deletions(-) diff --git a/src/routes/admin.js b/src/routes/admin.js index c0cba6e4..31fcfba7 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -1327,19 +1327,20 @@ router.post('/gemini-accounts/generate-auth-url', authenticateAdmin, async (req, try { const { state } = req.body; - // 使用固定的 localhost:45462 作为回调地址 - const redirectUri = 'http://localhost:45462'; + // 使用新的 codeassist.google.com 回调地址 + const redirectUri = 'https://codeassist.google.com/authcode'; logger.info(`Generating Gemini OAuth URL with redirect_uri: ${redirectUri}`); - const { authUrl, state: authState } = await geminiAccountService.generateAuthUrl(state, redirectUri); + const { authUrl, state: authState, codeVerifier, redirectUri: finalRedirectUri } = await geminiAccountService.generateAuthUrl(state, redirectUri); - // 创建 OAuth 会话 + // 创建 OAuth 会话,包含 codeVerifier const sessionId = authState; await redis.setOAuthSession(sessionId, { state: authState, type: 'gemini', - redirectUri: redirectUri, // 保存固定的 redirect_uri 用于 token 交换 + redirectUri: finalRedirectUri, + codeVerifier: codeVerifier, // 保存 PKCE code verifier createdAt: new Date().toISOString() }); @@ -1389,11 +1390,20 @@ router.post('/gemini-accounts/exchange-code', authenticateAdmin, async (req, res return res.status(400).json({ error: 'Authorization code is required' }); } - // 使用固定的 localhost:45462 作为 redirect_uri - const redirectUri = 'http://localhost:45462'; - logger.info(`Using fixed redirect_uri: ${redirectUri}`); + let redirectUri = 'https://codeassist.google.com/authcode'; + let codeVerifier = null; - const tokens = await geminiAccountService.exchangeCodeForTokens(code, redirectUri); + // 如果提供了 sessionId,从 OAuth 会话中获取信息 + if (sessionId) { + const sessionData = await redis.getOAuthSession(sessionId); + if (sessionData) { + redirectUri = sessionData.redirectUri || redirectUri; + codeVerifier = sessionData.codeVerifier; + logger.info(`Using session redirect_uri: ${redirectUri}, has codeVerifier: ${!!codeVerifier}`); + } + } + + const tokens = await geminiAccountService.exchangeCodeForTokens(code, redirectUri, codeVerifier); // 清理 OAuth 会话 if (sessionId) { @@ -1731,7 +1741,7 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => { searchPatterns = [pattern]; } - logger.info(`📊 Searching patterns:`, searchPatterns); + logger.info('📊 Searching patterns:', searchPatterns); // 获取所有匹配的keys const allKeys = []; diff --git a/src/services/geminiAccountService.js b/src/services/geminiAccountService.js index 7ffe286b..07b25f48 100644 --- a/src/services/geminiAccountService.js +++ b/src/services/geminiAccountService.js @@ -77,19 +77,31 @@ function createOAuth2Client(redirectUri = null) { ); } -// 生成授权 URL +// 生成授权 URL (支持 PKCE) async function generateAuthUrl(state = null, redirectUri = null) { - const oAuth2Client = createOAuth2Client(redirectUri); + // 使用新的 redirect URI + const finalRedirectUri = redirectUri || 'https://codeassist.google.com/authcode'; + const oAuth2Client = createOAuth2Client(finalRedirectUri); + + // 生成 PKCE code verifier + const codeVerifier = await oAuth2Client.generateCodeVerifierAsync(); + const stateValue = state || crypto.randomBytes(32).toString('hex'); + const authUrl = oAuth2Client.generateAuthUrl({ + redirect_uri: finalRedirectUri, access_type: 'offline', scope: OAUTH_SCOPES, - prompt: 'select_account', - state: state || uuidv4() + code_challenge_method: 'S256', + code_challenge: codeVerifier.codeChallenge, + state: stateValue, + prompt: 'select_account' }); return { authUrl, - state: state || authUrl.split('state=')[1].split('&')[0] + state: stateValue, + codeVerifier: codeVerifier.codeVerifier, + redirectUri: finalRedirectUri }; } @@ -145,12 +157,22 @@ async function pollAuthorizationStatus(sessionId, maxAttempts = 60, interval = 2 }; } -// 交换授权码获取 tokens -async function exchangeCodeForTokens(code, redirectUri = null) { +// 交换授权码获取 tokens (支持 PKCE) +async function exchangeCodeForTokens(code, redirectUri = null, codeVerifier = null) { const oAuth2Client = createOAuth2Client(redirectUri); try { - const { tokens } = await oAuth2Client.getToken(code); + const tokenParams = { + code: code, + redirect_uri: redirectUri + }; + + // 如果提供了 codeVerifier,添加到参数中 + if (codeVerifier) { + tokenParams.codeVerifier = codeVerifier; + } + + const { tokens } = await oAuth2Client.getToken(tokenParams); // 转换为兼容格式 return { diff --git a/web/admin-spa/src/components/accounts/OAuthFlow.vue b/web/admin-spa/src/components/accounts/OAuthFlow.vue index 77a62b61..37d9c91f 100644 --- a/web/admin-spa/src/components/accounts/OAuthFlow.vue +++ b/web/admin-spa/src/components/accounts/OAuthFlow.vue @@ -213,19 +213,16 @@ 2
-

+

在浏览器中打开链接并完成授权

-
    -
  1. 点击上方的授权链接,在新页面中完成Google账号登录
  2. -
  3. 点击“登录”按钮后可能会加载很慢(这是正常的)
  4. -
  5. 如果超过1分钟还在加载,请按 F5 刷新页面
  6. -
  7. 授权完成后会跳转到 http://localhost:45462 (可能显示无法访问)
  8. -
-
-

- - 提示:如果页面一直无法跳转,可以打开浏览器开发者工具(F12),F5刷新一下授权页再点击页面的登录按钮,在“网络”标签中找到以 localhost:45462 开头的请求,复制其完整URL。 +

+ 请在新标签页中打开授权链接,登录您的 Gemini 账户并授权。 +

+
+

+ + 注意:如果您设置了代理,请确保浏览器也使用相同的代理访问授权页面。

@@ -240,31 +237,27 @@

- 复制oauth后的链接 + 输入 Authorization Code

- 复制浏览器地址栏的完整链接并粘贴到下方输入框: + 授权完成后,页面会显示一个 Authorization Code,请将其复制并粘贴到下方输入框: