作者:Dennis_Ritchie 原文地址:https://learnku.com/articles/36655

多看看外面的世界

对于现在很多的php程序员而言,绝大部分时间都是在做业务有关的代码,其它方面可能涉及的比较少,因此今天准备和大家讲讲不一样的知识,Base64加密算法,上午花了一点儿时间用PHP重新实现了一遍,因为之前使用c写的,中间也出现了一些bug,但是很快修复了,代码我已经上传到了码云php-base64-implemention,希望大家下载下来仔细的分析一哈。

PHP实现Base64加密算法
PHP实现Base64加密算法

知识储备

如果对位操作不熟悉的读者,建议先看一下这方面的内容,非常简单,几分钟就可以了。

友情链接

ASCII图

Base64作用

base64的作用是把任意的字符序列转换为只包含特殊字符集的序列,那么base64加密之后的文本包含哪些字符呢?

  • A-Z
  • a-z
  • 0-9
  • +和/

上面总共包含64个字符,所以每个字符都使用6位来表示,下面有一张表,可以清晰的说明这个问题

PHP实现Base64加密算法
PHP实现Base64加密算法

这个是我在维基百科的截图,举个例子,对于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字符(参考上图)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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字符的逆过程,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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数值,这个数据就是未加密数据的真实字节数据。

加密代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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;
}
}

上面的代码和我之前分析的一模一样。

解密代码实现

解密的过程复杂一点儿,但是只要你看懂上面我所说的,肯定没问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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的底层知识和其它编程知识。

PHP 实现 Base64 加密算法
PHP 实现 Base64 加密算法