ONVIF WS-UsernameToken 密码验证

ONVIF WS-UsernameToken password validation

我正在尝试发送一条 ONVIF PTZ soap 消息以获取摄像机的状态作为简单测试。我也在努力保持这种纯粹JavaScript。我不能使用 Node.js 因为应用程序的其余部分是用不同的语言编写的,我需要它作为客户端。我尝试做的测试之一是复制 ONVIF TM 应用程序程序员指南中的结果。我可以发送 soap 消息以从 SoapUI 获取状态,但 SoapUI 不使用 WS-UsernameToken。

这是一个简单的 HTML 文件:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<!-- This folder is for asking the question of how to access a module from JQuery -->
    <head>
        <title>My Test Page</title>
        <!-- sha.js is from jsSHA library (https://github.com/Caligatio/jsSHA) -->
        <script src="./crypto/sha1.js"></script>
        <script src="./soap.js"></script>
        <script src="https://code.jquery.com/jquery-3.6.0.js" integrity="sha256-H+K7U5CnXl1h5ywQfKtSj8PCmoN9aaq30gDh27Xc0jk=" crossorigin="anonymous"></script>
    </head>
    <body>
        My page.

        <h1>Camera Status:</h1>
        <textarea class="statusArea" rows="20" cols="40" style="border:none;">
        
        </textarea>
        <script>
            $(document).ready(function() { 
                testSoap(); 
            });
        </script>
    </body>
</html>

这是一个 JavaScript 文件:

const testPW = "testPassword";

const textHash = new jsSHA( "SHA-1", "TEXT");

const PasswordType = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest";
const WSSE = 'xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"';
const WSU = 'xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"';

const testData = {
    nonce: 'LKqI6G/AikKCQrN0zqZFlg==', 
    date: '2010-09-16T07:50:45Z',
    password: 'userpassword',
    result: 'tuOSpGlFlIXsozq4HFNeeGeFLEI='
};

const pwDigestFormula = (nonce_, date_, pw_) => {
    let temp = nonce_ + date_ + pw_;
    textHash.update(temp);
    return textHash.getHash("B64");
}

const getNonce = (length = 24) => {
    var text = "";
    var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    for(var i = 0; i < length; i++) {
        text += possible.charAt(Math.floor(Math.random() * possible.length));
    }
    return text;
};

const getIsoTimestamp = () => {
    let d = (new Date()).toISOString();
    console.log(d);
    return d;
};

const getPasswordDigest = (password_) => {
    let result = {
        passwordType: PasswordType,
        nonce: getNonce(),
        created: getIsoTimestamp(),
        digestPassword: null
    };

    result.digestPassword = pwDigestFormula(atob(result['nonce']), result['created'], password_);
    return result;
}

const TEST_ONVIF_PTZ_SERVICE_URL = "http://###.###.###.###/onvif/ptz";

const getObjectTypeName = (object_) => {
    return (object_?.constructor?.name ?? null);
};


/*
    Parts of this class were from 
 */
class SoapMessageObj {

    #mediaProfile = 'test!';

    commands = {
        SECURE_HEADER: (username_, password_, nonce_, isoTimestamp_) => 
            `<soap:Header>
                <wsse:Security xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> 
                    <wsse:UsernameToken> 
                        <wsse:Username>${username_}</wsse:Username>
                        <wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">${nonce_}</wsse:Nonce>
                        <wsu:Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">${isoTimestamp_}</wsu:Created>
                        <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">${password_}</wsse:Password>
                    </wsse:UsernameToken> 
                </wsse:Security> 
            </soap:Header>`,
        STATUS: (profileToken_ = 'media_profile1', header_ = '<soap:Header/>', attributes_ = null) => {
            if (null!== attributes_) {
                attributes_ = ` ${attributes_.join(' ')}`;
            } else {
                attributes_ = '';
            }

            return `<?xml version="1.0" encoding="utf-8"?>
                <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:wsdl="http://www.onvif.org/ver20/ptz/wsdl" ${attributes_}>
                    ${header_}
                    <soap:Body>
                        <wsdl:GetStatus>
                            <wsdl:ProfileToken>${profileToken_}</wsdl:ProfileToken>
                        </wsdl:GetStatus>
                    </soap:Body>
                </soap:Envelope>`
        }
    };

    xmlSerializer = new XMLSerializer();

    // String containing the soap message
    #soapMessage = null;

    // URL object
    #url = null; 

    constructor(soapUrl_) {
        let objectType = getObjectTypeName(soapUrl_);
        switch(objectType) {
            case 'String':
                this.#url = new URL(soapUrl_);
                break;
            case 'URL':
                this.#url = soapUrl_;
                break;
            default:
                throw new Error(`Error: unknown object in SoapMessageObj call: ${objectType}`);
        }
    };

    /*
     * Getters and Setters
     */
    get soapMessage() {
        return this.#soapMessage;
    };
    set soapMessage(value) {
        this.#soapMessage = value;
    };

    get url() {
        return this.#url;
    };
    set url(url_) {
        this.#url = url_;
    };

    get mediaProfile() {
        return this.#mediaProfile;
    }
    set mediaProfile(mediaProfile_) {
        this.#mediaProfile = mediaProfile_;
    }


    /*
        Default processing for Success
    */
    async processSuccess(data_, status_, req_)  {
        let dataType = getObjectTypeName(data_);
        console.log('Successfully Sent command');
        console.debug( `SUCCESS.  Status: ${status_}` );
        console.debug('Data object: ' + dataType);
        console.debug('req object: ' + getObjectTypeName(req_));

        this.response = data_;
        if (dataType === "XMLDocument") {
            console.debug(this.xmlSerializer.serializeToString(data_));
        } else {
            for (let o in data_) {
                console.debug(`${o}: ${data_[o]}`);
            }
        }
    };

    /*
        Default processing for failure.
    */
    async processError(data_, status_, req_) {
        console.debug( `ERROR.  Status: ${status_}` );
        let dataType = getObjectTypeName(data_);
        console.debug('Data object: ' + dataType);
        if (dataType === "XMLDocument") {
            console.debug(this.xmlSerializer.serializeToString(data_));
        } else {
            dataType = getObjectTypeName(data_.responseXML);
            if (dataType  === "XMLDocument" ) {
                this.response = data_.responseXML;
                console.clear();
                console.debug('responseXML property object: ' + dataType);
                console.debug(this.xmlSerializer.serializeToString(data_.responseXML));
            } else {
                this.response = data_;
                for (let o in data_) {
                    console.debug(`${o}: ${data_[o]}`);
                }
            }
        }
    };
            
    /*
        Pass in JavaScript SoapMessageObj object
        The bind is needed to insure the right class/object for the "this" variable. 
    */
    async sendSoapMessage(soapMessage_, success_ = this.processSuccess.bind(this), failure_ = this.processError.bind(this), context_ = this) {
        jQuery.support.cors = true;

        $.ajax({
            type: "POST",
            url: context_.url.href,
            crossDomain: true,
            processData: false,
            data: soapMessage_,
            success: success_,
            error: failure_
        });
    };

};


/*
    Test function
 */
function testSoap() {
    //try to replicate the example from ONVIF_WG-APG-Application_Programmers_Guide-1.pdf
    let test = btoa(pwDigestFormula( atob(testData.nonce), testData.date, testData.password ) )
    console.debug(`atob(btoa): ${test} testData equal: ${test==testData.result}`);
    test = atob(pwDigestFormula( btoa(testData.nonce), testData.date, testData.password ) );
    console.debug(`atob(btoa): ${test} testData equal: ${test==testData.result}`);
};

更新 2022 年 3 月 9 日 从 testSoap 中删除了额外的代码。

我将此张贴在这里,以便任何其他寻找答案的人都能得到它。我通过谷歌搜索、来自同事的 link 以及反复试验找到了答案。我能够使用两个 JavaScript 代码文件复制示例。为了方便起见,我把它们合并成一个。

/**
 * base64.js
 * Original author: Chris Veness
 * Repository: https://gist.github.com/chrisveness/e121cffb51481bd1c142
 * License: MIT (According to a comment).
 *
 * 03/09/2022 JLM Updated to ES6 and use strict.
 */

/**
 * Encode string into Base64, as defined by RFC 4648 [http://tools.ietf.org/html/rfc4648].
 * As per RFC 4648, no newlines are added.
 *
 * Characters in str must be within ISO-8859-1 with Unicode code point <= 256.
 *
 * Can be achieved JavaScript with btoa(), but this approach may be useful in other languages.
 *
 * @param {string} str_ ASCII/ISO-8859-1 string to be encoded as base-64.
 * @returns {string} Base64-encoded string.
 */
 "use strict";

 const B64_CHARS =
     "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
 
 //function base64Encode(str) {
 const base64Encode = (str_) => {
     if (/([^\u0000-\u00ff])/.test(str_)) throw Error("String must be ASCII");
 
     let b64 = B64_CHARS;
     let o1,
         o2,
         o3,
         bits,
         h1,
         h2,
         h3,
         h4,
         e = [],
         pad = "",
         c;
 
     c = str_.length % 3; // pad string to length of multiple of 3
     if (c > 0) {
         while (c++ < 3) {
             pad += "=";
             str_ += "[=10=]";
         }
     }
     // note: doing padding here saves us doing special-case packing for trailing 1 or 2 chars
 
     for (c = 0; c < str_.length; c += 3) {
         // pack three octets into four hexets
         o1 = str_.charCodeAt(c);
         o2 = str_.charCodeAt(c + 1);
         o3 = str_.charCodeAt(c + 2);
 
         bits = (o1 << 16) | (o2 << 8) | o3;
 
         h1 = (bits >> 18) & 0x3f;
         h2 = (bits >> 12) & 0x3f;
         h3 = (bits >> 6) & 0x3f;
         h4 = bits & 0x3f;
 
         // use hextets to index into code string
         e[c / 3] =
             b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
     }
     str_ = e.join(""); // use Array.join() for better performance than repeated string appends
 
     // replace 'A's from padded nulls with '='s
     str_ = str_.slice(0, str_.length - pad.length) + pad;
 
     return str_;
 };
 
 /**
  * Decode string from Base64, as defined by RFC 4648 [http://tools.ietf.org/html/rfc4648].
  * As per RFC 4648, newlines are not catered for.
  *
  * Can be achieved JavaScript with atob(), but this approach may be useful in other languages.
  *
  * @param {string} str_ Base64-encoded string.
  * @returns {string} Decoded ASCII/ISO-8859-1 string.
  */
 //function Base64Decode(str) {
 const base64Decode = (str_) => {
     if (!/^[a-z0-9+/]+={0,2}$/i.test(str_) || str_.length % 4 != 0)
         throw Error("Not base64 string");
 
     let b64 = B64_CHARS;
     let o1,
         o2,
         o3,
         h1,
         h2,
         h3,
         h4,
         bits,
         d = [];
 
     for (let c = 0; c < str_.length; c += 4) {
         // unpack four hexets into three octets
         h1 = b64.indexOf(str_.charAt(c));
         h2 = b64.indexOf(str_.charAt(c + 1));
         h3 = b64.indexOf(str_.charAt(c + 2));
         h4 = b64.indexOf(str_.charAt(c + 3));
 
         bits = (h1 << 18) | (h2 << 12) | (h3 << 6) | h4;
 
         o1 = (bits >>> 16) & 0xff;
         o2 = (bits >>> 8) & 0xff;
         o3 = bits & 0xff;
 
         d[c / 4] = String.fromCharCode(o1, o2, o3);
         // check for padding
         if (h4 == 0x40) d[c / 4] = String.fromCharCode(o1, o2);
         if (h3 == 0x40) d[c / 4] = String.fromCharCode(o1);
     }
     str_ = d.join(""); // use Array.join() for better performance than repeated string appends
 
     return str_;
 };
 

/*************************************************************************************
 * wsse.js - Generate WSSE authentication header in JavaScript
 */

//
// wsse.js - Generate WSSE authentication header in JavaScript
// (C) 2005 Victor R. Ruiz <victor*sixapart.com> - http://rvr.typepad.com/
//
// Parts:
//   SHA-1 library (C) 2000-2002 Paul Johnston - BSD license
//   ISO 8601 function (C) 2000 JF Walker All Rights
//   Base64 function (C) aardwulf systems - Creative Commons
//
// Example call:
//
//   let w = wsseHeader(Username, Password);
//   alert('X-WSSE: ' + w);
//
// Changelog:
//   2005.07.21 - Release 1.0
//

/*
 * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
 * in FIPS PUB 180-1
 * Version 2.1a Copyright Paul Johnston 2000 - 2002.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for details.
 */

/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
let hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase        */
let b64pad = "="; /* base-64 pad character. "=" for strict RFC compliance   */
let chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode      */

const VALID_CHARS =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
const hex_sha1 = (s_) => {
    return binb2hex(core_sha1(str2binb(s_), s_.length * chrsz));
};
const b64_sha1 = (s_) => {
    return binb2b64(core_sha1(str2binb(s_), s_.length * chrsz));
};
const str_sha1 = (s_) => {
    return binb2str(core_sha1(str2binb(s_), s_.length * chrsz));
};
const hex_hmac_sha1 = (key_, data_) => {
    return binb2hex(core_hmac_sha1(key_, data_));
};
const b64_hmac_sha1 = (key_, data_) => {
    return binb2b64(core_hmac_sha1(key_, data_));
};
const str_hmac_sha1 = (key_, data_) => {
    return binb2str(core_hmac_sha1(key_, data_));
};

/*
 * Perform a simple self-test to see if the VM is working
 */
function sha1_vm_test() {
    return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d";
}

/*
 * Calculate the SHA-1 of an array of big-endian words, and a bit length
 */
function core_sha1(x_, len_) {
    /* append padding */
    x_[len_ >> 5] |= 0x80 << (24 - (len_ % 32));
    x_[(((len_ + 64) >> 9) << 4) + 15] = len_;

    let w = Array(80);
    let a = 1732584193;
    let b = -271733879;
    let c = -1732584194;
    let d = 271733878;
    let e = -1009589776;

    for (let i = 0; i < x_.length; i += 16) {
        let olda = a;
        let oldb = b;
        let oldc = c;
        let oldd = d;
        let olde = e;

        for (let j = 0; j < 80; j++) {
            if (j < 16) w[j] = x_[i + j];
            else w[j] = rol(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
            let t = safe_add(
                safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
                safe_add(safe_add(e, w[j]), sha1_kt(j))
            );
            e = d;
            d = c;
            c = rol(b, 30);
            b = a;
            a = t;
        }

        a = safe_add(a, olda);
        b = safe_add(b, oldb);
        c = safe_add(c, oldc);
        d = safe_add(d, oldd);
        e = safe_add(e, olde);
    }
    return Array(a, b, c, d, e);
}

/*
 * Perform the appropriate triplet combination function for the current
 * iteration
 */
function sha1_ft(t_, b_, c_, d_) {
    if (t_ < 20) return (b_ & c_) | (~b_ & d_);
    if (t_ < 40) return b_ ^ c_ ^ d_;
    if (t_ < 60) return (b_ & c_) | (b_ & d_) | (c_ & d_);
    return b_ ^ c_ ^ d_;
}

/*
 * Determine the appropriate additive constant for the current iteration
 */
function sha1_kt(t_) {
    return t_ < 20
        ? 1518500249
        : t_ < 40
        ? 1859775393
        : t_ < 60
        ? -1894007588
        : -899497514;
}

/*
 * Calculate the HMAC-SHA1 of a key and some data
 */
function core_hmac_sha1(key_, data_) {
    let bkey = str2binb(key_);
    if (bkey.length > 16) bkey = core_sha1(bkey, key_.length * chrsz);

    let ipad = Array(16),
        opad = Array(16);
    for (let i = 0; i < 16; i++) {
        ipad[i] = bkey[i] ^ 0x36363636;
        opad[i] = bkey[i] ^ 0x5c5c5c5c;
    }

    let hash = core_sha1(
        ipad.concat(str2binb(data_)),
        512 + data.length * chrsz
    );
    return core_sha1(opad.concat(hash), 512 + 160);
}

/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
function safe_add(x_, y_) {
    let lsw = (x_ & 0xffff) + (y_ & 0xffff);
    let msw = (x_ >> 16) + (y_ >> 16) + (lsw >> 16);
    return (msw << 16) | (lsw & 0xffff);
}

/*
 * Bitwise rotate a 32-bit number to the left.
 */
function rol(num_, cnt_) {
    return (num_ << cnt_) | (num_ >>> (32 - cnt_));
}

/*
 * Convert an 8-bit or 16-bit string to an array of big-endian words
 * In 8-bit function, characters >255 have their hi-byte silently ignored.
 */
function str2binb(str_) {
    let bin = Array();
    let mask = (1 << chrsz) - 1;
    for (let i = 0; i < str_.length * chrsz; i += chrsz)
        bin[i >> 5] |=
            (str_.charCodeAt(i / chrsz) & mask) << (32 - chrsz - (i % 32));
    return bin;
}

/*
 * Convert an array of big-endian words to a string
 */
function binb2str(bin_) {
    let str = "";
    let mask = (1 << chrsz) - 1;
    for (let i = 0; i < bin_.length * 32; i += chrsz)
        str += String.fromCharCode(
            (bin_[i >> 5] >>> (32 - chrsz - (i % 32))) & mask
        );
    return str;
}

/*
 * Convert an array of big-endian words to a hex string.
 */
function binb2hex(binarray_) {
    let hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
    let str = "";
    for (let i = 0; i < binarray_.length * 4; i++) {
        str +=
            hex_tab.charAt(
                (binarray_[i >> 2] >> ((3 - (i % 4)) * 8 + 4)) & 0xf
            ) +
            hex_tab.charAt((binarray_[i >> 2] >> ((3 - (i % 4)) * 8)) & 0xf);
    }
    return str;
}

/*
 * Convert an array of big-endian words to a base-64 string
 */
function binb2b64(binarray_) {
    //  let tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    let tab = VALID_CHARS;
    let str = "";
    for (let i = 0; i < binarray_.length * 4; i += 3) {
        let triplet =
            (((binarray_[i >> 2] >> (8 * (3 - (i % 4)))) & 0xff) << 16) |
            (((binarray_[(i + 1) >> 2] >> (8 * (3 - ((i + 1) % 4)))) & 0xff) <<
                8) |
            ((binarray_[(i + 2) >> 2] >> (8 * (3 - ((i + 2) % 4)))) & 0xff);
        for (let j = 0; j < 4; j++) {
            if (i * 8 + j * 6 > binarray_.length * 32) str += b64pad;
            else str += tab.charAt((triplet >> (6 * (3 - j))) & 0x3f);
        }
    }
    return str;
}

// aardwulf systems
// This work is licensed under a Creative Commons License.
// http://www.aardwulf.com/tutor/base64/
function encode64(input_) {
    let keyStr = `${VALID_CHARS}=`;
    /* 
    let keyStr = "ABCDEFGHIJKLMNOP" +
                "QRSTUVWXYZabcdef" +
                "ghijklmnopqrstuv" +
                "wxyz0123456789+/" +
                "=";
*/

    let output = "";
    let chr1,
        chr2,
        chr3 = "";
    let enc1,
        enc2,
        enc3,
        enc4 = "";
    let i = 0;

    do {
        chr1 = input_.charCodeAt(i++);
        chr2 = input_.charCodeAt(i++);
        chr3 = input_.charCodeAt(i++);

        enc1 = chr1 >> 2;
        enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
        enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
        enc4 = chr3 & 63;

        if (isNaN(chr2)) {
            enc3 = enc4 = 64;
        } else if (isNaN(chr3)) {
            enc4 = 64;
        }

        output =
            output +
            keyStr.charAt(enc1) +
            keyStr.charAt(enc2) +
            keyStr.charAt(enc3) +
            keyStr.charAt(enc4);
        chr1 = chr2 = chr3 = "";
        enc1 = enc2 = enc3 = enc4 = "";
    } while (i < input_.length);

    return output;
}

// TITLE
// TempersFewGit v 2.1 (ISO 8601 Time/Date script)
//
// OBJECTIVE
// Javascript script to detect the time zone where a browser
// is and display the date and time in accordance with the
// ISO 8601 standard.
//
// AUTHOR
// John Walker
// http://321WebLiftOff.net
// jfwalker@ureach.com
//
// ENCOMIUM
// Thanks to Stephen Pugh for his help.
//
// CREATED
// 2000-09-15T09:42:53+01:00
//
// UPDATED
// 2022-03-11 JLM Updated to ES6 and to use less strings.
//
// REFERENCES
// For more about ISO 8601 see:
// http://www.w3.org/TR/NOTE-datetime
// http://www.cl.cam.ac.uk/~mgk25/iso-time.html
//
// COPYRIGHT
// This script is Copyright  2000 JF Walker All Rights
// Reserved but may be freely used provided this colophon is
// included in full.
//
function isodatetime() {
    let today = new Date();
    let year = today.getFullYear();
    if (year < 2000) {
        // this should not be needed now
        // Y2K Fix, Isaac Powell
        year = year + 1900; // http://onyx.idbsu.edu/~ipowell
    }
    let month = today.getMonth() + 1;
    let day = today.getDate();
    let hour = today.getHours();
    let hourUTC = today.getUTCHours();
    let diff = hour - hourUTC;
    if (diff > 12) diff -= 24; // Fix the problem for town with real negative diff
    if (diff <= -12) diff += 24; // Fix the problem for town with real positive diff
    let hourdifference = Math.abs(diff);
    let minute = today.getMinutes();
    let minuteUTC = today.getUTCMinutes();
    let minutedifference;
    let second = today.getSeconds();
    let timezone;
    if (minute != minuteUTC && minuteUTC < 30 && diff < 0) {
        hourdifference--;
    }
    if (minute != minuteUTC && minuteUTC > 30 && diff > 0) {
        hourdifference--;
    }
    minutedifference = (minute != minuteUTC)? ":30" : ":00";
    timezone = `${diff < 0 ? "-" : "+"}${(hourdifference < 10)?"0":""}${hourdifference}${minutedifference}`;
    if (month <= 9) month = `0${month}`; //"0" + month;
    if (day <= 9) day = `0${day}`; //"0" + day;
    if (hour <= 9) hour = `0${hour}`; //"0" + hour;
    if (minute <= 9) minute = `0${minute}`; //"0" + minute;
    if (second <= 9) second = `0${second}`; //"0" + second;
    let time = `${year}-${month}-${day}T${hour}:${minute}:${second}${timezone}`;
    return time;
}

// (C) 2005 Victor R. Ruiz <victor*sixapart.com>
// Code to generate WSSE authentication header
//
// http://www.sixapart.com/pronet/docs/typepad_atom_api
//
// X-WSSE: UsernameToken Username="name", PasswordDigest="digest", Created="timestamp", Nonce="nonce"
//
//  * Username- The username that the user enters (the TypePad username).
//  * Nonce. A secure token generated anew for each HTTP request.
//  * Created. The ISO-8601 timestamp marking when Nonce was created.
//  * PasswordDigest. A SHA-1 digest of the Nonce, Created timestamp, and the password
//    that the user supplies, base64-encoded. In other words, this should be calculated
//    as: base64(sha1(Nonce . Created . Password))
//

function wsse(password_) {
    let passwordDigest, nonce, created;
    let r = new Array();

    //    Nonce = b64_sha1(isodatetime() + 'There is more than words');
    nonce = b64_sha1(`${isodatetime()}There is more than words`);
    let nonceEncoded = encode64(nonce);
    created = isodatetime();
    passwordDigest = b64_sha1(nonce + created + password_);

    r[0] = nonceEncoded;
    r[1] = created;
    r[2] = passwordDigest;
    return r;
}

function wsseHeader(username_, password_) {
    let w = wsse(password_);
    //    let header = 'UsernameToken Username="' + Username + '", PasswordDigest="' + w[2] + '", Created="' + w[1] + '", Nonce="' + w[0] + '"';
    let header = `UsernameToken Username="${username_}", PasswordDigest="${w[2]}", Created="${w[1]}", Nonce="${w[0]}"`;
    return header;
}

这里是测试代码函数:

/*
    Test function
 */
function testSoap() {
    let n1 = base64Decode(testData.nonce);
    let result = wsseTesting(testData.password, n1, testData.date);
    console.debug(`password digest: ${result[2]} equal: ${result[2]==testData.result}`);
    enter code here
}

希望这对其他人也有帮助。