2-Base64編碼

編碼原理

Base64編碼就是把3個8位的二進制數據用4個ASCII可見字符展示出來。編碼時,將3個8位二進制碼重新分組成4個6位的二進制碼,不足6位的,右側補零,然后這4個6位的二進制碼高位補兩個0,形成4個8位的字節數據,然后取每個字節的十進制值在編碼表中對應的字符作為最終的編碼數據。Base64編碼后的數據長度是源數據長度的4/3。標準的Base64編碼要求最終的數據長度是4字節的整數倍,不足4字節的倍數時要用填充字符補齊,填充字符為等號“=”。編碼表如下

0 A     1 B     2 C     3 D     4 E     5 F     6 G     7 H    
 8 I     9 J    10 K    11 L    12 M    13 N    14 O    15 P    
16 Q    17 R    18 S    19 T    20 U    21 V    22 W    23 X    
24 Y    25 Z    26 a    27 b    28 c    29 d    30 e    31 f    
32 g    33 h    34 i    35 j    36 k    37 l    38 m    39 n    
40 o    41 p    42 q    43 r    44 s    45 t    46 u    47 v    
48 w    49 x    50 y    51 z    52 0    53 1    54 2    55 3    
56 4    57 5    58 6    59 7    60 8    61 9    62 +    63 /

例如ASCII碼A的Base64編碼過程為

字符:A
ASCII碼:65
二進制:  0100 0001
重新分組:010000 01
低位補零:010000 010000
高位補零:00010000 00010000 
轉十進制:16       16
對應字符:Q        Q
填充字符:Q        Q        =        =
最終結果:QQ==

代碼實現

使用Bouncy Castle實現

下面的代碼使用開源軟件Bouncy Castle實現Base64編解碼,使用的版本是1.56。

import java.io.UnsupportedEncodingException;
import org.bouncycastle.util.encoders.Base64;
public class Base64TestBC {
    public static void main(String[] args)
            throws UnsupportedEncodingException {
        // 編碼
        byte data[] = "A".getBytes();
        byte[] encodeData = Base64.encode(data);
        String encodeStr = Base64.toBase64String(data);
        System.out.println(new String(encodeData, "UTF-8"));
        System.out.println(encodeStr);
        // 解碼
        byte[] decodeData = Base64.decode(encodeData);
        byte[] decodeData2 = Base64.decode(encodeStr);
        System.out.println(new String(decodeData, "UTF-8"));
        System.out.println(new String(decodeData2, "UTF-8"));
    }
}

程序輸出為

QQ==
QQ==
A
A

使用Apache Commons Codec實現

下面的代碼使用開源軟件Apache Commons Codec實現Base64編解碼,使用的版本是1.10。

import java.io.UnsupportedEncodingException;
import org.apache.commons.codec.binary.Base64;
public class Base64TestCC {
    public static void main(String[] args)
            throws UnsupportedEncodingException {
        // 編碼
        byte data[] = "A".getBytes();
        byte[] encodeData = Base64.encodeBase64(data);
        String encodeStr = Base64.encodeBase64String(data);
        System.out.println(new String(encodeData, "UTF-8"));
        System.out.println(encodeStr);
        // 解碼
        byte[] decodeData = Base64.decodeBase64(encodeData);
        byte[] decodeData2 = Base64.decodeBase64(encodeStr);
        System.out.println(new String(decodeData, "UTF-8"));
        System.out.println(new String(decodeData2, "UTF-8"));
    }
}

源碼分析

Bouncy Castle實現源碼分析

Bouncy Castle實現Base64編解碼的方法和其實現Hex編解碼的方法類似,源碼是org.bouncycastle.util.encoders.Base64Encoder類,實現編碼時首先定義了一個編碼表和填充字符

protected final byte[] encodingTable =
{
    (byte)'A', (byte)'B', (byte)'C', (byte)'D', 
    (byte)'E', (byte)'F', (byte)'G', (byte)'H',
    (byte)'I', (byte)'J', (byte)'K', (byte)'L',
    (byte)'M', (byte)'N', (byte)'O', (byte)'P',
    (byte)'Q', (byte)'R', (byte)'S', (byte)'T', 
    (byte)'U', (byte)'V', (byte)'W', (byte)'X',
    (byte)'Y', (byte)'Z', (byte)'a', (byte)'b', 
    (byte)'c', (byte)'d', (byte)'e', (byte)'f', 
    (byte)'g', (byte)'h', (byte)'i', (byte)'j', 
    (byte)'k', (byte)'l', (byte)'m', (byte)'n',
    (byte)'o', (byte)'p', (byte)'q', (byte)'r', 
    (byte)'s', (byte)'t', (byte)'u', (byte)'v',
    (byte)'w', (byte)'x', (byte)'y', (byte)'z',
    (byte)'0', (byte)'1', (byte)'2', (byte)'3', 
    (byte)'4', (byte)'5', (byte)'6', (byte)'7',
    (byte)'8', (byte)'9', (byte)'+', (byte)'/'
}; 
protected byte    padding = (byte)'=';

