多看看外面的世界
對於現在很多的php程式設計師而言,絕大部分時間都是在做業務有關的程式碼,其它方面可能涉及的比較少,因此今天準備和大家講講不一樣的知識,Base64加密演算法,上午花了一點兒時間用PHP重新實現了一遍,因為之前使用c寫的,中間也出現了一些bug,但是很快修復了,程式碼我已經上傳到了碼雲php-base64-implemention,希望大家下載下來仔細的分析一哈。
知識儲備
如果對位操作不熟悉的讀者,建議先看一下這方面的內容,非常簡單,幾分鐘就可以了。
友情連結
Base64作用
base64的作用是把任意的字元序列轉換為只包含特殊字符集的序列,那麼base64加密之後的文字包含哪些字元呢?
- A-Z
- a-z
- 0-9
- +和/
上面總共包含64個字元,所以每個字元都使用6位來表示,下面有一張表,可以清晰的說明這個問題
這個是我在維基百科的截圖,舉個例子,對於Base64加密之後的字元A,對應的數值為0,二進位制表示就是000000,如果你現在不懂,沒關係,後面我會仔細的講解加密和解密的過程。
Base64加密
上面我已經提到了,每個Base64字元用6位來表示,但是一個位元組是8位,所以3個位元組剛好可以生成4個Base64字元,這應該很容易計算出來,下面我給大家舉個例子,假如說現在有個字串為"123",1的ASCII為49,那麼轉換為二進位制就是 00110001,2的ASCII為50,那麼轉換為二進位制就是00110010,3的ASCII為51,那麼轉換為二進位制就是00110011,這三個二進位制組合在一起就是這樣:001100010011001000110011
上面的二進位制位總共24位,從左到右依次取6位,對應關係如下:
- 第一個6位,001100,查閱上面的圖示,對應M
- 第二個6位,010011,同樣的操作,對應T
- 第三個6位,001000,對應I
- 第四個6位,110011,對應z
所以經過上面的分析,123轉換為Base64之後,就是MTIz,是不是很簡單?正常情況下都是很美好的,但是我們剛才的分析建立在加密之前的位元組數是3的倍數,那麼如果不是呢,比如剩下一個位元組,或者是2個,別急,下面來一一分析。
補齊
如果剩下一個位元組,那麼也就是說剩下8位,因為6位才能組合成一組啊,所以我們需要給它補上,補多少呢?只要4位就行了,12位剛好可以湊成2個Base64字元,那麼補什麼呢?很簡單,補0000就可以了,還是以上面的123為例,但是我們給它加上一個4,所以現在是“1234”,根據上面的分析,123剛好可以轉換為4個Base64字元,所以不管它,和上面的一模一樣,。現在我們只需要分析後面的4,4的ASCII為52,轉換為二進位制就是00110100,我們給它加上4個0,那麼結果就是001101000000,再對它進行6位分割,001101和000000,查表得到N和A,沒錯,這就是正確答案,但是為了後面的解碼,我們需要在加密後的字串末尾加上2個“=”,就是“MTIzNA==”。
如果剩下2個位元組的話,2個位元組剛好16位,6位一組的話,也就是說,少了2位,這樣就可以組合成18位了(3個Base64字元),這裡我們以字串“12”為例,1的ASCII轉換為二進位制是00110001,2的ASCII轉換為二進位制是00110010,我們將它組合在一起然後補齊之後(加上2個0),就是001100010011001000,按照6位一組進行分割,然後查表求得,結果是MTI,但是為了後面的解碼,我們需要在加密後的字串末尾加上1個“=”,就是“MTI=”。
Base64解密
有了加密的基礎,解密就很簡單了,以上面的加密結果為例 “MTIzNA==”,下面我們分別分析:
- 我們首先判斷字串末尾是否有“=”,如果沒有的話,那麼也就是說,原始字串沒有補位操作,按照4個Base64字元轉換為3個8位的位元組演算法就可以了,4個字元組合起來就是24位,按照8位一個位元組,就是三個位元組。
- 如果末尾有2個等於號“==”,也就是說之前進行了補位操作,通過上面加密的流程可以知道,原始位元組流中,剩餘1個位元組,補了4個0,得到了2個Base64字元,所以加密字串中,除了最後2個字元,其餘按照沒有補位的轉換操作就可以了,對於最後的2個Base64字元,我們把他們對應的二進位制位組合起來,然後再進行 右移 4位,就得到了一個8位的位元組。
- 如果末尾有一個等於號“=”,也就是說未加密之前,剩餘2個位元組,所以按照上面所說的,加密的時候,需要補齊2個0,這樣就形成了三個Base64字元,那麼除了最後的三個字元,其餘的按照正常的轉換就可以了,對於最後的三個Base64字元,我們把他們的二進位制位組合起來總共18位,然後右移2位,就得到了16位的2個位元組。
Base64解密的時候,需要查上面的表,進行反向操作,舉個例子,對於Base64字元M,查表得到它對應的6位二進位制位為001100,一定要謹記這一點。
Base64 程式碼實現
上面講解了Base64的加密和解密方法,說起來容易做起來難啊,在PHP裡面尤其如此
6位數字 轉換為Base64字元(參考上圖)
function normalToBase64Char($num)
{
if ($num >= 0 && $num <= 25) {
return chr(ord('A') + $num);
} else if ($num >= 26 && $num <= 51) {
return chr(ord('a') + ($num - 26));
} else if ($num >= 52 && $num <= 61) {
return chr(ord('0') + ($num - 52));
} else if ($num == 62) {
return '+';
} else {
return '/';
}
}
上面的程式碼就是截圖的PHP程式碼實現,這裡我提醒大家不要把Base64的a字元和ASCII的a字元混淆起來,兩種情況下存在著上圖的對映關係,再次提醒一下,這個函式傳入的是6位的資料。
Base64字元轉換為 6位數字
這個過程就是 6位數字 轉換為Base64字元的逆過程,程式碼如下:
function base64CharToInt($num)
{
if ($num >= 65 && $num <= 90) {
return ($num - 65);
} else if ($num >= 97 && $num <= 122) {
return ($num - 97) + 26;
} else if ($num >= 48 && $num <= 57) {
return ($num - 48) + 52;
} else if ($num == 43) {
return 62;
} else {
return 63;
}
}
對於任意一個Base64字元,我們首先要獲取到它對應的ASCII值,再根據這個值,通過上面的表的對映關係,求出它對應Base64數值,這個資料就是未加密資料的真實位元組資料。
加密程式碼實現
function encode($content)
{
$len = strlen($content);
$loop = intval($len / 3);//完整組合
$rest = $len % 3;//剩餘位元組數,需要補齊
$ret = "";
//首先計算完整組合
for ($i = 0; $i < $loop; $i++) {
$base_offset = 3 * $i;
//每三個位元組組合成一個無符號的24位的整數
$int_24 = (ord($content[$base_offset]) << 16)
| (ord($content[$base_offset + 1]) << 8)
| (ord($content[$base_offset + 2]) << 0);
//6位一組,每一組都進行Base64字串轉換
$ret .= self::normalToBase64Char($int_24 >> 18);
$ret .= self::normalToBase64Char(($int_24 >> 12) & 0x3f);
$ret .= self::normalToBase64Char(($int_24 >> 6) & 0x3f);
$ret .= self::normalToBase64Char($int_24 & 0x3f);
}
//需要補齊的情況
if ($rest == 0) {
return $ret;
} else if ($rest == 1) {
//剩餘1個位元組,此時需要補齊4位
$int_12 = ord($content[$loop * 3]) << 4;
$ret .= self::normalToBase64Char($int_12 >> 6);
$ret .= self::normalToBase64Char($int_12 & 0x3f);
$ret .= "==";
return $ret;
} else {
//剩餘2個位元組,需要補齊2位
$int_18 = ((ord($content[$loop * 3]) << 8) | ord($content[$loop * 3 + 1])) << 2;
$ret .= self::normalToBase64Char($int_18 >> 12);
$ret .= self::normalToBase64Char(($int_18 >> 6) & 0x3f);
$ret .= self::normalToBase64Char($int_18 & 0x3f);
$ret .= "=";
return $ret;
}
}
上面的程式碼和我之前分析的一模一樣。
解密程式碼實現
解密的過程複雜一點兒,但是隻要你看懂上面我所說的,肯定沒問題。
function decode($content)
{
$len = strlen($content);
if ($content[$len - 1] == '=' && $content[$len - 2] == '=') {
//說明加密的時候,剩餘1個位元組,補齊了4位,也就是左移了4位,所以除了最後包含的2個字元,前面的所有字元可以4個字元一組
$last_chars = substr($content, -4);
$full_chars = substr($content, 0, $len - 4);
$type = 1;
} else if ($content[$len - 1] == '=') {
//說明加密的時候,剩餘2個位元組,補齊了2位,也就是左移了2位,所以除了最後包含的3個字元,前面的所有字元可以4個字元一組
$last_chars = substr($content, -4);
$full_chars = substr($content, 0, $len - 4);
$type = 2;
} else {
$type = 3;
$full_chars = $content;
}
//首先處理完整的部分
$loop = strlen($full_chars) / 4;
$ret = "";
for ($i = 0; $i < $loop; $i++) {
$base_offset = 4 * $i;
$int_24 = (self::base64CharToInt(ord($full_chars[$base_offset])) << 18)
| (self::base64CharToInt(ord($full_chars[$base_offset + 1])) << 12)
| (self::base64CharToInt(ord($full_chars[$base_offset + 2])) << 6)
| (self::base64CharToInt(ord($full_chars[$base_offset + 3])) << 0);
$ret .= chr($int_24 >> 16);
$ret .= chr(($int_24 >> 8) & 0xff);
$ret .= chr($int_24 & 0xff);
}
//緊接著處理補齊的部分
if ($type == 1) {
$l_char = chr(((self::base64CharToInt(ord($last_chars[0])) << 6)
| (self::base64CharToInt(ord($last_chars[1])))) >> 4);
$ret .= $l_char;
} else if ($type == 2) {
$l_two_chars = ((self::base64CharToInt(ord($last_chars[0])) << 12)
| (self::base64CharToInt(ord($last_chars[1])) << 6)
| (self::base64CharToInt(ord($last_chars[2])) << 0)) >> 2;
$ret .= chr($l_two_chars >> 8);
$ret .= chr($l_two_chars & 0xff);
}
return $ret;
}
告誡
任何程式碼都不能缺少理論的支撐,所以在看程式碼前,請仔細的閱讀Base64的基本原理,一旦原理看懂了,閱讀程式碼就不是那麼難了,任何時候閱讀別人的程式碼,這都是應該謹記的地方,之前就已經告訴大家了,程式碼已經上傳到碼雲,php-base64-implemention,程式碼沒有問題,完全可以執行,如果有問題可以找我,博文的最後面有我的聯絡方式,祝您假期愉快。
交流學習
我建了一個qq群,大家平時可以交流學習,我也會給大家講解Laravel的底層知識和其它程式設計知識。