在當今SPA應用流行的情況下,頁面上的所有東西都是通過javascript進行載入,本文將帶你一步一步截獲使用者請求,並修改請求地址。
我們主要使用的方法為Hook原生介面進行介面呼叫攔截;在攔截前,先定義一個URL修改的函式,統一將URL請求中的before修改為after,你在你的實際處理中可能會更加複雜。
function srcHook(url) {
let nUrl = url.replace("hook-before", "hook-after");
return nUrl;
}
Ajax請求
在前端中,一般是通過Ajax向後臺請求資料,所以首要需要攔截的就是Ajax的請求。
先來看一下如何發出一個Ajax請求:
var xhr = new XMLHttpRequest();
xhr.timeout = 3000;
xhr.ontimeout = function (event) {
alert("請求超時!");
}
xhr.open('GET', '/data/hook-before.txt');
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
resolve(xhr.responseText);
WriteLogs("====響應 " + xhr.responseText);
}
}
可以看到,傳入URL引數的方法是xhr.open
,所以我們重寫XMLHttpRequest
的open
方法進行攔截。重寫前,需要先儲存一下原生方法。
好了,現在開始正式Hook了:
var $open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function () {
if (srcHook) {
var src = srcHook(arguments[1]);
if (src === false) return;
if (src) {
arguments[1] = src;
}
}
return $open.apply(this, arguments);
}
是的,就是這麼簡單,在重寫的open
方法中,對URL引數進行修改,然後呼叫原生方法。
通過我們的日誌資訊,可以看到,訪問修改已經成功:
沒問題,Hook成功。但是,你看下面,還有一個fetch
型別的請求沒有Hook到,彆著急,馬上處理它。
仍然首先來看一下fetch
的呼叫方法:
fetch("/data/hook-before.txt")
.then(function(response){
return response.text();
}).then(function(text){
alert(text);
})
fetch
是一個全域性函式,第一個引數為需要請求的網址。我們只需要重寫window
物件上的fetch
函式即可。
var $fetch = window.fetch;
window.fetch = function () {
if (srcHook) {
var src = srcHook(arguments[0]);
if (src === false) return;
if (src) {
arguments[0] = src;
}
}
return $fetch.apply(window, arguments);
}
這下沒問題了,兩種請求方式都攔截了。
DOM請求
對於正常的Ajax請求,我們已經進行了處理,但在有些情況下,會在頁面中使用JSONP
來進行跨域請求。
我們仍然是先來看一下JSONP
的實現:
var url="/data/hook-before.js";
var script = document.createElement('script');
script.setAttribute('src', url);
document.getElementsByTagName('head')[0].appendChild(script);
可以看出,JSONP的本質是向DOM中插入一個SCRIPT的Element。從程式碼中,我輕鬆的找到的Hook點,Element例項的setAttribute
方法。
var $setAttribute = Element.prototype.setAttribute;
Element.prototype.setAttribute = function () {
if (this.tagName=="SCRIPT"&&arguments[0]=="src"&&srcHook) {
var src = srcHook(arguments[1]);
if (src === false) return;
if (src) {
arguments[1] = src;
}
}
return $setAttribute.apply(this, arguments);
}
和xhr的hook完全一樣。
通過同樣的方法,也可以把
img
、link
、iframe
、a
給hook掉。
然而,上面的hook好像也差了點啥。請看下面的程式碼:
var url="/data/hook-before.js";
var script = document.createElement('script');
script.src=url;
document.getElementsByTagName('head')[0].appendChild(script);
沒錯,不呼叫setAttribute
方法一樣可以設定src
。
先看一看src
在原型鏈上的定義:
{get: ƒ, set: ƒ, enumerable: true, configurable: true}
通過定義可以知道src
的屬性描述符(property descriptor)就可以重寫的,這下好辦了,我們重寫一下src
的setter
。
var descriptor=Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, "src");
var setter=descriptor["set"];
descriptor["set"]=function(value){
if (srcHook) {
var src = srcHook(arguments[0]);
if (src === false) return;
if (src) {
arguments[0] = src;
}
}
return setter.apply(this, arguments);
}
descriptor["configurable"]=false;
//由於src的set有可能會被其它指令碼修改回去,此處通過設定configurable=false來強行禁止修改
Object.defineProperty(HTMLScriptElement.prototype, "src", descriptor);
通過同樣的方法,也可以把
img
、link
、iframe
、a
,style
中和URL相關的屬性處理掉。
提示:innerHTML
也是通過這種方法進行處理。
CSS中的請求
要發起一個請求,除了上面描述的方法外,也可以通過css中的background-image
屬性發起。
document.getElementById("#id").style.background="url(/data/hook-before.jpg)";
CSS屬性屬於CSSStyleDeclaration
物件,該物件的原型上有以下屬性可以發起請求:
- cssText
- background-image
- background
- border-image
- borderImage
- border-image-source
- borderImageSource
使用程式碼示例中的方法設定CSS屬性,會直接發起請求,我們無法攔截。但是,我們可以通過呼叫CSSStyleDeclaration
的setProperty
方法進行屬性設定,所以我們需要在CSSStyleDeclaration
的原型鏈上定義上面的屬性,通過設定setter
和getter
,然後呼叫setProperty
方法進行實際設定。程式碼示例如下:
Object.defineProperty(CSSStyleDeclaration.prototype, "background",
{
get: function () {
return this.getPropertyValue("background");
},
set: function (v) {
v=srcHook(v);
this.setProperty("background", v);
}
}
);
Object.defineProperty(CSSStyleDeclaration.prototype, "background-image",
{
get: function () {
return this.getPropertyValue("background-image");
},
set: function (v) {
v=srcHook(v);
this.setProperty("background-image", v);
}
}
);
var descriptor = Object.getOwnPropertyDescriptor(CSSStyleDeclaration.prototype, "setProperty");
var valuer = descriptor["value"];
descriptor["value"] = function () {
if (srcHook) {
var src = srcHook(arguments[1]);
if (src === false) return;
if (src) {
arguments[1] = src;
}
}
return valuer.apply(this, arguments);
}
descriptor["configurable"] = false;
//由於src的set有可能會被其它指令碼修改回去,此處通過設定configurable=false來強行禁止修改
Object.defineProperty(CSSStyleDeclaration.prototype, "setProperty", descriptor);
由於在對
background-image
,background
等屬性進行hook時,呼叫了setProperty
方法進行設定,若原始碼中直接就呼叫的setProperty
方法進行設定,則需要對setProperty
的屬性描述符(property descriptor)進行重寫。
HTML中的請求
HTML中的請求,我們無法進行攔截,但可以使用MutationObserver
監聽DOM
物件的建立,對於其中的a
標籤,可以修改href
屬性。對於img
的src
屬性也可以修改,但無法阻止請求的發出,修改後的請求也會正常發出。
我們先在HTML中新增一個圖片顯示的DOM
<img src="/data/hook-before.jpg" />
在沒有監聽和修改前,頁面顯示的是HOOK前的圖片,如下:
然後,我們在JS中新增監聽和修改的程式碼,我們僅用IMG進行測試:
function DomWatch() {
// part 1
var observer = new MutationObserver(function(mutationsList, mutationObserver){
mutationsList.forEach(function(mutation){
if(!mutation.addedNodes) return;
mutation.addedNodes.forEach(function(node){
if(node.tagName!=="IMG") return;
node.src=srcHook(node.src);
})
})
});
// part 2
observer.observe(document, {childList:true,attributes:true,subtree:true});
}
DomWatch();
儲存,然後重新整理一下頁面,可以發現顯示的圖片已經發生了改變。
在這裡,雖然我們看到的圖片已經發生了變化,但實際是在HTML中指定的圖片依然會發出請求。
在Developer Tools的網路標籤中,可以看到,發出了兩次圖片請求。
關於MutationObserver
的具體用法,請可以參考
- https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
- https://www.zhangxinxu.com/wordpress/2019/08/js-dom-mutation-observer/
在HTML中的DOM,也可以通過遍歷的方式進行修改,但是如果用
innerHTML
建立的DOM,處理上就會比較麻煩。
WebSocket中的請求
WebSocket中的請求是在new
的時候指定的,如下:
new WebSocket("ws://121.40.165.18:8800")
我們需要攔截WebSocket的new
操作,並將連線地址修改為我們需要的地址,對於new
的攔截,這裡使用ES6的Proxy
進行處理。在這裡,我們統一將地址修改為ws://119.29.3.36:6700/
。
const __WebSocket = new Proxy(window.WebSocket, {
construct(target, args) {
args[0]="ws://119.29.3.36:6700/";
return new target(...args);
}
});
window.WebSocket = __WebSocket;
未解決的問題
如果在網頁的指令碼中,有通過location
或location.href
來重定向頁面地址,則無法對這個動作進行攔截,location
物件已經被瀏覽器定義為了不可偽造,目前沒有找到好的辦法,只能通過服務端代理,將呼叫該屬性的js程式碼進行替換。
通過屬性描述可以看到,window
上的location
和location.href
均設定了不可修改。
{enumerable: true, configurable: false, get: ƒ, set: ƒ}
寫在最後
本文是以前在將淘寶手機頁面搬進微信裡顯示的時候的研究結果,但後來這個也沒有用起來,現在基於互聯互通的政策要求,也就更沒有使用場景了。
本文的相關程式碼已上傳: