我在使用Chrome 81時,如果Web服務器開啓了HTTP認證,包括基本認證與摘要認證,通過Chrome直接輸入url,會彈出對話框,要求用戶輸入用戶名和密碼,如果用戶名密碼都正確,則可以正常訪問,這是因爲瀏覽器會自己通過header向服務器發送認證信息。
但如果需要通過ajax訪問該url,執行代碼xhr.open(method, url, async, username, password),並不能通過HTTP的401認證,無論是服務器要求基本認證還是摘要認證,都無法通過,我明明傳了用戶名和密碼呀?換用IE 11,無論是摘要認證還是基本認證,都能完美通過,這點還是不錯的。
沒辦法,Chrome是當前主流瀏覽器,必須要兼容啊,既然有些瀏覽器沒有實現,就自己實現吧,代碼中做了兼容,如果是IE本身支持ajax的HTTP認證,則JS中自己的認證不會生效,而在Chrome中則會使用自己實現的認證流程。
代碼中用到了md5.js,請自行下載。
調用方法:
HttpAuth.ajax({
type: "POST", //請求類型,GET或POST
url: "http://ip:port/path/interfaceName", //請求地址
authType: "Digest", //認證類型,支持Diget與Basic,爲空會自動判斷
username: "user1", //用戶名
password: "123456", //密碼
data: {name:"我本有心", "gender":"男"}, //POST數據
contentType: "application/json; charset=UTF-8", //Content-Type
context: this, //回調函數上下文
success: function () { //成功回調
console.log("401 success");
},
error: function () { //失敗回調
console.log("401 error");
},
complete: function () { //完成回調,無論成功或失敗都會執行
console.log("401 complete");
}
});
實現代碼如下:
var HttpAuth = {
ajax: function (settings) {
/*
settings與$.ajax使用方式相同
{
url : "http://ip:port/path", //請求地址
type: "POST", //請求方式,默認GET
async: false, //是否異步,默認true
data : "", //發送到服務器的數據
username: "", //用戶名
password: "", //密碼
timeout: 3000, //超時時間,毫秒
contentType: "application/json; charset=UTF-8", //請求類型
context: window, //回調函數上下文
success : function(result) { //請求成功回調函數
console.log(result);
},
error: function(xhr) { //請求失敗回調函數
console.log(xhr);
},
complete: function(xhr) { //請求完成後回調函數 (請求成功或失敗之後均調用)
console.log(xhr);
},
headers: {key1: "value1", key2: "value2"} //請求時需要附加的Header
authType: "Digest", //認證類型,"Digest"或"Basic"
}
*/
var method = settings.type ? settings.type.toUpperCase() : "GET";
var url = settings.url;
var async = settings.hasOwnProperty("async") ? settings.async : true;
var username = settings.username ? settings.username : "";
var password = settings.password ? settings.password : "";
var urlMD5 = this._md5(url);
var dataStr = "";
if (typeof settings.data == "string") {
dataStr = settings.data;
} else if (typeof settings.data == "object") {
dataStr = JSON.stringify(settings.data);
}
settings._method = method;
settings._entityBody = dataStr;
settings._wwwaKey = "HttpAuth_" + urlMD5 + "_wwwa";
settings._ncKey = "HttpAuth_" + urlMD5 + "_nc";
settings.headers = settings.headers || {};
var xhr = new XMLHttpRequest();
//xhr.open(method, url, async);
xhr.open(method, url, async, username, password);
this._setHeader(xhr, settings);
if (settings.timeout) {
xhr.timeout = settings.timeout;
}
xhr._that = this;
xhr._settings = settings;
xhr.onreadystatechange = this._stateChange;
xhr.send(dataStr);
this._log("ajax requesting..., url=" + url + ", dataStr=" + dataStr);
},
_setHeader: function (xhr, settings) { //設置xhr的請求Header
//Authorization
if (settings.authType) { //需要認證
var wwwAuthenticate = localStorage.getItem(settings._wwwaKey);
var authorization = "*"; //默認值無意義
if (wwwAuthenticate) { //能讀取到,說明之前有緩存,或者是剛剛經過401並緩存了數據
authorization = this._getAuthorization(wwwAuthenticate, settings);
}
xhr.setRequestHeader("Authorization", authorization);
}
//Content-Type
if (settings.contentType) {
xhr.setRequestHeader("Content-Type", settings.contentType);
}
//循環設置Header
var headerNames = Object.getOwnPropertyNames(settings.headers);
for (var i = 0; i < headerNames.length; i++) {
xhr.setRequestHeader(headerNames[i], settings.headers[headerNames[i]]);
}
},
_stateChange: function () {
//this爲xhr
if (this.readyState == 4) {
var that = this._that; //that爲HttpAuth
var settings = this._settings;
if (this.status == 401) { //服務器要求提供認證信息
if (settings.hasOwnProperty("_auth401")) { //避免死循環
that._log("401 auth fail", "error");
that._log(settings);
} else {
var wwwAuthenticate = this.getResponseHeader("WWW-Authenticate");
localStorage.setItem(settings._wwwaKey, wwwAuthenticate); //緩存WWW-Authenticate
localStorage.setItem(settings._ncKey, 0); //nonceCount初始化
that._log("401 auth", "warn");
settings._auth401 = 1; //避免死循環
settings.authType = that._getAuthType(wwwAuthenticate); //校正用戶設定的authType
that.ajax(settings);
}
} else {
settings._auth401 && that._log("401 auth ok", "warn");
that._log("ajax status=" + this.status + ", responseText=" + this.responseText);
if (this.status == 200) {
if (settings.success) { //成功回調
settings.context ? settings.success.call(settings.context) : settings.success();
}
} else {
if (settings.error) { //失敗回調
settings.context ? settings.error.call(settings.context) : settings.error();
}
}
if (settings.error) { //結束回調
settings.context ? settings.complete.call(settings.context) : settings.complete();
}
}
}
},
_getAuthorization: function (wwwAuthenticate, settings) { //計算Authorization
var authorization = "";
if (settings.authType == "Basic") {
authorization = this._getBasicAuthorization(wwwAuthenticate, settings);
} else if (settings.authType == "Digest") {
authorization = this._getDigestAuthorization(wwwAuthenticate, settings);
}
return authorization;
},
_getBasicAuthorization: function (wwwAuthenticate, settings) { //計算Basic認證所需要的Authorization
//生成Authorization示例:Basic YWRtaW46MTIzNDU2
var plain = settings.username + ":" + settings.password;
var cipher = btoa(plain);
this._log("plain=" + plain + ", cipher=" + cipher);
//生成Authorization
var authorization = "Basic " + cipher;
return authorization;
},
_getDigestAuthorization: function (wwwAuthenticate, settings) { //計算Digest認證所需要的Authorization
/*
//生成Authorization示例:Digest username="xxx", realm="xxx", nonce="xxx", uri="xxx", response="xxx", opaque="xxx", qop=xxx, nc=xxx, cnonce="xxx"
username 用戶名,客戶端提供,需要在服務器配置
realm 領域,服務器產生,從401響應的WWW-Authenticate取得
nonce 服務端隨機值,服務器產生,從401響應的WWW-Authenticate取得
uri 訪問URI,客戶端提供
response 簽名,客戶端提供,根據指定算法計算得出,服務器會用同樣算法生成後比對
opaque 服務器產生,從401響應的WWW-Authenticate取得
qop 保護質量,服務器產生,從401響應的WWW-Authenticate取得
nc 計數器,客戶端提供,從1開始,每次加1,格式:00000001
cnonce 客戶端隨機值,客戶端提供
*/
var authObj = this._getAuthObject(wwwAuthenticate);
//基本參數
var authType = authObj.authType;
var username = settings.username;
var realm = authObj.realm;
var nonce = authObj.nonce;
var digestURI = this._extractUri(settings.url);
var opaque = authObj.opaque;
var qop = authObj.qop;
var nonceCount = this._getNonceCount(settings);
var clientNonce = this._getClientNonce();
var password = settings.password;
var method = settings._method;
var entityBody = settings._entityBody;
var response, ha1, ha2, a1, a2, plain;
//計算ha1
a1 = username + ":" + realm + ":" + password;
ha1 = this._md5(a1); //HA1=MD5(username:realm:password)
this._log("a1=" + a1 + ", ha1=" + ha1);
//計算ha2
if (qop == "auth-int") { //qop值爲"auth-int",HA2=MD5(method:digestURI:MD5(entityBody))
a2 = method + ":" + digestURI + ":" + this.md5(entityBody); //FIXME 如果entityBody爲空如何處理?
} else { //qop值爲"auth"或未指定,HA2=MD5(method:digestURI)
a2 = method + ":" + digestURI;
}
ha2 = this._md5(a2);
this._log("a2=" + a2 + ", ha2=" + ha2);
//計算response
if (qop == "auth-int" || qop == "auth") { //qop值爲"auth"或"auth-int",response=MD5(HA1:nonce:nonceCount:clientNonce:qop:HA2)
plain = ha1 + ":" + nonce + ":" + nonceCount + ":" + clientNonce + ":" + qop + ":" + ha2;
} else { //qop未指定,response=MD5(HA1:nonce:HA2)
plain = ha1 + ":" + nonce + ":" + ha2;
}
response = this._md5(plain);
this._log("plain=" + plain + ", response=" + response);
//生成Authorization
var authorization = "Digest ";
authorization += 'username="' + username + '", '; //用戶名,客戶端提供,需要在服務器配置
authorization += 'realm="' + realm + '", '; //領域,服務器產生,從401響應的WWW-Authenticate取得
authorization += 'nonce="' + nonce + '", '; //服務端隨機值,服務器產生,從401響應的WWW-Authenticate取得
authorization += 'uri="' + digestURI + '", '; //訪問URI,客戶端提供
authorization += 'response="' + response + '", '; //簽名,客戶端提供,根據指定算法計算得出,服務器會用同樣算法生成後比對
authorization += 'opaque="' + opaque + '", '; //服務器產生,從401響應的WWW-Authenticate取得
authorization += 'qop=' + qop + ', '; //保護質量,服務器產生,從401響應的WWW-Authenticate取得
authorization += 'nc=' + nonceCount + ', '; //計數器,客戶端提供,從1開始,每次加1
authorization += 'cnonce="' + clientNonce + '"'; //客戶端隨機值,客戶端提供
return authorization;
},
_getAuthObject: function (wwwAuthenticate) { //提取服務器返回的摘要認證參數
var type = this._getAuthType(wwwAuthenticate);
var obj;
if (type == "Digest") {
//wwwAuthenticate示例:Digest realm="WBYX", qop="auth", nonce="1583989654942:2ab901721748c084572fde8706983c55", opaque="B2E0C5F42FA322D8104C785B59561A81"
obj = {
authType: type,
realm: this._getAuthItem(wwwAuthenticate, "realm"), //WBYX
qop: this._getAuthItem(wwwAuthenticate, "qop"), //auth
nonce: this._getAuthItem(wwwAuthenticate, "nonce"), //1583989654942:2ab901721748c084572fde8706983c55
opaque: this._getAuthItem(wwwAuthenticate, "opaque"), //B2E0C5F42FA322D8104C785B59561A81
algorithm: this._getAuthItem(wwwAuthenticate, "algorithm")
};
} else if (type == "Basic") {
//wwwAuthenticate示例:Basic realm="WBYX"
obj = {
authType: type,
realm: this._getAuthItem(wwwAuthenticate, "realm") //WBYX
};
}
return obj;
},
_getAuthType: function (wwwAuthenticate) { //獲取認證類型
var type = wwwAuthenticate.split(" ")[0]; //Digest或Basic
return type;
},
_getAuthItem: function (str, name) { //從qop="auth"中提取出auth
var arr = str.match(name + '=' + '"(.+?)"');
//str爲:Digest realm="Authenticate yourself", qop="auth", nonce="1583989654942:2ab901721748c084572fde8706983c55", opaque="B2E0C5F42FA322D8104C785B59561A81"
//模式爲:auth="(?<result>.+?)"
//結果爲["auth", auth, index: 4, input: qop="auth", groups: {result: auth}]
var value = "";
if (arr && arr.length == 2) {
value = arr[1];
}
return value;
},
_extractUri: function (fullURL) { //從http://192.168.1.57/sddc/dataCollect提取/sddc/dataCollect
var uri = fullURL;
if (fullURL.substr(0, 4) == "http") { //http開頭說明是絕對地址
var searchPos = fullURL.indexOf("://") + "://".length;
var rootPos = fullURL.indexOf("/", searchPos); //查找"http://"後第一個"/"的位置
uri = fullURL.substring(rootPos);
}
return uri;
},
_getClientNonce: function () { //產生clientNonce,要求隨機且唯一
var clientNonce = "cn" + String(Date.now());
return clientNonce;
},
_getNonceCount: function (settings) { //產生nonceCount
var nc = Number(localStorage.getItem(settings._ncKey)); //如果不存在則爲0
nc = nc + 1; //第一次爲1,之後每次加1
localStorage.setItem(settings._ncKey, nc);
var ncStr = String(nc);
var len = 8 - ncStr.length;
for (var i = 0; i < len; i++) { //將"1"轉爲"00000001"
ncStr = "0" + ncStr;
}
return ncStr;
},
_md5: function (str) {
return md5(str); //調用md5.min.js
},
_logSwitch: true, //日誌開關
_log: function (msg, type) {
if (!this._logSwitch) {
return;
}
if (type == "warn") {
console.warn(msg);
} else if (type == "error") {
console.error(msg);
} else {
console.log(msg);
}
}
};