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);
        }
    }
};

 

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