当前位置: 首页 > 知识库问答 >
问题:

使用JavaScript的atob解码base64不能正确解码utf-8字符串

桑鸿志
2023-03-14

我正在使用Javascriptwindow.atob()函数来解码一个base64编码的字符串(特别是GitHub API中base64编码的内容)。问题是我得到了ASCII编码的字符(像而不是)。如何正确处理传入的base64编码的流,以便将其解码为UTF-8?

共有1个答案

洪建茗
2023-03-14

尽管JavaScript(ECMAScript)已经成熟,但Base64、ASCII和Unicode编码的脆弱性已经引起了很多令人头疼的问题(其中大部分问题都是在这个问题的历史上出现的)。

请考虑以下示例:

const ok = "a";
console.log(ok.codePointAt(0).toString(16)); //   61: occupies < 1 byte

const notOK = "✓"
console.log(notOK.codePointAt(0).toString(16)); // 2713: occupies > 1 byte

console.log(btoa(ok));    // YQ==
console.log(btoa(notOK)); // error

为什么我们会遇到这种情况?

来源:MDN(2021)

最初的MDN文章还讨论了window.btoa.atob的损坏性质,它们后来在现代ECMAScript中得到了修补。MDN最初的文章解释道:

“Unicode问题”由于domString是16位编码的字符串,在大多数调用window.bto的浏览器中,如果某个字符超出了8位字节(0x00~0xFF)的范围,则会导致字符超出范围异常

(为ASCII base64解决方案保持滚动)

来源:MDN(2021)

MDN推荐的解决方案是实际对二进制字符串表示进行编码:

// convert a Unicode string to a string in which
// each 16-bit unit occupies only one byte
function toBinary(string) {
  const codeUnits = new Uint16Array(string.length);
  for (let i = 0; i < codeUnits.length; i++) {
    codeUnits[i] = string.charCodeAt(i);
  }
  return btoa(String.fromCharCode(...new Uint8Array(codeUnits.buffer)));
}

// a string that contains characters occupying > 1 byte
let encoded = toBinary("✓ à la mode") // "EycgAOAAIABsAGEAIABtAG8AZABlAA=="
function fromBinary(encoded) {
  binary = atob(encoded)
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return String.fromCharCode(...new Uint16Array(bytes.buffer));
}

// our previous Base64-encoded string
let decoded = fromBinary(encoded) // "✓ à la mode"
  • 第一种方法是转义整个字符串(使用UTF-8,请参阅EncodeURIComponent),然后对其进行编码;
  • 第二种方法是将UTF-16domString转换为UTF-8字符数组,然后对其进行编码。

关于以前的解决方案的注意事项:MDN文章最初建议使用unescapeescape来解决字符超出范围异常问题,但后来已不推荐使用。这里的一些其他答案建议使用decodeuricomponentencodeuricomponent来解决这个问题,但事实证明这是不可靠和不可预测的。这个答案的最新更新使用了现代JavaScript函数来提高速度和更新代码。

如果您想节省一些时间,还可以考虑使用库:

  • JS-Base64(NPM,适合Node.js)
  • base64-js
    function b64EncodeUnicode(str) {
        // first we use encodeURIComponent to get percent-encoded UTF-8,
        // then we convert the percent encodings into raw bytes which
        // can be fed into btoa.
        return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
            function toSolidBytes(match, p1) {
                return String.fromCharCode('0x' + p1);
        }));
    }
    
    b64EncodeUnicode('✓ à la mode'); // "4pyTIMOgIGxhIG1vZGU="
    b64EncodeUnicode('\n'); // "Cg=="
    function b64DecodeUnicode(str) {
        // Going backwards: from bytestream, to percent-encoding, to original string.
        return decodeURIComponent(atob(str).split('').map(function(c) {
            return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
        }).join(''));
    }
    
    b64DecodeUnicode('4pyTIMOgIGxhIG1vZGU='); // "✓ à la mode"
    b64DecodeUnicode('Cg=='); // "\n"

