PHP 实现 Base64 加密算法

多看看外面的世界

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

PHP实现Base64加密算法

知识储备

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

友情链接

ASCII图

Base64作用

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

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

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

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字符(参考上图)

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的底层知识和其它编程知识。

PHP 实现 Base64 加密算法

本作品采用《CC 协议》,转载必须注明作者和本文链接
如果有不懂的地方,可以加我的qq:1174332406,或者是微信:itshardjs
本帖由系统于 11个月前 自动加精
Dennis_Ritchie
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 12
lmaster

非常棒

11个月前 评论

//剩余2个字节,需要补齐2位
$int_18 = ((ord($content[$loop 3]) << 8) | ord($content[$loop 3 + 1])) << 2;
你好,这段我没有看懂。想问下,要补齐至18位,第一个字节是不是应该左移10位,然后第二个字节左移2位?

11个月前 评论
Dennis_Ritchie (楼主) 11个月前
Dennis_Ritchie (楼主) 11个月前
attitude (作者) 11个月前
attitude (作者) 11个月前

学习了,感谢分享

11个月前 评论

请教下,上述理论是否与PHP函数库的base64_decodebase64_encode底层处理相同的

11个月前 评论
Dennis_Ritchie (楼主) 11个月前
Gibberish (作者) 11个月前
小李世界

老司机好高产,跟不上了

10个月前 评论
Galois 9个月前

写的真好,学习了 :clap:

10个月前 评论

不明白 一个六位 是怎么从ASCII 值 晃晃悠悠 变成 base64输出的 上面不是在写补0吗 这就算是补0了?

10个月前 评论
南城北岛

严格来说,这是编码/解码. 不是加密/解密 :stuck_out_tongue_closed_eyes:

9个月前 评论

请问下 1ASCII为什么是49 2是50

6个月前 评论

@wade 百度下ASCII 对照表 这就是王八的翘臀---规定啊

6个月前 评论
wade 6个月前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!