• 記一個 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下载贵州快三开奖 丰都县 | 观塘区 | 兴化市 | 庆城县 | 双辽市 | 栾川县 | 昌吉市 | 仁化县 | 丰都县 | 六枝特区 | 丰宁 | 苗栗县 | 竹北市 | 玉龙 | 永胜县 | 宜川县 | 时尚 | 高台县 | 合江县 | 休宁县 | 崇礼县 | 南川市 | 繁峙县 | 孟州市 | 墨脱县 | 翁牛特旗 | 衡阳县 | 玉门市 | 房产 | 侯马市 | 五河县 | 南平市 | 彭州市 | 腾冲县 | 黔西县 | 巴中市 | 安化县 | 霞浦县 | 盖州市 | 和硕县 | 溆浦县 | 灯塔市 | 富平县 | 广东省 | 清流县 | 冀州市 | 西畴县 | 余干县 | 巴楚县 | 夏津县 | 青海省 | 巴彦县 | 邵阳市 | 时尚 | 锦州市 | 珠海市 | 温宿县 | 阜城县 | 怀宁县 | 海南省 | 琼结县 | 临夏县 | 崇信县 | 黄陵县 | 会宁县 | 通州区 | 隆子县 | 云阳县 | 宁都县 | 沙河市 | 安多县 | 吴川市 | 兰西县 | 惠水县 | 七台河市 | 华宁县 | 界首市 | 大邑县 | 阿克苏市 | 萍乡市 | 乐至县 | 肥乡县 | 佳木斯市 | 即墨市 | 常宁市 | 蒙山县 | 桂林市 | 柳州市 | 河源市 | 兰考县 | 柘荣县 | 芜湖市 | 绍兴县 | 牡丹江市 | 冀州市 | 哈密市 | 三门峡市 | 东至县 | 廊坊市 | 定日县 | 嵊泗县 | 仁布县 | 珲春市 | 万州区 | 汽车 | 蚌埠市 | 华蓥市 | 霍州市 | 菏泽市 | 花垣县 | 宣威市 | 靖远县 | 丰台区 | 临城县 | 阿克苏市 | 高碑店市 | 葫芦岛市 | 新民市 | 台南县 | 札达县 | 揭东县 | 武陟县 | 大安市 | 读书 | 蕉岭县 | 来安县 | 扶风县 | 万载县 | 山阳县 | 方正县 | 河池市 | 交口县 | 安化县 | 凤翔县 | 肥乡县 | 高青县 | 汝城县 | 台江县 | 陇西县 | 耿马 | 台南县 | 乐亭县 | 山阴县 | 青海省 | 城口县 | 孟州市 | 福鼎市 | 密云县 | 忻城县 | 东海县 | 当雄县 | 南阳市 | 阳曲县 | 蕉岭县 | 玉田县 | 西平县 | 靖远县 | 武宁县 | 永平县 | 怀来县 | 莒南县 | 玉树县 | 突泉县 | 海阳市 | 建始县 | 乐山市 | 惠水县 | 定日县 | 久治县 | 山阳县 | 乡城县 | 封丘县 | 怀宁县 | 正定县 | 镇远县 | 政和县 | 集安市 | 扎囊县 | 浙江省 | 浪卡子县 | 上高县 | 嘉峪关市 | 寿宁县 | 尼玛县 | 沧州市 | 德安县 | 隆安县 | 梁河县 | 中西区 | 晋州市 | 曲沃县 | 阜宁县 | 金门县 | 汝州市 | 乐平市 | 准格尔旗 | 乾安县 | 绥棱县 | 报价 | 镇康县 | 北碚区 | 宁津县 | 晋州市 | 紫金县 | 宁晋县 | 上饶县 | 雅江县 |