使用 Javascript 的 atob 解码 base64 无法正确解码 utf-8 字符串

IT技术 javascript encoding utf-8
2021-02-07 17:01:07

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

6个回答

Unicode 问题

尽管 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

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

Base64 按照设计要求将二进制数据作为其输入。就 JavaScript 字符串而言,这意味着每个字符仅占一个字节的字符串。因此,如果您将一个字符串传递给 btoa() 包含占用超过一个字节的字符,您将得到一个错误,因为这不被视为二进制数据。

资料来源:MDN(2021)

最初的 MDN 文章还介绍了window.btoaand 的.atob破坏性,此后在现代 ECMAScript 中得到了修复。原始的,现已死亡的 MDN 文章解释说:

“Unicode 问题” 由于DOMStrings 是 16 位编码的字符串,因此在大多数浏览器中window.btoaCharacter Out Of Range exception如果字符超出 8 位字节(0x00~0xFF)的范围,则调用Unicode 字符串将导致 a


具有二进制互操作性的解决方案

(继续滚动以获取 ASCII base64 解决方案)

资料来源:MDN(2021)

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

编码 UTF8 ⇢ 二进制

// 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=="

解码二进制 ⇢ UTF-8

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"

这有点失败,是您会注意到编码的字符串EycgAOAAIABsAGEAIABtAG8AZABlAA==不再与先前解决方案的 string 匹配4pyTIMOgIGxhIG1vZGU=这是因为它是二进制编码的字符串,而不是 UTF-8 编码的字符串。如果这对您来说无关紧要(即,您没有从另一个系统转换以 UTF-8 表示的字符串),那么您就可以开始了。但是,如果您想保留 UTF-8 功能,最好使用下面描述的解决方案。


具有ASCII base64互操作性的解决方案

这个问题的整个历史显示了多年来我们不得不用多少种不同的方法来解决损坏的编码系统。尽管最初的 MDN 文章不再存在,但这个解决方案仍然可以说是更好的解决方案,并且在解决“Unicode 问题”方面做得很好,同时维护了可以在base64decode.org上解码的纯文本 base64 字符串

有两种可能的方法可以解决这个问题:

  • 第一个是转义整个字符串(使用 UTF-8,请参阅encodeURIComponent),然后对其进行编码;
  • 第二种是将 UTF-16DOMString转换为 UTF-8 字符数组,然后对其进行编码。

关于以前解决方案的说明:MDN 文章最初建议使用unescapeescape来解决Character Out Of Range异常问题,但它们已被弃用。这里的一些其他答案建议使用decodeURIComponentand解决这个问题encodeURIComponent,这已被证明是不可靠和不可预测的。此答案的最新更新使用现代 JavaScript 函数来提高速度和现代化代码。

如果你想节省一些时间,你也可以考虑使用一个库:

编码 UTF8 ⇢ base64

    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=="

解码 base64 ⇢ UTF8

    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,例如 when c == \nc.charCodeAt(0).toString(16)return a, forcea表示为0a)。


typescript支持

这是具有一些额外 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 时第一次遇到这个问题。为了让它在(移动)Safari 上正常工作,我实际上必须解码之前从 base64 源中去除所有空白这在 2021 年是否仍然相关,我不知道:

function b64_to_utf8( str ) {
    str = str.replace(/\s/g, '');    
    return decodeURIComponent(escape(window.atob( str )));
}
另一种解码方法decodeURIComponent(atob('4pyTIMOgIGxhIG1vZGU=').split('').map(x => '%' + x.charCodeAt(0).toString(16)).join('')) 不是性能最高的代码,但它就是这样。
2021-03-14 17:01:07
你救了我的日子,兄弟
2021-03-18 17:01:07
w3schools.com/jsref/jsref_unescape.asp “在 JavaScript 1.5 版中不推荐使用 unescape() 函数。请改用 decodeURI() 或 decodeURIComponent()。”
2021-04-09 17:01:07
更新: MDN 中的解决方案 #1 “Unicode 问题”已修复,b64DecodeUnicode('4pyTIMOgIGxhIG1vZGU=');现在可以正确输出“✓ à la 模式”
2021-04-10 17:01:07
return String.fromCharCode(parseInt(p1, 16)); 具有 TypeScript 兼容性。
2021-04-10 17:01:07

