揭秘!如何用油猴脚本破解无界微前端请求拦截难题

2 阅读6分钟

当油猴脚本遇上微前端,你的请求拦截器还管用吗?

引言

在信息安全领域,垂直越权是一个常见但危害巨大的漏洞。当普通用户能够访问本应只有管理员才能操作的接口时,系统的安全防线就出现了缺口。为了解决这个问题,安全团队要求我们将页面上的每个按钮/菜单与后端接口权限进行强制绑定——这意味着我们需要清晰地知道:点击每个按钮,究竟触发了哪些接口请求?

这个需求看似简单,实际操作却困难重重。以我们最近的一个项目为例:

  • 一个"新增用户"按钮,可能触发3个接口
  • 一个"导出报表"按钮,可能触发5个异步请求
  • 一个复杂的表单提交,可能包含10+个接口调用

更棘手的是,这些按钮分布在不同的菜单下,接口可能来自不同的微服务,路径还带有各种动态参数。人工分析?效率太低。借助浏览器开发者工具?操作繁琐,难以批量导出。

油猴脚本配合请求拦截,似乎是个完美的解决方案——自动捕获按钮点击后的所有请求,一键导出接口列表,效率提升百倍。

然而,当我们满怀信心地部署脚本后,却发现了一个令人崩溃的事实:页面采用了无界微前端架构,子应用的所有请求都"隐身"了!

原来,无界微前端为了实现应用隔离,将子应用运行在独立的 iframe 沙箱中。子应用的请求根本不在主窗口的 window 对象上发起,我们精心编写的请求拦截代码,就像对着空气挥拳——完全无效。

这是为什么?又该如何解决?本文将带你深入理解微前端环境下的请求拦截机制,并提供一个完整的解决方案。

一、问题的本质:为什么拦截不到子应用的请求?

1.1 传统请求拦截的原理

在普通页面中,我们通过重写 window.fetchXMLHttpRequest 来拦截请求:

// 简单的请求拦截示例
const originalFetch = window.fetch;
window.fetch = function(...args) {
    console.log('捕获到请求:', args[0]);
    return originalFetch.apply(this, args);
};

这种方式之所以有效,是因为所有请求都通过全局的 window 对象发起。

1.2 无界微前端的隔离机制

无界微前端为了实现应用隔离,采用了iframe + Web Worker的架构:

  • iframe 隔离:子应用运行在独立的 iframe 中,拥有自己的 window 对象
  • JS 沙箱:通过 Web Worker 或 iframe 实现 JavaScript 执行环境的隔离
  • 通信机制:主应用和子应用通过 postMessage 进行通信

这就导致了一个关键问题:子应用的请求是在 iframe 内部发起的,不在主窗口的 window 对象上,因此主窗口的请求拦截代码无法捕获到子应用的请求。

二、破解之道:多维度拦截策略

2.1 核心思路

要解决这个问题,我们需要采用组合拳策略:

  1. 主窗口拦截:捕获主应用的请求
  2. iframe 注入:向子应用 iframe 注入拦截代码
  3. 消息通信:通过 postMessage 将子应用请求传回主窗口
  4. 动态监听:实时监控新创建的 iframe

2.2 关键技术实现

2.2.1 MutationObserver 监听 iframe 创建

function observeIframes() {
    // 监听新添加的 iframe
    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            mutation.addedNodes.forEach((node) => {
                if (node.nodeName === 'IFRAME') {
                    injectIntoIframe(node);
                }
            });
        });
    });
    
    observer.observe(document.documentElement, {
        childList: true,
        subtree: true
    });
}

2.2.2 iframe 内部请求拦截

function injectIntoIframe(iframe) {
    try {
        const iframeWindow = iframe.contentWindow;
        
        // 拦截 iframe 中的 fetch
        const originalFetch = iframeWindow.fetch;
        iframeWindow.fetch = function(...args) {
            const [url, options = {}] = args;
            
            return originalFetch.apply(this, args).then(response => {
                // 通过 postMessage 将请求信息发送给主窗口
                window.postMessage({
                    type: 'REQUEST_CAPTURED',
                    data: {
                        url: url,
                        method: options.method || 'GET',
                        status: response.status
                    }
                }, '*');
                return response;
            });
        };
        
        // 拦截 iframe 中的 XMLHttpRequest
        const originalXHR = iframeWindow.XMLHttpRequest;
        // ... 类似的拦截逻辑
    } catch (e) {
        console.warn('iframe 注入失败(可能是跨域限制):', e);
    }
}

2.2.3 主窗口接收消息

window.addEventListener('message', (event) => {
    if (event.data && event.data.type === 'REQUEST_CAPTURED') {
        const request = event.data.data;
        console.log('捕获到子应用请求:', request);
        // 处理捕获到的请求
    }
});

2.3 进阶优化:避免重复注入

const injectedIframes = new WeakSet();

function injectIntoIframe(iframe) {
    // 避免重复注入
    if (injectedIframes.has(iframe)) return;
    
    // 等待 iframe 加载完成
    if (iframe.contentWindow?.document?.readyState === 'complete') {
        performInjection(iframe);
    } else {
        iframe.addEventListener('load', () => performInjection(iframe));
    }
}

三、实战案例:权限映射采集助手

基于以上原理,我们开发了一个完整的 权限映射采集助手 油猴脚本。这个脚本不仅解决了微前端请求拦截的问题,还提供了丰富的实用功能。

3.1 核心功能

  1. 一键采集:点击页面按钮,自动捕获后续触发的所有接口
  2. 智能分组:按点击事件自动分组,清晰展示按钮与接口的关联
  3. 多前缀支持:可配置多个服务前缀,自动从URL中移除
  4. 白名单过滤:支持字符串和正则表达式的白名单规则
  5. 微前端兼容:完美支持无界微前端子应用的请求捕获

3.2 配置管理

const ConfigManager = {
    // 默认配置
    defaults: {
        captureDuration: 3000,
        servicePrefixes: ['/api/v1', '/app'],
        whitelist: [
            '/health',
            '/metrics',
            /\/api\/v\d+\/heartbeat/
        ]
    },
    
    // 解析多行输入(支持正则)
    parseRulesInput(text) {
        return text.split('\n')
            .map(line => line.trim())
            .filter(line => line)
            .map(line => {
                if (line.startsWith('/') && line.lastIndexOf('/') > 0) {
                    const lastSlash = line.lastIndexOf('/');
                    const pattern = line.slice(1, lastSlash);
                    const flags = line.slice(lastSlash + 1);
                    return new RegExp(pattern, flags);
                }
                return line;
            });
    }
};

3.3 智能URL格式化

function formatURL(url) {
    try {
        const urlObj = new URL(url, location.href);
        let pathname = urlObj.pathname;
        
        // 移除所有匹配的服务前缀
        CONFIG.servicePrefixes.forEach(prefix => {
            if (typeof prefix === 'string') {
                if (pathname.startsWith(prefix)) {
                    pathname = pathname.slice(prefix.length);
                }
            } else if (prefix instanceof RegExp) {
                pathname = pathname.replace(prefix, '');
            }
        });
        
        return pathname;
    } catch(e) {
        return url;
    }
}

四、最佳实践与注意事项

4.1 跨域问题处理

当 iframe 与主页面不同源时,无法直接访问 contentWindow。解决方案:

  1. try-catch 包裹:优雅降级
  2. postMessage 通信:作为备选方案
  3. Service Worker:更底层的拦截方式

4.2 性能优化建议

  1. 使用 WeakSet 记录注入状态:避免重复注入
  2. 延迟注入:等待 iframe 完全加载
  3. 事件节流:避免频繁的 UI 更新

4.3 调试技巧

// 添加调试日志
function addRecord(method, url, status, source) {
    console.debug(`[${source}] ${method} ${url} [${status}]`);
    // ... 其他逻辑
}

// 查看已注入的 iframe
console.log('已注入的 iframe 数量:', injectedIframes.size);

五、结语

微前端架构给前端开发带来了诸多便利,但也给开发者工具带来了新的挑战。通过深入理解微前端的隔离机制,并采用多维度拦截策略,我们成功解决了油猴脚本无法捕获子应用请求的问题。

本文提供的解决方案不仅适用于无界微前端,对于 Qiankun、Single-spa 等其他微前端框架也同样适用。核心思路都是:识别隔离环境、注入拦截代码、建立通信机制

希望这篇文章能帮助你更好地理解微前端环境下的请求拦截机制,并在实际工作中派上用场。如果你有更好的解决方案,欢迎在评论区分享讨论!


扩展阅读: