VS Code TypeScript扩展无法捕获登录回调URL中的安全令牌
VS Code扩展开发:TypeScript中无法捕获Webview导航事件获取令牌问题
问题描述
我正在学习TypeScript,并用它开发VS Code扩展。需求是:扩展初始化时打开Webview面板指向公司登录页,用户输入有效凭据后页面跳转到回调页,URL中会临时包含令牌信息。用C#开发VS扩展时,我通过NavigationComplete事件处理器检查URL中的特定字符串来解析所需信息,但在TypeScript里,事件处理器无法捕获事件,导致拿不到令牌信息,找不到问题出在哪,求帮忙。
关键文件核心代码
extension.ts
// Import the module and reference it with the alias vscode in your code below import * as vscode from 'vscode'; import { showLoginWindow } from './helpers/showLoginWindow'; import { baseWebUrl } from './helpers/TokenUtils'; export async function activate(context: vscode.ExtensionContext) { console.log('Congratulations, your extension "xyz" is now active!'); const targetUrl = loginUrl; // 注意:loginUrl未定义,需替换为实际登录地址 try { const tokeninfo = await showLoginWindow(targetUrl); // 此处漏传context参数 } catch (error) { console.error(error); } } export function deactivate() {}
showLoginWindow.ts
import * as vscode from 'vscode'; import { TokenResponse } from '../classes/tokenResponse'; import { baseWebUrl } from '../helpers/TokenUtils'; import path from 'path'; export function showLoginWindow(context: vscode.ExtensionContext, targetUrl: string): Promise<TokenResponse> { return new Promise((resolve, reject) => { const panel = vscode.window.createWebviewPanel( 'loginWindow', 'Website Login', vscode.ViewColumn.One, { enableScripts: true, localResourceRoots: [vscode.Uri.file(path.join(context.extensionPath, 'src/scripts'))] } ); panel.webview.html = getWebviewContent(targetUrl); // Handle messages from the webview panel.webview.onDidReceiveMessage( message => { console.log('Received message from webview:', message); switch (message.command) { case 'urlChanged': const currentUrl = message.url; if (currentUrl.includes("extension-callback")) { const splitChar = currentUrl.includes("#") ? '#' : '?'; const fragment = currentUrl.split(splitChar).pop(); if (fragment) { const queryParams = new URLSearchParams(fragment); const accessToken = queryParams.get("access_token") || ''; const expiresAt = queryParams.get("expires_at") || '0'; const expiresIn = queryParams.get("expires_in") || '0'; const providerToken = queryParams.get("provider_token") || ''; const refreshToken = queryParams.get("refresh_token") || ''; const tokenType = queryParams.get("token_type") || ''; const expiryAt = parseInt(expiresAt, 10); const expirySeconds = parseInt(expiresIn, 10); const newTokenResponse = new TokenResponse( accessToken, expiryAt, expirySeconds, refreshToken, tokenType ); resolve(newTokenResponse); panel.dispose(); } } break; } }, undefined, context.subscriptions ); }); } function getWebviewContent(targetUrl: string): string { return ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Login</title> </head> <body> <iframe id="loginFrame" src="${targetUrl}" width="100%" height="800px"></iframe> <script> const vscode = acquireVsCodeApi(); let lastUrl = ''; document.getElementById('loginFrame').onload = function() { const iframe = document.getElementById('loginFrame'); setInterval(() => { try { const currentUrl = iframe.contentWindow.location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; console.log('Sending message to VS Code extension'); vscode.postMessage({ command: 'urlChanged', url: currentUrl }); } } catch (e) { console.error('Error accessing iframe content:', e); } }, 1000); }; </script> </body> </html> `; } function handleLoginSuccess(tokenResponse: TokenResponse) { vscode.window.showInformationMessage('Login succeeded!'); console.log('Token Response:', tokenResponse); }
问题排查与修复方案
1. 修复参数传递错误
extension.ts中调用showLoginWindow时漏传了context参数,这会直接导致函数执行失败,修正如下:
// extension.ts中修正调用 const tokeninfo = await showLoginWindow(context, targetUrl);
2. 解决iframe跨域无法获取URL的核心问题
浏览器同源策略限制了跨域iframe的contentWindow.location访问,这是你拿不到回调URL的根本原因,提供两种可行方案:
方案一:直接用Webview加载登录页,监听原生导航事件
去掉iframe,让Webview直接加载登录URL,利用VS Code Webview的onDidNavigate事件捕获导航变化,完全避开跨域问题:
// showLoginWindow.ts中修改面板逻辑 export function showLoginWindow(context: vscode.ExtensionContext, targetUrl: string): Promise<TokenResponse> { return new Promise((resolve, reject) => { const panel = vscode.window.createWebviewPanel( 'loginWindow', 'Website Login', vscode.ViewColumn.One, { enableScripts: true, // 允许加载外部URL localResourceRoots: [] } ); // 直接跳转到登录页 panel.webview.html = `<meta http-equiv="refresh" content="0; url=${targetUrl}">`; // 监听Webview导航事件 panel.webview.onDidNavigate(() => { const currentUrl = panel.webview.url; if (currentUrl.includes("extension-callback")) { // 复用原有的令牌解析逻辑 const splitChar = currentUrl.includes("#") ? '#' : '?'; const fragment = currentUrl.split(splitChar).pop(); if (fragment) { const queryParams = new URLSearchParams(fragment); const accessToken = queryParams.get("access_token") || ''; const expiresAt = queryParams.get("expires_at") || '0'; const expiresIn = queryParams.get("expires_in") || '0'; const refreshToken = queryParams.get("refresh_token") || ''; const tokenType = queryParams.get("token_type") || ''; const expiryAt = parseInt(expiresAt, 10); const expirySeconds = parseInt(expiresIn, 10); const newTokenResponse = new TokenResponse( accessToken, expiryAt, expirySeconds, refreshToken, tokenType ); resolve(newTokenResponse); panel.dispose(); } } }, undefined, context.subscriptions); // 用户关闭面板时拒绝Promise panel.onDidDispose(() => { reject(new Error('Login panel closed by user')); }, undefined, context.subscriptions); }); }
方案二:让回调页主动发送令牌信息
如果可以修改公司的回调页,在回调页中添加主动向Webview发送消息的脚本,避开跨域限制:
// 回调页中添加脚本 if (window.parent !== window) { // 提取令牌参数(hash或search) const tokenParams = window.location.hash.slice(1) || window.location.search.slice(1); window.parent.postMessage({ command: 'loginSuccess', params: tokenParams }, '*'); // 建议限定为Webview的源,提升安全性 }
然后修改Webview的脚本,监听回调页的消息并转发给扩展:
// 修改getWebviewContent中的脚本 <script> const vscode = acquireVsCodeApi(); window.addEventListener('message', (event) => { if (event.data.command === 'loginSuccess') { // 构造包含令牌的回调URL格式,复用原解析逻辑 vscode.postMessage({ command: 'urlChanged', url: `extension-callback?${event.data.params}` }); } }); </script>
3. 修正其他小问题
- extension.ts中
loginUrl未定义,需替换为实际的登录地址(比如baseWebUrl + '/login'); - showLoginWindow.ts中
handleLoginSuccess函数未被使用,可删除或在resolve后调用。
内容的提问来源于stack exchange,提问作者Ken B
相关产品推荐
相关产品推荐

