• 記一個 Base64 有關的 Bug

    本文原計劃寫兩部分內容,第一是記錄最近遇到的與 Base64 有關的 Bug,第二是 Base64 編碼的原理詳解。結果寫了一半發現,誒?不復雜的一個事兒怎么也要講這么長?不利于閱讀和理解啊(其實是今天有點懶想去休閑娛樂會兒),所以 Base64 編碼的原理詳解的部分將在下一篇帶來,敬請關注。

    0x01 遇到的現象

    A 向 B 提供了一個接口,約定接口參數 Base64 編碼后傳遞。

    但 A 對 B 傳遞的參數進行 Base64 解碼時報錯了:

    Illegal base64 character a

    0x02 原因分析

    搜索后發現這是一個好多網友們都踩過的坑,簡而言之就一句話:Base64 編/解碼器有不同實現,有的不相互兼容。

    比如我上面遇到的現象,可以使用下面這段代碼完整模擬復現:

    package org.mazhuang.base64test;
    
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.util.Base64Utils;
    import sun.misc.BASE64Encoder;
    
    @SpringBootApplication
    public class Base64testApplication implements CommandLineRunner {
        @Override
        public void run(String... args) throws Exception {
            byte[] content = "It takes a strong man to save himself, and a great man to save another.".getBytes();
            String encrypted = new BASE64Encoder().encode(content);
            byte[] decrypted = Base64Utils.decodeFromString(encrypted);
            System.out.println(new String(decrypted));
        }
    
        public static void main(String[] args) {
            SpringApplication.run(Base64testApplication.class, args);
        }
    
    }

    以上代碼執行會報異常:

    Caused by: java.lang.IllegalArgumentException: Illegal base64 character a
        at java.util.Base64$Decoder.decode0(Base64.java:714) ~[na:1.8.0_202-release]
        at java.util.Base64$Decoder.decode(Base64.java:526) ~[na:1.8.0_202-release]

    注: 測試代碼里的那個字符串如果很短,比如「Hello, World」這種,可以正常解碼。

    也就是說,用 sun.misc.BASE64Encoder 編碼,用 org.springframework.util.Base64Utils 進行解碼,是有問題的,我們可以用它倆分別對以上符串進行編碼,然后輸出看看差異。測試代碼:

    byte[] content = "It takes a strong man to save himself, and a great man to save another.".getBytes();
    
    System.out.println(new BASE64Encoder().encode(content));
    System.out.println("--- 華麗的分隔線 ---");
    System.out.println(Base64Utils.encodeToString(content));

    輸出:

    SXQgdGFrZXMgYSBzdHJvbmcgbWFuIHRvIHNhdmUgaGltc2VsZiwgYW5kIGEgZ3JlYXQgbWFuIHRv
    IHNhdmUgYW5vdGhlci4=
    --- 華麗的分隔線 ---
    SXQgdGFrZXMgYSBzdHJvbmcgbWFuIHRvIHNhdmUgaGltc2VsZiwgYW5kIGEgZ3JlYXQgbWFuIHRvIHNhdmUgYW5vdGhlci4=

    可以看到 sun.misc.BASE64Encoder 編碼后的內容換行了,而換行符的 ASCII 編碼正好是 0x0a,如此貌似解釋得通了。讓我們進一步跟蹤一下,找一下出現這種差異的源頭。

    0x03 更進一步

    在 IDEA 里按住 CTRL 或 COMMAND 鍵點擊方法名,可以跳轉到它們的實現。

    3.1 sun.misc.BASE64Encoder.encode

    這種寫法主要涉及到兩個類,sun.misc 包下的 BASE64Encoder 和 CharacterEncoder,其中后者是前者的父類。

    它實際工作的 encode 方法是在 CharacterEncoder 文件里,帶注釋版如下:

    
    public void encode(InputStream inStream, OutputStream outStream)
        throws IOException {
        int     j;
        int     numBytes;
        // bytesPerLine 在 BASE64Encoder 里實現,返回 57
        byte    tmpbuffer[] = new byte[bytesPerLine()];
    
        // 用 outStream 構造一個 PrintStream
        encodeBufferPrefix(outStream);
    
        while (true) {
            // 讀取最多 57 個 bytes
            numBytes = readFully(inStream, tmpbuffer);
            if (numBytes == 0) {
                break;
            }
            // 啥也沒干
            encodeLinePrefix(outStream, numBytes);
            // 每次處理 3 bytes,編碼成 4 bytes,不足位的補 0 位和 '='
            for (j = 0; j < numBytes; j += bytesPerAtom()) {
                // ...
            }
            if (numBytes < bytesPerLine()) {
                break;
            } else {
                // 換行
                encodeLineSuffix(outStream);
            }
        }
        // 啥也沒干
        encodeBufferSuffix(outStream);
    }

    然后在 CharacterEncoder 類的注釋里我們可以看到編碼后的格式:

    [Buffer Prefix]
    [Line Prefix][encoded data atoms][Line Suffix]
    [Buffer Suffix]

    而結合 BASE64Encoder 這個實現類來看,Buffer Prefix、Buffer Suffix 和 Line Prefix 都為空,Line Suffix 為 \n

    至此,我們已經找到實現中換行的部分——這個編碼器實現里,讀取 57 個 byte 作為一行進行編碼(編碼完成后是 76 個 byte)。

    3.2 org.springframework.util.Base64Utils.encodeToString

    這種寫法主要涉及到 org.springframework.util.Base64Utils 和 java.util.Base64 兩個類,可以看到前者主要是后者的封裝。

    Base64Utils.encodeToString 這種寫法最終用到的是 Base64.Encoder.RFC4648 這種編碼器:

    // isURL = false,newline = null,linemax = -1,doPadding = true
    static final Encoder RFC4648 = new Encoder(false, null, -1, true);

    留意 newline 和 linemax 的值。

    然后看實際的編碼實現所在的 Base64.encode0 方法:

    private int encode0(byte[] src, int off, int end, byte[] dst) {
        // ...
        while (sp < sl) {
            // ...
    
            // 這個條件不會滿足,不會加換行
            if (dlen == linemax && sp < end) {
                for (byte b : newline){
                    dst[dp++] = b;
                }
            }
        }
        // ...
        return dp;
    }

    所以……這個實現里沒有換行。

    0x04 小結

    經過以上的分析,真相已經大白了,就是兩個編碼器的實現不一樣,我們在開發過程中注意使用匹配的編碼解碼器就 OK 了,就是用哪個 Java 包下面的編碼器編碼,就用相同包下的對應解碼器解碼。

    至于為啥會出現不一樣的實現,它們之間有過什么來龍去脈、恩怨情仇,Base64 的詳細原理等等,就厚著老臉,邀請大家且聽下回分解吧!:-P


    假如你對我的文章感興趣,可以關注我的微信公眾號『悶騷的程序員』隨時閱讀更多內容。

    posted on 2020-03-01 18:10  朩頭亼の儛  閱讀(...)  評論(...編輯  收藏

    導航

    統計

    贵州快三平台贵州快三主页贵州快三网站贵州快三官网贵州快三娱乐贵州快三开户贵州快三注册贵州快三是真的吗贵州快三登入贵州快三快三贵州快三时时彩贵州快三手机app下载贵州快三开奖 濮阳市 | 玉林市 | 石狮市 | 涡阳县 | 抚州市 | 承德市 | 商洛市 | 团风县 | 和顺县 | 兴义市 | 黄山市 | 且末县 | 平潭县 | 惠州市 | 志丹县 | 朝阳区 | 伊金霍洛旗 | 麻江县 | 沙雅县 | 大港区 | 沧州市 | 襄垣县 | 突泉县 | 孝义市 | 江城 | 重庆市 | 辰溪县 | 巴彦县 | 应用必备 | 社会 | 应城市 | 陈巴尔虎旗 | 合阳县 | 汶上县 | 新竹县 | 饶平县 | 元氏县 | 阜阳市 | 桐梓县 | 阳原县 | 霞浦县 | 武冈市 | 札达县 | 景谷 | 板桥市 | 木里 | 双柏县 | 晴隆县 | 宜宾县 | 红安县 | 临泉县 | 开原市 | 页游 | 虞城县 | 启东市 | 马公市 | 德昌县 | 景德镇市 | 遵化市 | 通山县 | 清原 | 东乌珠穆沁旗 | 那曲县 | 嵊泗县 | 沾益县 | 乐亭县 | 来安县 | 常宁市 | 凤翔县 | 澜沧 | 囊谦县 | 保定市 | 永年县 | 闵行区 | 庐江县 | 温州市 | 青浦区 | 达拉特旗 | 苏尼特左旗 | 固安县 | 谢通门县 | 武宣县 | 宁乡县 | 二连浩特市 | 泽普县 | 义马市 | 正定县 | 周口市 | 古浪县 | 诏安县 | 昭觉县 | 精河县 | 盐山县 | 鸡东县 | 诏安县 | 长垣县 | 宝丰县 | 邹平县 | 沂南县 | 肥西县 | 翁牛特旗 | 漠河县 | 阳城县 | 疏勒县 | 息烽县 | 潢川县 | 都昌县 | 吉林省 | 阿图什市 | 犍为县 | 无为县 | 湟源县 | 文成县 | 江油市 | 印江 | 闸北区 | 双流县 | 北京市 | 屏南县 | 从江县 | 临城县 | 普陀区 | 衡东县 | 楚雄市 | 清涧县 | 若羌县 | 收藏 | 聂拉木县 | 类乌齐县 | 普定县 | 米易县 | 宜都市 | 莆田市 | 汽车 | 临高县 | 惠安县 | 民和 | 泊头市 | 黑水县 | 临江市 | 丹寨县 | 元氏县 | 梁山县 | 长宁县 | 南投市 | 稷山县 | 新安县 | 德保县 | 南木林县 | 高雄市 | 朔州市 | 寿阳县 | 潞城市 | 晋州市 | 务川 | 南部县 | 隆安县 | 潍坊市 | 曲阜市 | 明水县 | 道真 | 景德镇市 | 繁峙县 | 陆川县 | 双江 | 明水县 | 大竹县 | 山阴县 | 博罗县 | 德令哈市 | 泰和县 | 甘洛县 | 宕昌县 | 句容市 | 抚州市 | 鹤峰县 | 从化市 | 罗定市 | 云阳县 | 河源市 | 舞钢市 | 永康市 | 克东县 | 额尔古纳市 | 故城县 | 乌什县 | 临澧县 | 林甸县 | 疏勒县 | 余干县 | 延庆县 | 江陵县 | 乌鲁木齐市 | 沾化县 | 白朗县 | 河间市 | 铜鼓县 | 河池市 | 尉犁县 | 澳门 | 徐水县 | 明溪县 | 竹山县 | 桓台县 | 阿拉善左旗 | 诸暨市 | 博客 |