然后編碼的代碼如下,首先依次處理連續的3字節的數據,因為連續的3個字節可以完整的轉換為4個字節的數據。最后處理末尾的字節,末尾的字節分為3種情況,如果是字節數正好是3的倍數,即末尾沒有多余的字節,不作處理。如果末尾剩余1個字節,那么需要補兩個填充字符,如果末尾有2個字節,那么需要補1個填充字符

public int encode(
    byte[]          data,
    int             off,
    int             length,
    OutputStream    out) 
    throws IOException
{
    int modulus = length % 3;
    int dataLength = (length - modulus);
    int a1, a2, a3;
    
    for (int i = off; i < off + dataLength; i += 3)
    {
        a1 = data[i] & 0xff;
        a2 = data[i + 1] & 0xff;
        a3 = data[i + 2] & 0xff;
        out.write(encodingTable[(a1 >>> 2) & 0x3f]);
        out.write(encodingTable[((a1 << 4) | (a2 >>> 4)) & 0x3f]);
        out.write(encodingTable[((a2 << 2) | (a3 >>> 6)) & 0x3f]);
        out.write(encodingTable[a3 & 0x3f]);
    }
    /*
     * process the tail end.
     */
    int    b1, b2, b3;
    int    d1, d2;
    switch (modulus)
    {
    case 0:        /* nothing left to do */
        break;
    case 1:
        d1 = data[off + dataLength] & 0xff;
        b1 = (d1 >>> 2) & 0x3f;
        b2 = (d1 << 4) & 0x3f;
        out.write(encodingTable[b1]);
        out.write(encodingTable[b2]);
        out.write(padding);
        out.write(padding);
        break;
    case 2:
        d1 = data[off + dataLength] & 0xff;
        d2 = data[off + dataLength + 1] & 0xff;
        b1 = (d1 >>> 2) & 0x3f;
        b2 = ((d1 << 4) | (d2 >>> 4)) & 0x3f;
        b3 = (d2 << 2) & 0x3f;
        out.write(encodingTable[b1]);
        out.write(encodingTable[b2]);
        out.write(encodingTable[b3]);
        out.write(padding);
        break;
    }
    return (dataLength / 3) * 4 + ((modulus == 0) ? 0 : 4);
}

解碼的方法同樣是首先構建解碼表,解碼表是一個128位數組,每個位置代表對應的ASCII碼,該位置上的值表示該ASCII碼在編碼表中的序號。具體到Base64的解碼表,每個編碼表上的可見字符,在解碼表中其ASCII碼對應的十進制位置上的值就是其編碼的序號,比如編碼表中數字0對應的字符是A,而A的ASCII碼是65,那么解碼表的第65個位置上的值就是0,其他的值都是-1。生成解碼表的源碼如下

protected final byte[] decodingTable = new byte[128];
protected void initialiseDecodingTable()
{
    for (int i = 0; i < decodingTable.length; i++)
    {
        decodingTable[i] = (byte)0xff;
    }
    
    for (int i = 0; i < encodingTable.length; i++)
    {
        decodingTable[encodingTable[i]] = (byte)i;
    }
}

解碼表實際上是這樣的(不可見字符統一用空白表示)

  -1      -1      -1      -1      -1      -1      -1      -1    
  -1      -1      -1      -1      -1      -1      -1      -1    
  -1      -1      -1      -1      -1      -1      -1      -1    
  -1      -1      -1      -1      -1      -1      -1      -1    
  -1    ! -1    " -1    # -1    $ -1    % -1    & -1    ' -1    
