你以为你请求的就是你想请求的吗?

在当今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,所以我们重写XMLHttpRequestopen方法进行拦截。重写前,需要先保存一下原生方法。

好了,现在开始正式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参数进行修改,然后调用原生方法。

通过我们的日志信息,可以看到,访问修改已经成功:

image-20211103164438126

没问题,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完全一样。

通过同样的方法,也可以把imglinkiframea给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)就可以重写的,这下好办了,我们重写一下srcsetter

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);

通过同样的方法,也可以把imglinkiframeastyle中和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属性,会直接发起请求,我们无法拦截。但是,我们可以通过调用CSSStyleDeclarationsetProperty方法进行属性设置,所以我们需要在CSSStyleDeclaration的原型链上定义上面的属性,通过设置settergetter,然后调用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-imagebackground等属性进行hook时,调用了setProperty方法进行设置,若原代码中直接就调用的setProperty方法进行设置,则需要对setProperty的属性描述符(property descriptor)进行重写。

HTML中的请求

HTML中的请求,我们无法进行拦截,但可以使用MutationObserver监听DOM对象的创建,对于其中的a标签,可以修改href属性。对于imgsrc属性也可以修改,但无法阻止请求的发出,修改后的请求也会正常发出。

我们先在HTML中添加一个图片显示的DOM

<img src="/data/hook-before.jpg" />

在没有监听和修改前,页面显示的是HOOK前的图片,如下:

image

然后,我们在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();

保存,然后刷新一下页面,可以发现显示的图片已经发生了改变。

image-20211103094141533

在这里,虽然我们看到的图片已经发生了变化,但实际是在HTML中指定的图片依然会发出请求。

image-20211103094342364

在Developer Tools的网络标签中,可以看到,发出了两次图片请求。

关于MutationObserver的具体用法,请可以参考

在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;
image-20211103161320432

未解决的问题

如果在网页的脚本中,有通过locationlocation.href来重定向页面地址,则无法对这个动作进行拦截,location对象已经被浏览器定义为了不可伪造,目前没有找到好的办法,只能通过服务端代理,将调用该属性的js代码进行替换。

通过属性描述可以看到,window上的locationlocation.href均设置了不可修改。

{enumerable: true, configurable: false, get: ƒ, set: ƒ}

写在最后

本文是以前在将淘宝手机页面搬进微信里显示的时候的研究结果,但后来这个也没有用起来,现在基于互联互通的政策要求,也就更没有使用场景了。

本文的相关代码已上传:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • Vue最全知识点,面试必备(基础到进阶,覆盖vue3.0,持续更新整理,欢迎补充讨论) 参考文章传送:1.童欧巴对...
    Robertbiu阅读 391评论 0 3
  • 基础篇说说你对MVVM的理解 Model-View-ViewModel的缩写,Model代表数据模型,View代表...
    dac06a3906bb阅读 242评论 0 1
  • 1.说说对双向绑定的理解 1.1、双向绑定的原理是什么 我们都知道Vue是数据双向绑定的框架,双向绑定由三个重要部...
    GuessYe阅读 460评论 0 0
  • 1 MVC 和 MVVM 区别 MVC MVC 全名是 Model View Controller,是模型(mod...
    c88cfe19384a阅读 1,542评论 0 1
  • ![Flask](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAW...
    极客学院Wiki阅读 7,234评论 0 3