当油猴脚本遇上微前端,你的请求拦截器还管用吗?
引言
在信息安全领域,垂直越权是一个常见但危害巨大的漏洞。当普通用户能够访问本应只有管理员才能操作的接口时,系统的安全防线就出现了缺口。为了解决这个问题,安全团队要求我们将页面上的每个按钮/菜单与后端接口权限进行强制绑定——这意味着我们需要清晰地知道:点击每个按钮,究竟触发了哪些接口请求?
这个需求看似简单,实际操作却困难重重。以我们最近的一个项目为例:
- 一个"新增用户"按钮,可能触发3个接口
- 一个"导出报表"按钮,可能触发5个异步请求
- 一个复杂的表单提交,可能包含10+个接口调用
更棘手的是,这些按钮分布在不同的菜单下,接口可能来自不同的微服务,路径还带有各种动态参数。人工分析?效率太低。借助浏览器开发者工具?操作繁琐,难以批量导出。
油猴脚本配合请求拦截,似乎是个完美的解决方案——自动捕获按钮点击后的所有请求,一键导出接口列表,效率提升百倍。
然而,当我们满怀信心地部署脚本后,却发现了一个令人崩溃的事实:页面采用了无界微前端架构,子应用的所有请求都"隐身"了!
原来,无界微前端为了实现应用隔离,将子应用运行在独立的 iframe 沙箱中。子应用的请求根本不在主窗口的 window 对象上发起,我们精心编写的请求拦截代码,就像对着空气挥拳——完全无效。
这是为什么?又该如何解决?本文将带你深入理解微前端环境下的请求拦截机制,并提供一个完整的解决方案。
一、问题的本质:为什么拦截不到子应用的请求?
1.1 传统请求拦截的原理
在普通页面中,我们通过重写 window.fetch 和 XMLHttpRequest 来拦截请求:
// 简单的请求拦截示例
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 核心思路
要解决这个问题,我们需要采用组合拳策略:
- 主窗口拦截:捕获主应用的请求
- iframe 注入:向子应用 iframe 注入拦截代码
- 消息通信:通过
postMessage将子应用请求传回主窗口 - 动态监听:实时监控新创建的 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 核心功能
- 一键采集:点击页面按钮,自动捕获后续触发的所有接口
- 智能分组:按点击事件自动分组,清晰展示按钮与接口的关联
- 多前缀支持:可配置多个服务前缀,自动从URL中移除
- 白名单过滤:支持字符串和正则表达式的白名单规则
- 微前端兼容:完美支持无界微前端子应用的请求捕获
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。解决方案:
- try-catch 包裹:优雅降级
- postMessage 通信:作为备选方案
- Service Worker:更底层的拦截方式
4.2 性能优化建议
- 使用 WeakSet 记录注入状态:避免重复注入
- 延迟注入:等待 iframe 完全加载
- 事件节流:避免频繁的 UI 更新
4.3 调试技巧
// 添加调试日志
function addRecord(method, url, status, source) {
console.debug(`[${source}] ${method} ${url} [${status}]`);
// ... 其他逻辑
}
// 查看已注入的 iframe
console.log('已注入的 iframe 数量:', injectedIframes.size);
五、结语
微前端架构给前端开发带来了诸多便利,但也给开发者工具带来了新的挑战。通过深入理解微前端的隔离机制,并采用多维度拦截策略,我们成功解决了油猴脚本无法捕获子应用请求的问题。
本文提供的解决方案不仅适用于无界微前端,对于 Qiankun、Single-spa 等其他微前端框架也同样适用。核心思路都是:识别隔离环境、注入拦截代码、建立通信机制。
希望这篇文章能帮助你更好地理解微前端环境下的请求拦截机制,并在实际工作中派上用场。如果你有更好的解决方案,欢迎在评论区分享讨论!
扩展阅读: