前文介绍了通过WAF在页面注入JS,通过劫持从浏览器页面发起的请求,从而实现透明零侵入的提供风控和防爬能力。本文主要讨论如何在浏览器端实现对所有出站请求的透明拦截。
劫持点分析
浏览器中发起网络请求的途径主要有以下几种,我们的目标:全部劫持,一个不漏。
Hook #1: AJAX劫持
原理
AJAX请求主要通过XMLHttpRequest 是浏览器提供的原生对象。劫持的核心思路是:
- 备份原生构造函数
- 替换为我们的 Proxy 构造函数
- 转发所有调用到原生对象,同时注入我们的逻辑
┌─────────────────────────────────────────────────────────┐
│ Application Code │
│ xhr.open() / xhr.send() │
└─────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Hijacked XMLHttpRequest │
│ ┌───────────────────────────────────────────────────┐ │
│ │ • Intercept open() → Inject params to URL │ │
│ │ • Intercept send() → Inject params to POST body │ │
│ │ • Intercept response → Check risk_user flag │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Native XMLHttpRequest │
│ (Real Network) │
└─────────────────────────────────────────────────────────┘
核心实现
var oldXML = window.XMLHttpRequest; // 备份原生对象
function hookXMLHttpRequest() {
var originXHR = new oldXML(); // 创建真实 XHR 实例
var hookXHR = this; // 代理对象引用
// Hook open(): 劫持 URL,注入前端采集的风控数据
hookXHR.open = function(method, url) {
this._url = url;
var parsedUrl = parseURL(url);
if (isConfigUrl(parsedUrl) && method === 'GET') {
parsedUrl.search += (parsedUrl.search ? '&ua=' : '?ua=') + encodeURIComponent(ua);
url = combineUrl(parsedUrl);
}
originXHR.open.call(originXHR, method, url);
}
// Hook send(): POST 请求注入数据
hookXHR.send = function(data) {
if (this._method === 'POST' && isConfigUrl(this._parsedUrl)) {
data = addPostData(data, 'ua', ua);
}
originXHR.send.call(originXHR, data);
}
// Hook readystatechange: 响应拦截
originXHR.onreadystatechange = function(e) {
syncStatus(originXHR, hookXHR);
if (originXHR.readyState === 4 && originXHR.responseText === 'risk_user') {
// 风险用户!阻断请求,触发人机验证
block_list.push({ type: 'ajax', oldXHR: hookXHR, /* ... */ });
showBlock();
return;
}
hookXHR.onreadystatechange?.call(hookXHR, e);
}
}
function syncStatus(xhr, copy) {
//需要随着state变化同步的属性
try {
var syncList = ['readyState', 'response', 'responseText', 'responseType', 'responseXML', 'status', 'withCredentials', 'upload', 'statusText'];
for (var i = 0; i < syncList.length; i++) {
var name = syncList[i];
copy[name] = xhr[name];
}
} catch(e) {}
}
// 替换全局构造函数
window.XMLHttpRequest = hookXMLHttpRequest;
TODO List:
因为篇幅精简了具体实现,为了保证Ajax的完整实现,还需要实现如下细节
| 技术点 | 说明 |
|---|---|
| 状态同步 | readyState, response, status 等属性需实时同步到代理对象 |
| 方法代理 | abort(), getResponseHeader() 等需要透传到原生对象 |
| 事件绑定 | onload, onerror, ontimeout 等事件需正确转发 |
Hook #2: JSONP 劫持
原理
JSONP 是通过在DOM上动态创建 <script> 标签实现跨域请求。通用的劫持点(部分网站可以基于JSONP的具体实现,需要进行部分自定义):
document.head.appendChild()document.head.insertBefore()
核心实现
function hookJSONP() {
function jsonpFactory(oldFn) {
return function(ele) {
if (ele.tagName !== 'SCRIPT') return oldFn.apply(this, arguments);
var url = parseURL(ele.src);
var callbackName = parseQuery(url.search).callback;
if (isConfigUrl(url)) {
// 1. 修改 URL 注入参数
ele.src += '&ua=' + encodeURIComponent(ua);
// 2. 劫持回调函数
var oldCb = window[callbackName];
window[callbackName] = function(data) {
if (data === 'risk_user') {
block_list.push({ type: 'jsonp', url: ele.src, oldCbs: oldCb });
showBlock();
} else {
oldCb?.(data); // 正常执行业务回调
}
};
}
return oldFn.apply(this, arguments); // 执行原生方法
}
}
// Hook DOM 操作方法
hook(document.head, 'appendChild', jsonpFactory);
hook(document.head, 'insertBefore', jsonpFactory);
}
function hook(obj, key, factory) {
var oldFn = obj[key];
obj[key] = factory(oldFn); // 工厂模式:包装原函数
}
Hook #3: Form 表单劫持
原理
表单提交是比较传统的API请求方式。可以通过监听 submit 事件,在提交前动态注入隐藏字段。这样可以避免在用户浏览过程中导致form表单发生变化的情况。
核心实现
function hookForm() {
var forms = document.forms;
for (var i = 0; i < forms.length; i++) {
var form = forms[i];
if (isConfigUrl(parseURL(form.action))) {
// 监听 submit 事件(捕获阶段)
form.addEventListener('submit', function(e) {
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'ua';
input.value = ua; // 注入风控数据
form.appendChild(input);
}, true);
}
}
}
Hook #4: Link 导航劫持
原理
用户点击 <a> 标签时的页面跳转也需要拦截,因为有可能通过跳转GET的方式实现API请求。通过监听 document 的 click 事件可以实现劫持。
核心实现
function hookLink() {
eventHandler(document, 'click', function(e) {
e = e || window.event;
var target = e.target || e.srcElement;
do {
if (target.tagName == 'A') {
if (isConfigUrl(target)) {
target.search += (target.search ? '&ua=' : '?ua=') + encodeURIComponent(ua);
target.src = combineUrl(target);
}
break;
}
} while (target = target.parentNode);
});
}