You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 05:44:52