( -1    ) -1    * -1    + 62    , -1    - -1    . -1    / 63    
0 52    1 53    2 54    3 55    4 56    5 57    6 58    7 59    
8 60    9 61    : -1    ; -1    < -1    = -1    > -1    ? -1    
@ -1    A  0    B  1    C  2    D  3    E  4    F  5    G  6    
H  7    I  8    J  9    K 10    L 11    M 12    N 13    O 14    
P 15    Q 16    R 17    S 18    T 19    U 20    V 21    W 22    
X 23    Y 24    Z 25    [ -1    \ -1    ] -1    ^ -1    _ -1    
` -1    a 26    b 27    c 28    d 29    e 30    f 31    g 32    
h 33    i 34    j 35    k 36    l 37    m 38    n 39    o 40    
p 41    q 42    r 43    s 44    t 45    u 46    v 47    w 48    
x 49    y 50    z 51    { -1    | -1    } -1    ~ -1      -1

解碼的過程實際上就是獲取連續4個字符,取解碼表中對應的值,都去掉高兩位,則剩余24個二進制位,然后將這個24個二進制碼重組成3個字節作為解碼的輸出。對于最后的4個字符,要判斷是否有填充字符,如果有填充字符,則作相應的處理。源碼如下:

public int decode(
    byte[]          data,
    int             off,
    int             length,
    OutputStream    out)
    throws IOException
{
    byte    b1, b2, b3, b4;
    int     outLen = 0;
    
    int     end = off + length;
    
    while (end > off)
    {
        if (!ignore((char)data[end - 1]))
        {
            break;
        }
        
        end--;
    }
    
    int  i = off;
    int  finish = end - 4;
    
    i = nextI(data, i, finish);
    while (i < finish)
    {
        b1 = decodingTable[data[i++]];
        
        i = nextI(data, i, finish);
        
        b2 = decodingTable[data[i++]];
        
        i = nextI(data, i, finish);
        
        b3 = decodingTable[data[i++]];
        
        i = nextI(data, i, finish);
        
        b4 = decodingTable[data[i++]];
        if ((b1 | b2 | b3 | b4) < 0)
        {
            throw new IOException("invalid "
                    + "characters encountered in base64 data");
        }
        
        out.write((b1 << 2) | (b2 >> 4));
        out.write((b2 << 4) | (b3 >> 2));
        out.write((b3 << 6) | b4);
        
        outLen += 3;
        
        i = nextI(data, i, finish);
    }
    outLen += decodeLastBlock(out, (char)data[end - 4], 
            (char)data[end - 3], (char)data[end - 2], 
            (char)data[end - 1]);
    
    return outLen;
}
private boolean ignore(char c)
{
    return (c == '\n' || c =='\r' || c == '\t' || c == ' ');
}
private int nextI(byte[] data, int i, int finish)
{
    while ((i < finish) && ignore((char)data[i]))
    {
        i++;
    }
    return i;
}
private int decodeLastBlock(OutputStream out, char c1, 
        char c2, char c3, char c4) throws IOException
{
    byte    b1, b2, b3, b4;
    
    if (c3 == padding)
    {
        b1 = decodingTable[c1];
        b2 = decodingTable[c2];
        if ((b1 | b2) < 0)
        {
            throw new IOException("invalid characters "
                    + "encountered at end of base64 data");
        }
        out.write((b1 << 2) | (b2 >> 4));
        
        return 1;
    }
    else if (c4 == padding)
    {
        b1 = decodingTable[c1];
        b2 = decodingTable[c2];
        b3 = decodingTable[c3];
        if ((b1 | b2 | b3) < 0)
        {
            throw new IOException("invalid characters"
                    + " encountered at end of base64 data");
        }
        
        out.write((b1 << 2) | (b2 >> 4));
        out.write((b2 << 4) | (b3 >> 2));
        
        return 2;
    }
    else
    {
        b1 = decodingTable[c1];
        b2 = decodingTable[c2];
        b3 = decodingTable[c3];
        b4 = decodingTable[c4];
        if ((b1 | b2 | b3 | b4) < 0)
        {
            throw new IOException("invalid characters"
                    + " encountered at end of base64 data");
        }
        
        out.write((b1 << 2) | (b2 >> 4));
        out.write((b2 << 4) | (b3 >> 2));
        out.write((b3 << 6) | b4);
        
        return 3;
    } 
}

從代碼中可以看到,在解碼時會忽略首、尾、中間的空白。

Apache Commons Codec的實現

Apache Commons Codec的實現較復雜,該實現抽象出一個BaseNCodec抽象類用以同時支持Base32和Base64編解碼,Base64編解碼的實現類是org.apache.commons.codec.binary.Base64,編碼的實現也是定義了編碼表,由于Apache Commons Codec的Base64類同時支持UrlBase64編碼,所以定義了兩個編碼表,本文暫不分析這部分代碼。

Base64編碼的分塊

標準的Base64編碼要求每76個字符后面加回車換行符(\r\n),一行無論是否夠76個字符,末尾都要加回車換行。Bouncy Castle沒有實現該功能,而Apache Commons Codec實現了該功能。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容

  • 編碼問題一直困擾著開發人員,尤其在 Java 中更加明顯,因為 Java 是跨平臺語言,不同平臺之間編碼之間的切換...
    x360閱讀 2,496評論 1 20
  • Base64編碼由來 Base64最早是用來解決電子郵件的傳輸問題。 傳統的電子郵件是1982年定下技術規范的,詳...
    Ashton閱讀 2,601評論 0 6
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,810評論 18 139
  • 編碼原理 Hex編碼就是把一個8位的字節數據用兩個十六進制數展示出來,編碼時,將8位二進制碼重新分組成兩個4位的字...
    awesome丁閱讀 20,880評論 1 4
  • 為什么要編碼 不知道大家有沒有想過一個問題,那就是為什么要編碼?我們能不能不編碼?要回答這個問題必須要回到計算機是...
    艾小天兒閱讀 17,429評論 0 2