事情会改变的。逃逸/ UNESCAPE方法已被弃用。

您可以在对字符串进行 Base64 编码之前对其进行 URI 编码。请注意,这不会产生 Base64 编码的 UTF8,而是 Base64 编码的 URL 编码数据。双方必须就相同的编码达成一致。

请参阅此处的工作示例:http : //codepen.io/anon/pen/PZgbPW

// encode string
var base64 = window.btoa(encodeURIComponent('€ 你好 æøåÆØÅ'));
// decode string
var str = decodeURIComponent(window.atob(tmp));
// str is now === '€ 你好 æøåÆØÅ'

对于 OP 的问题,第三方库(例如js-base64)应该可以解决问题。

你是对的,我已经更新了文本以指出这一点。谢谢。另一种方法似乎是自己实现 base64,使用第三方库(例如 js-base64)或收到“错误:无法在 'Window' 上执行 'btoa':要编码的字符串包含超出 Latin1 范围的字符。 ”
2021-03-25 17:01:07
我想指出您不是在生成输入字符串的 base64,而是生成他的编码组件。所以如果你把它送走对方就无法将它解码为“base64”并得到原始字符串
2021-03-26 17:01:07

对我有用的完整文章:https : //developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding

我们从 Unicode/UTF-8 编码的部分是

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"

这是当今最常用的方法之一。

2021-04-01 17:01:07
对我有用,因为我正在尝试解码包含德语变音符号的 Github API 响应。谢谢!!
2021-04-04 17:01:07

如果将字符串视为字节更适合您,则可以使用以下函数

function u_atob(ascii) {
    return Uint8Array.from(atob(ascii), c => c.charCodeAt(0));
}

function u_btoa(buffer) {
    var binary = [];
    var bytes = new Uint8Array(buffer);
    for (var i = 0, il = bytes.byteLength; i < il; i++) {
        binary.push(String.fromCharCode(bytes[i]));
    }
    return btoa(binary.join(''));
}


// example, it works also with astral plane characters such as '𝒞'
var encodedString = new TextEncoder().encode('✓');
var base64String = u_btoa(encodedString);
console.log('✓' === new TextDecoder().decode(u_atob(base64String)))
如需更快、更跨浏览器的解决方案(但输出基本相同),请参阅stackoverflow.com/a/53433503/5601591
2021-03-14 17:01:07
谢谢。您的回答对于帮助我完成这项工作至关重要,这让我在多天的时间里花了很多时间。+1。stackoverflow.com/a/51814273/470749
2021-03-18 17:01:07
u_atob 和 u_btoa 使用自 IE10 (2012) 以来在每个浏览器中可用的函数,对我来说看起来很可靠(如果你指的是 TextEncoder,那只是一个例子)
2021-03-30 17:01:07

将 base64 解码为 UTF8 字符串

以下是@brandonscript 当前投票最多的答案

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

上面的代码可以工作,但速度很慢。如果您的输入是一个非常大的 base64 字符串,例如 base64 html 文档的 30,000 个字符。这将需要大量计算。

这是我的答案,使用内置 TextDecoder,对于大输入,比上述代码快近 10 倍。

function decodeBase64(base64) {
    const text = atob(base64);
    const length = text.length;
    const bytes = new Uint8Array(length);
    for (let i = 0; i < length; i++) {
        bytes[i] = text.charCodeAt(i);
    }
    const decoder = new TextDecoder(); // default is utf-8
    return decoder.decode(bytes);
}
这实际上是一个非常酷的解决方案。我认为它在过去是行不通的,因为 atob 和 btoa 已损坏,但现在不是。
2021-03-13 17:01:07