Ajax实现HTTP摘要认证与基本认证

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章