1Ajax定義
Ajax:(Asynchronous JavaScript and XML)用JavaScript執(zhí)行異步網(wǎng)絡(luò)請求。
1.1Ajax技術(shù)帶來的改變
- 一次HTTP請求對應(yīng)一個頁面
- 異步數(shù)據(jù)刷新
Ajax技術(shù)包含的內(nèi)容:
- 使用CSS和XHTML來表示。
- 使用DOM模型來交互和動態(tài)顯示。
- 使用XMLHttpRequest來和服務(wù)器進行異步通信。
- 使用javascript來綁定和調(diào)用。
1.2利用XMLHttpRequest發(fā)送請求
(function () {
function success(text) {
alert('success' + text);
}
function fail(code) {
alert('fail' + code);
}
var request = new XMLHttpRequest(); // 新建XMLHttpRequest對象
request.onreadystatechange = function () { // 狀態(tài)發(fā)生變化時,函數(shù)被回調(diào)
if (request.readyState === 4) { // 成功完成
// 判斷響應(yīng)結(jié)果:
if (request.status === 200) {
// 成功,通過responseText拿到響應(yīng)的文本:
success(request.responseText);
} else {
// 失敗,根據(jù)響應(yīng)碼判斷失敗原因:
fail(request.status);
}
} else {
// HTTP請求還在繼續(xù)...
}
}
// 發(fā)送請求
request.open('GET', '/api/categories');
// request.open('POST', '/api/categories');
request.send();
alert('請求已發(fā)送,請等待響應(yīng)...');
})();
從中我們可以看出XMLHttpRequest對象主要包括以下屬性:
- onreadystatechange 回調(diào)函數(shù)
- readyState 網(wǎng)絡(luò)發(fā)起和結(jié)束的狀態(tài)
0: 請求未初始化
1: 服務(wù)器連接已建立
2: 請求已接收
3: 請求處理中
4: 請求已完成,且響應(yīng)已就緒 - status 網(wǎng)絡(luò)響應(yīng)的狀態(tài)
200: "OK"
404: 未找到頁面 - open 規(guī)定請求的類型、URL 以及是否異步處理請求
- send 將請求發(fā)送到服務(wù)器,post可支持入?yún)?/li>
2AJAX跨域請求的問題
因為瀏覽器的同源策略導(dǎo)致的,默認情況下,JavaScript在發(fā)送AJAX請求時,URL的域名必須和當前頁面完全一致。
一致的含義:
- 域名 www.example.com和example.com不同
- 協(xié)議 http和https不同
- 端口號 :80和:8080不同
那么問題就來了,不是所有的資源或數(shù)據(jù)都會緩存在當前同源服務(wù)器下面,即使通過代理或緩存服務(wù)器的策略。
如何解決跨域訪問的問題?
由于技術(shù)實現(xiàn)和應(yīng)用場景原因,存在許多js跨域請求的解決方案,下面說我應(yīng)用過的幾種解決方案。
2.1JSONP
特點:
- 只能用GET請求
- 返回JavaScript
原理:利用了瀏覽器允許跨域引用JavaScript資源(場景,例如引用CDN下的第三方j(luò)s依賴)。
//動態(tài)添加<script>...</script>標簽
var js = document.createElement('script'),
head = document.getElementsByTagName('head')[0];
js.src = 'http://example.com/abc.js';
head.appendChild(js);
執(zhí)行以上方法后,瀏覽器拉取http://example.com/abc.js源碼:
refreshPrice({"0000001":{"code": "0000001", ... });
在當前JS中增加回調(diào)函數(shù)
function refreshPrice(data) {
var p = document.getElementById('test-jsonp');
p.innerHTML = '當前價格:' +
data['0000001'].name +': ' +
data['0000001'].price + ';' +
data['1399001'].name + ': ' +
data['1399001'].price;
}
這樣,我們就通過JSONP完成了跨域請求,得到了返回。
一般,我們采用JQuery提供的方式可以較為方便的實現(xiàn)jsonp。前提是,對應(yīng)的源有相應(yīng)的后端實現(xiàn),即提供GET方式的請求和規(guī)范的JavaScript返回。
值得注意的是,通過get請求的目標源文件采用的是fetch方式,而不是XMLHttpRequest對象,所以在瀏覽器network中XHR里找不到該請求。JSONP真正意義上,不屬于ajax的技術(shù)范疇。
2.2CORS
CORS:全稱Cross-Origin Resource Sharing,是HTML5規(guī)范定義的如何跨域訪問資源(前提是,瀏覽器支持HTML5)。
1簡單請求

當ajax通過瀏覽器發(fā)出跨域請求,瀏覽器收到響應(yīng)后檢查header中Access-Control-Allow-Origin是否包含當前域(origin)。如果有,則跨域成功。
2預(yù)檢測請求
PUT、DELETE以及其他類型如application/json的POST請求,將會進入以下流程。
先發(fā)出OPTIONS請求詢問服務(wù)端支持的請求方式;
確認成功后,進行數(shù)據(jù)的請求。

請參考https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
2.3JS-Native客戶端代理
原理很簡單,因為瀏覽器有同源限制,然后客戶端、后端并沒有,我們是可以在代理對象(客戶端或后端集群)上收到遠程服務(wù)的響應(yīng)。
因此,通過將前端的請求頭和請求body等參數(shù)封裝后,提交給客戶端進行抓取,從而達到跨域的目的。

mxSaveRequest({//此處調(diào)用js-native接口mxSaveRequest將請求發(fā)送給客戶端
itemName: "bindQuery-" + e + ".json",
type: "GET",
url: "https://zht.alipay.com/asset/bindQuery.json?_input_charset=utf-8&providerType=" + e + "&t=" + (new Date).getTime() + "&ctoken=" + (0, y.getCookie)("ctoken"),
headers: {
Accept: "*/*",
"Accept-Language": "en-US,en;q=0.8,zh;q=0.6,zh-CN;q=0.4,ja;q=0.2,zh-TW;q=0.2",
"Cache-Control": "max-age=0",
"X-Requested-With": "XMLHttpRequest",
Host: "zht.alipay.com",
Referer: "https://zht.alipay.com/asset/bankList.htm",
"User-Agent": navigator.userAgent
},
encoding: "GBK"
})
客戶端提供mxSaveRequest的端能力代碼
@NotProguard
@JavascriptInterface
public void mxSaveRequest(final String str) {//注入js對象用于接受跨域請求的參數(shù)
Logger.json(str);
WebViewECV3Presenter.this.handler.post(new Runnable() {
public void run() {
WebViewECV3Presenter.call(WebViewECV3Presenter.this, str);
}
});
}
static /* synthetic */ void call(final WebViewECV3Presenter webViewECV3Presenter, final String str) {
try {
new Thread(new Runnable() {
public void run() {
try {
JsBaseRequest jsBaseRequest = JsBaseRequest.getJsRequest(str);
byte[] response =
WebViewECV3Presenter.getbytes(webViewECV3Presenter, jsBaseRequest);//http request獲取請求的接口
WebViewECV3Presenter.a(webViewECV3Presenter,/*call back result*/ response, jsBaseRequest);//put
// maps jsreq.itemName
WebViewECV3Presenter.callback(webViewECV3Presenter, response, jsBaseRequest);//回調(diào)js請求的接口,給js回調(diào)的數(shù)據(jù)需要base64加密
} catch (Throwable e) {
ErrorHandle.b("sendRequest fail", e);
}
}
}).start();
} catch (Throwable e) {
ErrorHandle.b("sendRequest fail1", e);
}
}
//請求的數(shù)據(jù)結(jié)構(gòu)
public class JsBaseRequest {
public String type = "";
public String url = "";
public String headers = "";
public String data = "";
public String itemName = "";
public String encoding = "UTF-8";
public int failCode = ErrorCode.INFO_CODE_BASE;
public String responseId = "";
public static JsBaseRequest getJsRequest(String str) {
JsBaseRequest jsBaseRequest = new JsBaseRequest();
JSONObject jSONObject = null;
try {
jSONObject = new JSONObject(str);
} catch (JSONException e) {
e.printStackTrace();
}
jsBaseRequest.type = jSONObject.optString("type");
jsBaseRequest.url = jSONObject.optString("url");
jsBaseRequest.headers = jSONObject.optString("headers");
jsBaseRequest.data = jSONObject.optString("data");
jsBaseRequest.itemName = jSONObject.optString("itemName");
jsBaseRequest.responseId = jSONObject.optString("responseId");
if (jSONObject.has("encoding")) {
jsBaseRequest.encoding = jSONObject.optString("encoding");
if (jsBaseRequest.encoding.equalsIgnoreCase("UTF8")) {
jsBaseRequest.encoding = "UTF-8";
}
}
if (jSONObject.has("failCode")) {
jsBaseRequest.failCode = jSONObject.optInt("failCode");
}
return jsBaseRequest;
}
}
//callback JS
webViewECV3Presenter.webViewECV3Fragment.loadUrl("requestFinishCallback('" + jSONObject.toString() + "')");
2.3常見的問題
2.3.1身份憑證
Fetch(JSONP形式) 與 CORS 的一個有趣的特性是——可以基于 HTTP cookies 和 HTTP 認證信息發(fā)送身份憑證。一般而言對于跨域 XMLHttpRequest 或 Fetch 請求,瀏覽器不會發(fā)送身份憑證信息——即cookies。如果要發(fā)送憑證信息,需要設(shè)置 XMLHttpRequest 的某個特殊標志位——withCredentials。
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/credentialed-content/';
function callOtherDomain(){
if(invocation) {
invocation.open('GET', url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
}
2.3.2使用瀏覽器插件net-internals

遇到這樣的問題想查看header的詳細參數(shù),請使用插件。
2.3.3攜帶cookie的修改參數(shù)
ajax跨域中直接設(shè)置請求頭是不允許,我們只能在發(fā)送前修改cookie
document.cookie = 'domain=m.taobao.com';
$.ajax({
url: trust_url,
method: 'get',
xhrFields: {
withCredentials: true
},
crossDomain: true,
success: function (res) {
alert("success\n" + JSON.stringify(res));
alipayAuth(res);
},
error: function (res) {
alert("fail");
}
});
3抓取的應(yīng)用場景——芝麻分、花唄破解方案
3.1芝麻分接口獲取

18年中旬,基本完成支付寶、淘寶電商抓取的全部技術(shù)棧,其中對前端抓取最常見的就是ajax的跨域請求。

芝麻分的請求就是通過jsonp進行跨域的,支付寶服務(wù)端支持callback參數(shù),通過自定義function name可以在js端收到請求響應(yīng)回調(diào)。
我們注冊了一個名為mtopjsonp1的方法用來接受js對象。
3.2花唄接口獲取
ajax請求的代碼,其中有一次jsonp請求和一次CORS簡單跨域請求(響應(yīng)頭包含:access-control-allow-credentials: true),最后的結(jié)果會在客戶端保存。

花唄的請求結(jié)果:

CORS跨域二次請求方案在老版本的芝麻分和借唄,后端接口提供過,首先會發(fā)送一次option請求獲取后端支持跨域的操作get、post等,然后通過相應(yīng)的請求獲取數(shù)據(jù)。
當然以上討論的都是前端抓取的技術(shù)方案,如果在接口已經(jīng)破解的情況下,只需要客戶端或后端拿到cookie后,直接對服務(wù)端發(fā)起請求就可以拿到數(shù)據(jù)了,后端用python實現(xiàn)回更簡單。