Web 終極攔截技巧(全是騷操作)
來源:嗶哩嗶哩技術
攔截的價值
電腦科學領域的任何問題都可以透過增加一箇中間層來解決。—— Butler Lampson
如果系統的控制權、程式碼完全被掌控,很容易新增中間層;
現實情況我們往往無法控制系統的所有細節,所以需要使用一些 “非常規”(攔截) 手段來增加中間層。
常見的場景有
自動上報未捕獲的錯誤,進行錯誤監控
攔截網路請求(fetch、xhr)進行介面效能統計、統一錯誤碼處理、遠端 debug 介面
構造執行第三方程式碼、微應用必須的沙盒環境
攔截方法
攔截/覆寫瀏覽器 API
最常見的場景有透過攔截 console 實現錯誤上報。
const _error = console.error;console.error = (...args) => { _error.apply(console, args); console.info('在此處上報錯誤資訊...');}; // 其它程式碼列印錯誤console.error('error message');
專案中通常會基於 axios 此類的網路庫,做一些統一處理邏輯
但在某些場景,我們無法修改專案程式碼,就能透過攔截 fetch, xhr 來達到目的。
// 介面效能監控,開啟 , 在控制檯執行以下程式碼const _fetch = window.fetch;window.fetch = (...args) => { const startTime = performance.now(); return _fetch(...args).finally(() => { console.info('介面耗時:', Math.round(performance.now() - startTime), 'ms'); });}; await fetch('//example.com');
你可以選擇第三方庫(比如 xhook:)來快速實現 fetch, xhr 攔截功能。
瀏覽器中大多數 API 都是可以覆寫的,開啟腦洞,可以實現非常多的神奇功能:
網路 API (xhr, fetch, WebSocket)
效能監控、統一錯誤碼處理
新增額外 HTTP 引數(header, query)實現介面染色功能
修改 Host 將介面自動轉向代理服務,實現遠端除錯介面、Mock 資料
修改原型 (Array.prototype.at = ...)
polyfill 庫的必備手段
頁面跳轉 API (window.open, history.go back pushState)
修改跳轉的目的頁面
自動新增頁面跳轉埋點
刪除特定 API 禁用瀏覽器功能
禁止 js 訪問攝像頭 navigator.mediaDevices.getDisplayMedia = null
禁止 p2p 連線 window.RTCPeerConnection = null
事件、DOM 元素
瀏覽器也會提供一些具備攔截性質的 API,允許開發者實現特定功能。
一個 DOM 元素經常會繫結許多事件,如果你想讓特定的事件回撥函式先執行,以便新增一些前置邏輯或取消後續事件的執行;
可以瞭解 addEventListener#usecapture (opens new window)(https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#usecapture)的用法。
// 禁止響應頁面的所有點選事件(危險⚠️),第三個引數(usecapture)設為 truedocument.body.addEventListener( 'click', (evt) => { evt.preventDefault(); evt.stopPropagation(); }, true);
許多 DOM 元素都是在執行時動態建立的,如果需要修改動態建立的 DOM 元素可使用 MutationObserver(opens new window)(https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)
比如,攔截所有超連結(a 標籤),給目標連結新增 _source 引數
const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type !== 'childList' || !mutation.addedNodes) return; mutation.addedNodes.forEach((item) => { if (!item.nodeName === 'A') return; const targetUrl = new URL(item.href, location.href); targetUrl.searchParams.append('_source', 'any string'); item.href = targetUrl.href; }); }});observer.observe(document.body, { attributes: true, childList: true, subtree: true,});
TIP:MutationObserver 同樣適應於修改 iframe, img 的連結,或其它任意 DOM 元素的屬性
除錯小技巧
如果你的頁面因未知程式碼陷入了快速重新整理的死迴圈,可在專案中新增以下以下程式碼;
頁面重新整理前會進入 debug 狀態,在 devtools 中檢視呼叫堆疊(call stack)即可瞭解重新整理的原因
window.addEventListener('beforeunload', () => { debugger;});
TIP:http 302 屬於非程式碼導致頁面跳轉,上述程式碼無法攔截
當除錯第三方程式碼時,需要監聽某個不符合期望的物件屬性值
// debug 狀態下任意可訪問物件const obj = { prop: 1 }; // 在 devtools -> console 中執行以下程式碼_obj_prop = obj.prop;Object.defineProperty(obj, 'prop', { set: (v) => { _obj_prop = v; // 每次賦值都會進入 debug 狀態 debugger; }, get: () => _obj_prop,});// 試試執行 obj.prop = 2// 後續可在 console 中隨時訪問 _obj_prop 的當前值
如果需要監聽某個物件所有屬性值被讀寫的訊息,可以使用 Proxy
const obj = { prop: 1 }; const obj2 = new Proxy(obj, { get(target, key, receiver) { return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { debugger; return Reflect.set(target, key, value, receiver); },}); // 試試執行 obj2.abc = 2
注意差異
Object.defineProperty 沒有改變 obj 的引用,Proxy 生成了新物件 obj2
使用 Proxy 可以監聽物件(obj2)所有屬性的讀寫,而 Object.defineProperty 一次只能監聽一個屬性(prop)
ServiceWorker 攔截
前端可能會使用 ServiceWorker 來實現離線可用、快取資源、加速頁面訪問等功能。
// 安裝時快取資源self.addEventListener('install', (event) => { event.waitUntil( caches .open('v1') .then((cache) => cache.addAll(['/index.html', '/style.css', '/app.js'])) );});// 攔截頁面資源請求,使用快取響應(也可使用自定義內容響應請求)self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request).then((response) => { return response; }) );});
ServiceWorker 是前端頁面到伺服器之間的中間層,它能攔截同域名下的所有請求,快取或篡改請求結果,能實現的功能遠不止離線或加速訪問這麼簡單。
後文會從 WebContainer 原理分析 ServiceWorker 的高階玩法。
伺服器攔截
前面介紹的技巧都侷限在客戶端(瀏覽器)中,如果你掌握了真正的服務控制權,即配置伺服器和內網 DNS 域名等許可權(一般由公司內的工程效率團隊或運維負責),再配合前面介紹的瀏覽器攔截技巧,可玩性將大大增加。
一個 HTTP 請求往往會經過多個伺服器節點,每個節點就是一箇中間層。
1.DNS 決定 HTTP 請求由哪個閘道器(Nginx)處理
2.伺服器節點(閘道器、Service)節點能獲取、篡改 HTTP 請求中的所有資訊:Header、Cookie、Body
如何注入程式碼
前文介紹的客戶端攔截技巧都需要在瀏覽器中注入程式碼,以下列舉注入程式碼的方式,根據目的和能獲取的許可權決定採用哪種方式。
1.原始碼注入
如果你有原始碼控制權,那你可以對專案做任何事情,確保攔截程式碼優先執行即可
優點:靈活可控;缺點:通用性不好,侵入業務
2.構建、推送服務注入
工程團隊提供構建、推送服務,可編寫指令碼在構建產物中注入程式碼(比如 html 中新增 script 標籤)
優點:業務無感知,通用性好;缺點:不一定有許可權
3.閘道器注入
Nginx 向 html 中注入 script 也很簡單
優點同上,一般限於在開發、測試環境,不會上生產環境
4.瀏覽器外掛、devtools 注入
如果你啥許可權都沒有(普通使用者),又想幹一些“壞事”,則可選擇使用外掛注入程式碼,或臨時在 devtools 的 console 皮膚直接寫程式碼
優點:萬能注入、無法阻擋;缺點:只能影響當前瀏覽器,難以跨瀏覽器、裝置、使用者
常用的指令碼管理外掛:篡改猴 (opens new window)(),也可考慮自己寫瀏覽外掛
儘量讓被注入的程式碼早於業務程式碼執行,比如實現攔截並上報錯誤資訊,如果被注入的程式碼執行時機較晚,則會丟失執行前的錯誤資訊。
案例分析
WebContainer 原理
WebContainer (opens new window)是一種基於瀏覽器的執行時,可完全在瀏覽器標籤頁內執行 Node.js 應用程式和作業系統命令。
驚豔的地方有兩點:
能在瀏覽器中執行 Node 服務,居然還能啟動 DevServer “監聽埠”
離線後 IDE 開發中的頁面也能正常開發
以 (opens new window)為例
核心部分是將 Node 編譯成 WASM,然後 Mock 檔案系統、底層網路模組 使其能在瀏覽器中執行。
然後使用 Node 啟動 devServer 服務,監聽埠。
dev 頁面發起的請求被 ServerWorker 攔截對映成本地 Mock 的檔案
本地 Mock 的檔案由 IDE 原始碼使用 Node 編譯而成
原始碼的依賴包被對映成 http 請求從遠端獲取
比如安裝依賴 react-dom 對應的網路請求是:
(opens new window)
在瀏覽器中執行 DevServer 時,當然無法監聽 TCP 埠。
巧妙的地方是將埠對映成唯一的域名,透過 ServiceWorker 攔截域名下的所有請求,關聯到 Mock 的檔案系統。
詳情請看 WebContainer 原理分析(opens new window)
沙盒
沙盒(sandbox,又譯為沙箱)是一種安全機制,為執行中的程式提供隔離環境。通常是作為一些來源不可信、具破壞力或無法判定程式意圖的程式提供實驗之用。
假設你需要執行一段第三方程式碼,安全要求禁止訪問 document,以及 window 下的 open、location。
嘗試複製以下程式碼在 console 中執行
function safeExec(code) { const proxyWindow = new Proxy(window, { get(target, key, receiver) { if (['open', 'location', 'document'].includes(key)) throw new Error(`禁止訪問 key: ${key}`); if (key === 'window') return proxyWindow; return Reflect.get(target, key, receiver); }, // set() {} }); new Function('window', `with(window) { ${code} }`)(proxyWindow, null);}// Error 禁止訪問 key: opensafeExec(`window.open('//danger.com')`);
沙盒也是前端微應用的核心技術,透過 Proxy 監聽 window 的讀寫記錄,可以隔離多個微應用的執行環境,解除安裝某個微應用後也能將環境(window 物件)重置回該應用載入前的狀態。
通用域名服務
一般 Web 開發者使用會使用 localhost:8080、192.168.0.1:8080 這個訪問開發環境;
如果使用真實域名來轉發請求到開發環境(https://ff-dev.bilibili.com?_ip_=192.168.0.1&_port_=8080),能解決一些常見的問題:
輕易實現共享域名(bilibili.com)的登入態 cookie
無需配置 Web 服務的 https 證書
僅限於安全上下文的特性(https://developer.mozilla.org/zh-CN/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts)
在手機上使用 https 協議訪問開發服務,避免 http 協議導致許多 Web API 不可用
該服務是一個天然的中間層,可無感注入程式碼實現效率工具,比如:
遠端網路抓包、Mock
移動端控制檯(eruda)
遠端程式碼除錯(chii)
切換後端介面環境、介面染色
真實域名轉發請求,能解決部分場景的問題,也會帶來一些新問題需要解決:
靜態資源(/index.js)不帶引數,無法轉發到目的地,無法初始化頁面
頁面跳轉後,域名或引數(_ip_, _port_)會丟失
HMR 的 WebSocket(192.168.0.1:4000/socket.io) 連線失敗,無法自動熱更新
解決這問題確實有一些難度,好在前文已經介紹了對應的攔截技巧;
一旦解決問題部署通用域名服務後,就能給許多人提供便捷服務。
實現原理
1.載入 HTML
部署一個 Nginx 服務,併為其註冊域名(ff-dev.bilibili.com)
在 Nginx 中讀取 http 請求的 query 引數(_ip_, _port_),將請求轉發到對應目標
目標響應後,將引數也寫入到 Response 的 cookie 中
如果是 html 請求,則在 body 中注入 js
<body>...<body> 替換為<body><script> src="ff-dev-sdk.js"/>...</body>
2.載入靜態資源
靜態資源(/index.js)不帶引數,Nginx 改成從 cookie 中讀取引數,然後將請求轉發到目的地
3.解決頁面跳轉引數丟失
ff-dev-sdk.js 攔截所有 a 標籤、重寫 open 方法,在頁面跳轉的 url 新增引數(_ip_, _port_);
頁面跳轉後,新頁面也能轉發到正確的目的地
4.解決 HMR 無法熱更新
ff-dev-sdk.js 覆寫 WebSocket 的實現,將連結 192.168.0.1:4000/socket.io 替換為 ff-dev.bilibili.com/socket.io?_ip_=192.168.0.1&_port_=4000 繼續由 Nginx 轉發
以上步驟使用的技巧在前文都有介紹
注入 js 程式碼到 html 中,寫入引數(_ip_, _port_)到 cookie 中
攔截 a 標籤
覆寫 API:open,WebSocket
總結
應用中間層思路的經驗
“新增一箇中間層”是一種有效且通用的解決問題的思路
根據需要解決的問題,思考中間層的位置、以及一個能注入程式碼時機,並讓程式碼儘早執行
原始碼注入指令碼
構建、推送服務注入
閘道器注入
瀏覽器外掛、devtools 注入
Web 技術棧擁有非常大的可操作空間
利用 js 的動態特性,覆寫系統 API 實現攔截
靈活使用具有攔截性質的 API
ServiceWorker
瞭解 HTTP 請求的構成(Header、Cookie、Body),以及它流轉的節點
多種攔截技巧靈活搭配,釋放更強大的力量
能力越強(攔截範圍越大)、責任越大,注意安全
安全邊界
從前文看由於 js 靈活特性,甚至看起來有點不安全(覆寫系統 API),但恰恰相反,Web 平臺常與“安全”一起出現。
js 的動態靈活是執行在 Web 安全邊界(同源策略:https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy、內容安全策略:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP )構建的“大沙盒”內的。
新手開發者常碰到的跨域問題,就是因為碰到了安全邊界,安全邊界是 Web 平臺能力受限(相對 Native)的重要原因之一。
Web 平臺中的所有技巧策略都必須符合安全規則;
所以,建議大家在學習攔截技巧的同時,瞭解同源策略、內容安全策略(CSP) 等安全知識。
太騷了,接受不了?
從《通用域名服務》章節的原理來看,似乎有點過於“騷操作”了;
從效率角度出發,不要在乎騷不騷、髒不髒,有價值就幹。
解決“規範與效率”之間的衝突:隔離髒程式碼,不拉低系統整體程式碼質量。
附錄
addEventListener#usecapture:https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#usecapture
MutationObserver:https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
ServiceWorker:https://developer.mozilla.org/zh-CN/docs/Web/API/ServiceWorker
WebContainers:
WebContainer 原理分析:
瀏覽器的同源策略:https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
內容安全策略(CSP):https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP
來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70024420/viewspace-3006582/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- OkHttp 攔截器的一些騷操作HTTP
- web api新增攔截器WebAPI
- Android 截圖的各種騷操作Android
- 極簡架構模式-攔截過濾器模式架構模式過濾器
- Win10 Mobile如何開啟騷擾攔截和黑名單功能Win10
- 攔截器,攔截器棧總結
- OPPO K1攔截陌生騷擾電話的方法教程 OPPO K1怎麼設定來電攔截規則?
- 利用攔截器加快取完成介面防刷操作快取
- SpringMVC攔截器,設定不攔截的URLSpringMVC
- MyBatis攔截器MyBatis
- Mybatis 攔截器MyBatis
- 導彈攔截
- sql攔截器SQL
- 前端架構之vue+axios 前端實現登入攔截(路由攔截、http攔截)前端架構VueiOS路由HTTP
- vue中用axios攔截器攔截請求和響應VueiOS
- Spring MVC 中的攔截器的使用“攔截器基本配置” 和 “攔截器高階配置”SpringMVC
- 純前端生成Excel檔案騷操作——WebAssembly & web workers前端ExcelWeb
- Git騷操作Git
- JavaScript 騷操作JavaScript
- mysql騷操作MySql
- win10 microsoft edge網址被攔截如何取消攔截Win10ROS
- axios攔截器iOS
- Mybatis Interceptor 攔截器MyBatis
- Xposed攔截抽象方法抽象
- WKCrashSDK - crash攔截工具
- axios 攔截器iOS
- spring攔截器Spring
- Java interceptor 攔截器Java
- IOS 手勢攔截iOS
- SpringMVC攔截器SpringMVC
- WEB安全是什麼Web
- java web 過濾器跟攔截器的區別和使用JavaWeb過濾器
- 終極面試技巧——催眠對話和反面試薦面試
- Flume內建攔截器與自定義攔截器(程式碼實戰)
- opacity騷操作
- SpringMVC-攔截器SpringMVC
- 攔截過濾器模式過濾器模式
- gRPC(3):攔截器RPC