(为什么需要这样做?('00'+C.charcodeat(0).ToString(16)).slice(-2)将0前置到单个字符串,例如,当C==\n时,C.charcodeat(0).ToString(16)返回a,迫使a表示为0a)。

这里有一个相同的解决方案,带有一些额外的TypeScript兼容性(通过@ma-maddin):

// Encoding UTF8 ⇢ base64

function b64EncodeUnicode(str) {
    return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
        return String.fromCharCode(parseInt(p1, 16))
    }))
}

// Decoding base64 ⇢ UTF8

function b64DecodeUnicode(str) {
    return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
    }).join(''))
}

这使用了escapeunescape(现在已不推荐使用,但在所有现代浏览器中仍然有效):

function utf8_to_b64( str ) {
    return window.btoa(unescape(encodeURIComponent( str )));
}

function b64_to_utf8( str ) {
    return decodeURIComponent(escape(window.atob( str )));
}

// Usage:
utf8_to_b64('✓ à la mode'); // "4pyTIMOgIGxhIG1vZGU="
b64_to_utf8('4pyTIMOgIGxhIG1vZGU='); // "✓ à la mode"

还有最后一件事:我在调用GitHub API时第一次遇到这个问题。为了让它在(Mobile)Safari上正常工作,我实际上必须在解码base64源代码之前从源代码中去掉所有空白。这在2021年是否仍然相关,我不知道:

function b64_to_utf8( str ) {
    str = str.replace(/\s/g, '');    
    return decodeURIComponent(escape(window.atob( str )));
}
 类似资料:
  • 问题内容: 我正在使用Javascript 函数解码base64编码的字符串(特别是来自GitHubAPI的base64编码的内容)。问题是我回来了ASCII编码的字符(而不是)。如何正确处理传入的以base64编码的流,以便将其解码为utf-8? 问题答案: 此问题: “ Unicode问题”由于s是16位编码的字符串,因此在大多数浏览器中,如果字符超出8位字节的范围(0x00〜0xFF),则调

  • 我正在接收一个zip文件的内容(从一个API)作为一个base64编码的字符串。 如果我将该字符串粘贴到Notepad++中并执行 插件>MIME工具>Base64解码 生成类似的内容,但某些字符的解码方式不同(因此成为无效的zip文件)。其他方法抛出无效的URI错误。 如何在JavaScript中再现Notepad++行为?

  • 问题内容: 如何使用Android解码utf-8字符串?我尝试使用此命令,但输出与输入相同: 问题答案: 字符串不需要编码。它只是一个Unicode字符序列。 要将字符串转换为字节序列时需要进行 编码 。您选择的字符集(UTF-8,cp1255等)确定了Character-> Byte映射。请注意,字符不必转换为单个字节。在大多数字符集中,大多数Unicode字符都转换为至少两个字节。 字符串的编

  • 问题内容: 我在AngularJS上建立的SPA中有一个文本输入框,供用户向打印输出中添加标题。输入框的声明如下: 文本框中填充了服务器提供的默认标题。用户可以将标题更改为适合他们的名称。更改标题后,服务器将更新并在响应的标题中发送回新标题,然后替换框中的标题。这非常适合标准ASCII类型的字符。 但是,对于unicode字符(例如àßéçøö),它不起作用。文本已正确发送,在服务器上正确更新并正

  • 我需要解码成PDF文件的Base64字符串。我使用这个代码。但是window.atob命令总是报告错误:在窗口上执行“atob”失败:要解码的字符串没有正确编码。 我知道该文件是正确的,因为我已经使用一个将base64解码为pdf的网站对其进行了解码。我不知道它是否有用,但我们正在使用Aurelia框架。 转换函数 函数的调用

  • 问题内容: 我正在运行一个Python程序,该程序可获取UTF-8编码的网页,并使用BeautifulSoup从HTML中提取一些文本。 但是,当我将此文本写入文件(或在控制台上打印)时,它会以意外的编码方式写入。 示例程序: 运行此结果: 但是我希望Python Unicode字符串在单词中呈现为: 我已经试过了“fromEncoding”参数传递给BeautifulSoup,并试图与该对象,但