我在使用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);
}
}
};