mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
feat: 更新 Gemini OAuth 流程支持新的授权方式
- 使用 codeassist.google.com 作为新的回调地址 - 实现 PKCE 认证流程增强安全性 - 更新前端授权流程指引 - 简化授权码输入流程 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1327,19 +1327,20 @@ router.post('/gemini-accounts/generate-auth-url', authenticateAdmin, async (req,
|
|||||||
try {
|
try {
|
||||||
const { state } = req.body;
|
const { state } = req.body;
|
||||||
|
|
||||||
// 使用固定的 localhost:45462 作为回调地址
|
// 使用新的 codeassist.google.com 回调地址
|
||||||
const redirectUri = 'http://localhost:45462';
|
const redirectUri = 'https://codeassist.google.com/authcode';
|
||||||
|
|
||||||
logger.info(`Generating Gemini OAuth URL with redirect_uri: ${redirectUri}`);
|
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;
|
const sessionId = authState;
|
||||||
await redis.setOAuthSession(sessionId, {
|
await redis.setOAuthSession(sessionId, {
|
||||||
state: authState,
|
state: authState,
|
||||||
type: 'gemini',
|
type: 'gemini',
|
||||||
redirectUri: redirectUri, // 保存固定的 redirect_uri 用于 token 交换
|
redirectUri: finalRedirectUri,
|
||||||
|
codeVerifier: codeVerifier, // 保存 PKCE code verifier
|
||||||
createdAt: new Date().toISOString()
|
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' });
|
return res.status(400).json({ error: 'Authorization code is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用固定的 localhost:45462 作为 redirect_uri
|
let redirectUri = 'https://codeassist.google.com/authcode';
|
||||||
const redirectUri = 'http://localhost:45462';
|
let codeVerifier = null;
|
||||||
logger.info(`Using fixed redirect_uri: ${redirectUri}`);
|
|
||||||
|
|
||||||
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 会话
|
// 清理 OAuth 会话
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
@@ -1731,7 +1741,7 @@ router.get('/model-stats', authenticateAdmin, async (req, res) => {
|
|||||||
searchPatterns = [pattern];
|
searchPatterns = [pattern];
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`📊 Searching patterns:`, searchPatterns);
|
logger.info('📊 Searching patterns:', searchPatterns);
|
||||||
|
|
||||||
// 获取所有匹配的keys
|
// 获取所有匹配的keys
|
||||||
const allKeys = [];
|
const allKeys = [];
|
||||||
|
|||||||
@@ -77,19 +77,31 @@ function createOAuth2Client(redirectUri = null) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成授权 URL
|
// 生成授权 URL (支持 PKCE)
|
||||||
async function generateAuthUrl(state = null, redirectUri = null) {
|
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({
|
const authUrl = oAuth2Client.generateAuthUrl({
|
||||||
|
redirect_uri: finalRedirectUri,
|
||||||
access_type: 'offline',
|
access_type: 'offline',
|
||||||
scope: OAUTH_SCOPES,
|
scope: OAUTH_SCOPES,
|
||||||
prompt: 'select_account',
|
code_challenge_method: 'S256',
|
||||||
state: state || uuidv4()
|
code_challenge: codeVerifier.codeChallenge,
|
||||||
|
state: stateValue,
|
||||||
|
prompt: 'select_account'
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authUrl,
|
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
|
// 交换授权码获取 tokens (支持 PKCE)
|
||||||
async function exchangeCodeForTokens(code, redirectUri = null) {
|
async function exchangeCodeForTokens(code, redirectUri = null, codeVerifier = null) {
|
||||||
const oAuth2Client = createOAuth2Client(redirectUri);
|
const oAuth2Client = createOAuth2Client(redirectUri);
|
||||||
|
|
||||||
try {
|
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 {
|
return {
|
||||||
|
|||||||
@@ -213,19 +213,16 @@
|
|||||||
2
|
2
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="font-medium text-green-900 mb-2">
|
<p class="font-medium text-blue-900 mb-2">
|
||||||
在浏览器中打开链接并完成授权
|
在浏览器中打开链接并完成授权
|
||||||
</p>
|
</p>
|
||||||
<ol class="text-sm text-green-800 space-y-1 list-decimal list-inside mb-3">
|
<p class="text-sm text-blue-700 mb-2">
|
||||||
<li>点击上方的授权链接,在新页面中完成Google账号登录</li>
|
请在新标签页中打开授权链接,登录您的 Gemini 账户并授权。
|
||||||
<li>点击“登录”按钮后可能会加载很慢(这是正常的)</li>
|
</p>
|
||||||
<li>如果超过1分钟还在加载,请按 F5 刷新页面</li>
|
<div class="bg-yellow-50 p-3 rounded border border-yellow-300">
|
||||||
<li>授权完成后会跳转到 http://localhost:45462 (可能显示无法访问)</li>
|
<p class="text-xs text-yellow-800">
|
||||||
</ol>
|
<i class="fas fa-exclamation-triangle mr-1" />
|
||||||
<div class="bg-green-100 p-3 rounded border border-green-300">
|
<strong>注意:</strong>如果您设置了代理,请确保浏览器也使用相同的代理访问授权页面。
|
||||||
<p class="text-xs text-green-700">
|
|
||||||
<i class="fas fa-lightbulb mr-1" />
|
|
||||||
<strong>提示:</strong>如果页面一直无法跳转,可以打开浏览器开发者工具(F12),F5刷新一下授权页再点击页面的登录按钮,在“网络”标签中找到以 localhost:45462 开头的请求,复制其完整URL。
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -240,31 +237,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="font-medium text-green-900 mb-2">
|
<p class="font-medium text-green-900 mb-2">
|
||||||
复制oauth后的链接
|
输入 Authorization Code
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-green-700 mb-3">
|
<p class="text-sm text-green-700 mb-3">
|
||||||
复制浏览器地址栏的完整链接并粘贴到下方输入框:
|
授权完成后,页面会显示一个 Authorization Code,请将其复制并粘贴到下方输入框:
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
<label class="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
<i class="fas fa-key text-green-500 mr-2" />复制oauth后的链接
|
<i class="fas fa-key text-green-500 mr-2" />Authorization Code
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="authCode"
|
v-model="authCode"
|
||||||
rows="3"
|
rows="3"
|
||||||
class="form-input w-full resize-none font-mono text-sm"
|
class="form-input w-full resize-none font-mono text-sm"
|
||||||
placeholder="粘贴以 http://localhost:45462 开头的完整链接..."
|
placeholder="粘贴从Gemini页面获取的Authorization Code..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 space-y-1">
|
<div class="mt-2 space-y-1">
|
||||||
<p class="text-xs text-gray-600">
|
<p class="text-xs text-gray-600">
|
||||||
<i class="fas fa-check-circle text-green-500 mr-1" />
|
<i class="fas fa-check-circle text-green-500 mr-1" />
|
||||||
支持粘贴完整链接,系统会自动提取授权码
|
请粘贴从Gemini页面复制的Authorization Code
|
||||||
</p>
|
|
||||||
<p class="text-xs text-gray-600">
|
|
||||||
<i class="fas fa-check-circle text-green-500 mr-1" />
|
|
||||||
也可以直接粘贴授权码(code参数的值)
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user