万字总结PHP与JavaScript、PHP与PHP 实现开箱即用的AES、RSA和较为安全的自定义加解密算法

PHP教程 2025-09-06

实操(下方有理论)

没有绝对安全的系统

  • 对于前后端通信安全的声明:对于前端,网页代码是暴漏给用户的,用户可以任意摆布,可以反编译混淆过的代码,也可以调试、研究代码去攻破,因此不能保证百分百的安全,能做的仅仅是让攻击者增加攻击成本。
  • 明文硬编码漏洞:如果是硬编码,把key,secret等明文暴漏,写死在代码中,这是很不安全的行为,代码审查即可攻破,(世界是个巨大的草台班子,这种行为并不少)。
  • 调用漏洞:即使加密的逻辑代码被加密,秘钥被加密,可是其它模块的代码是明文的(此地无银三百两)(例如decrypt(response_data)),那么攻击者甚至不用调试加密代码,直接调用明文函数就可绕过。
  • 硬破解:开发者再怎么加密或混淆代码,但攻击者就像破案一样,结合Console控制台,一点一点耐心还原执行逻辑。
  • 通信获取:有些秘钥key,secret,是通过接口获取的,但是获取这些配置的过程,也不安全,可以被获取。
  • 对于前后端通信:能做的就是混淆所有代码(html加密混淆方式自行谷歌,有在线的和一些npm插件);不要硬编码;勤更换秘钥key,Secret,最好使用随机较长的。服务端在不影响业务的前提及时作废老数据验证策略。

服务端与客户端(AES对称加密)

  • 注意:
    • AES有两个非常重要的参数:key和vi,若攻击者知道key与vi,等同于知道了钥匙。
    • key:加密算法的核心,用于确保只有持有相同密钥的人才能解密数据,推荐随机生成。
    • vi:一个随机或伪随机的值,用于在加密过程中初始化加密算法的状态,确保使用相同密钥加密的相同明文会产生不同的密文。
  • PHP加密,JavaScript解密(PHP需要开启openssl扩展)
/** * @function PHP使用AES 256加密字符串数据 * @param    $str string 被加密的字符串 * @param    $key string 256位自定义key,是加密算法的核心,用于确保只有持有相同密钥的人才能解密数据,推荐随机生成32个随机字符,确保不可预测 * @param    $iv  string 128位初始化向量,是一个随机或伪随机的值,用于在加密过程中初始化加密算法的状态,确保使用相同密钥加密的相同明文会产生不同的密文 * @return string */function aesEncrypt($str, $key, $iv) {    return base64_encode(openssl_encrypt($str, 'AES-256-CBC', $key, true, $iv));}$str = '{"code":0,"msg":"success","data":{"lists":{"k1":"v1","k2":"v2"}}}';$key = 'abcdabcdabcdabcdabcdabcdabcdabcd';$iv  = 'abcdabcdabcdabcd';echo aesEncrypt($str, $key, $iv);
  • JavaScript加密,PHP解密
/** * @function PHP使用AES 256解密字符串数据 * @param    $str string 被解密的字符串 * @param    $key string 256位自定义key,是加密算法的核心,用于确保只有持有相同密钥的人才能解密数据,推荐随机生成32个随机字符,确保不可预测 * @param    $iv  string 128位初始化向量,是一个随机或伪随机的值,用于在加密过程中初始化加密算法的状态,确保使用相同密钥加密的相同明文会产生不同的密文 * @return string */function aesDecrypt($str, $key, $iv) {    return base64_decode(base64_encode(openssl_decrypt(base64_decode($str), 'AES-256-CBC', $key, true, $iv)));}$str = 'aZlC1imJbcUQblRR0QsMPGF+8Kbja/1fU+tdcTyAUVw=';$key = 'abcdabcdabcdabcdabcdabcdabcdabcd';$iv  = 'abcdabcdabcdabcd';echo aesDecrypt($str, $key, $iv);
  • 前端针对key和vi的生成与通信问题不完美的解决方案,怎么选是当前业务的能容忍的安全底线,与开发便捷度的权衡。
    • 若是MVC架构:可在后端实时赋值给JavaScript,存入js内存,加载一次页面变一次秘钥。
    • 若求便捷:把代码写死后混淆,避免明文泄露。或者每次通信都明文传输随机key和和随机vi,结合密文一起传送。
    • 若求安全:服务端写程序替换,或手动更换JavaScript脚本文件中的key和vi值,或根据用户的会话字符串(不要直接存会话中),前后端根据回话字符串利用相同的算法计算出新的key和vi值。

服务端与客户端(较为安全的自定义对称加密算法,避免AES算法的 key和vi在客户端存储问题)

  • 感受一下“Hello”的密文(安全等级绝对比不上主流加密算法)
    $2a34912a90c3cf9bb1ec6aa64351f8ef56ae5726697dcefb07f3a73b09e789545f1295726714ed66d3b04e7cf696fc32
  • PHP端加解密类
 $salt, 'length' => $length];    }    /**     * @function 计算偏移量 加密用     * @param    $v int 明文每个字节的ASCII编码值     * @param    $s int 自定义算法取余后的值     * @return   int     */    private static function calcOffsetEn($v, $s) {        $r = 255 - $v;        if ($s > $r) {            return $s - $r - 1;        }        return $v + $s;    }    /**     * @function 计算偏移量 解密用     * @param    $v int 密文每个字节的ASCII编码值     * @param    $s int 自定义算法取余后的值     * @return   int     */    private static function calcOffsetDe($v, $s) {        if ($v >= $s) {            return $v - $s;        }        return 255 - ($s - $v) + 1;    }    /**     * @function 明文和盐混淆成密文     * @param    $data string 要被加密的明文     * @param    $salt array  盐     * @return   string       加密后的数据     */    private static function encryptWithSalt($data, $salt) {        $result   = '';        //获取明文有几个单字节的长度,此处不要用mb_strlen        $data_len = strlen($data);        for ($i = 0; $i < $data_len; $i ++) {            //逐个字节混淆 字符串中每个字节的ASCII值 与            $result .= chr(static::calcOffsetEn(ord($data[$i]), ord($salt['salt'][$i % $salt['length']])));        }        return $result;    }    /**     * @function 密文解密     * @param    $data string 要被解密的密文     * @param    $salt array   盐     * @return   string     */    private static function decryptWithSalt($data, $salt) {        $result = '';        $data_len = strlen($data);        for ($i = 0; $i < $data_len; $i++) {            // 去盐处理并转换为字符            $result .= chr(static::calcOffsetDe(ord($data[$i]), ord($salt['salt'][$i % $salt['length']])));        }        return $result;    }    /**     * @function 加密数据     * @param    $data string 要被加密的字符串     * @return   string     */    public static function encrypt($data) {        if ($data == '') {            return '';        }        // 生成随机盐        $salt = static::generateRandStr();        // 将盐的长度、盐本身、密文        return static::$prefix . bin2hex(chr($salt['length']) . $salt['salt'] . static::encryptWithSalt(mb_convert_encoding($data, 'UTF-8', 'UTF-8'), $salt));    }    /**     * @function 解密数据     * @param    $data string 要被加密的字符串     * @return   string     */    public static function decrypt($data) {        if ($data == '') {            return '';        }        // 检查是否是加密字符串,通过标签判断        if ($data[0] != static::$prefix) {            return '';        }        // 去掉标签并从16进制字符串转换回二进制数据        $clear_data = hex2bin(substr($data, strlen(static::$prefix)));        // 获取盐的长度        $salt_len = ord($clear_data[0]);        // 将解密数据转换为UTF-8编码的字符串        return mb_convert_encoding(static::decryptWithSalt(substr($clear_data, 1 + $salt_len), ['salt' => substr($clear_data, 1, $salt_len), 'length' => $salt_len]), 'UTF-8', 'UTF-8');    }}
  • JavaScript端加解密类
class Crypt {    // 声明密文前缀    static prefix = '$';    /**     * 生成ASCII字符随机盐     * @returns {Object} {salt, length}     */    static generateRandStr() {        const length = Math.floor(Math.random() * 255) + 1;  // 随机盐的长度在1到255之间        let salt = '';        for (let i = 0; i < length; i++) {            salt += String.fromCharCode(Math.floor(Math.random() * 256));  // 生成一个随机的ASCII字符        }        return { salt, length };    }    /**     * 计算加密时的偏移量     * @param {number} v 明文字符的ASCII值     * @param {number} s 盐的ASCII值     * @returns {number}     */    static calcOffsetEn(v, s) {        const r = 255 - v;        if (s > r) {            return s - r - 1;        }        return v + s;    }    /**     * 计算解密时的偏移量     * @param {number} v 密文字符的ASCII值     * @param {number} s 盐的ASCII值     * @returns {number}     */    static calcOffsetDe(v, s) {        if (v >= s) {            return v - s;        }        return 255 - (s - v) + 1;    }    /**     * 明文与盐混淆加密     * @param {Uint8Array} data 明文的字节数据     * @param {Object} salt 盐     * @returns {Uint8Array} 加密后的密文     */    static encryptWithSalt(data, salt) {        let result = [];        const data_len = data.length;        for (let i = 0; i < data_len; i++) {            const data_char_code = data[i];            const salt_char_code = salt.salt.charCodeAt(i % salt.length);            result.push(Crypt.calcOffsetEn(data_char_code, salt_char_code));        }        return new Uint8Array(result);    }    /**     * 密文解密     * @param {Uint8Array} data 密文的字节数据     * @param {Object} salt 盐     * @returns {Uint8Array} 解密后的明文     */    static decryptWithSalt(data, salt) {        let result = [];        const data_len = data.length;        for (let i = 0; i < data_len; i++) {            const data_char_code = data[i];            const salt_char_code = salt.salt.charCodeAt(i % salt.length);            result.push(Crypt.calcOffsetDe(data_char_code, salt_char_code));        }        return new Uint8Array(result);    }    /**     * 将字符串转换为 UTF-8 字节数组     * @param {string} str     * @returns {Uint8Array}     */    static stringToBytes(str) {        const encoder = new TextEncoder();        return encoder.encode(str);    }    /**     * 将 UTF-8 字节数组转换为字符串     * @param {Uint8Array} bytes     * @returns {string}     */    static bytesToString(bytes) {        const decoder = new TextDecoder('utf-8');        return decoder.decode(bytes);    }    /**     * 加密数据     * @param {string} data 明文     * @returns {string} 加密后的字符串     */    static encrypt(data) {        if (data === '') return '';        // 生成随机盐        const salt = Crypt.generateRandStr();        // 将数据转换为字节数组        const byte_data = Crypt.stringToBytes(data);        // 加密数据        const encrypted_data = Crypt.encryptWithSalt(byte_data, salt);        // 生成盐长度字符并将其与盐和密文一起编码为十六进制字符串        const salt_length = String.fromCharCode(salt.length);        // 转换为 UTF-8 字节数组并进行十六进制编码        const all_data = new Uint8Array([salt_length.charCodeAt(0), ...salt.salt.split('').map(c => c.charCodeAt(0)), ...encrypted_data]);        return Crypt.prefix + Crypt.toHex(all_data);    }    /**     * 解密数据     * @param {string} data 加密后的字符串     * @returns {string} 解密后的明文     */    static decrypt(data) {        if (data === '') return '';        // 检查加密格式        if (data[0] !== Crypt.prefix) return '';        // 去掉前缀并解码16进制数据        const clear_data = Crypt.fromHex(data.slice(1));        // 获取盐的长度        const salt_length = clear_data[0];        // 解密数据并返回结果        const salt = {            salt: String.fromCharCode(...clear_data.slice(1, salt_length + 1)),            length: salt_length        };        const encrypted_data = clear_data.slice(salt_length + 1);        const decrypted_bytes = Crypt.decryptWithSalt(encrypted_data, salt);        return Crypt.bytesToString(decrypted_bytes);    }    /**     * 将字节数组转换为十六进制字符串     * @param {Uint8Array} bytes     * @returns {string}     */    static toHex(bytes) {        let hex = '';        for (let i = 0; i < bytes.length; i++) {            hex += bytes[i].toString(16).padStart(2, '0');        }        return hex;    }    /**     * 将十六进制字符串转换为字节数组     * @param {string} hex     * @returns {Uint8Array}     */    static fromHex(hex) {        let bytes = [];        for (let i = 0; i < hex.length; i += 2) {            bytes.push(parseInt(hex.substr(i, 2), 16));        }        return new Uint8Array(bytes);    }}
  • PHP加密JavaScript解密(测试包含中文、英文、数字、表情)
echo Crypt::encrypt(json_encode([    'code' => 0,    'msg'  => 'success',    'data' => [        'name'       => '张三',        'en_name'    => 'SanZhang',        'age'        => 20,        'mood_emoji' => '?'    ]]));
let json_str = Crypt.decrypt('$19f33b3ae6796c3a9d4e4d37373115791af270a19a201c19d2d06e5d9d55ddd15cd77e7959a4a47c9b5414e316fd83818c45f21f5d9e47edcd5cd7c96fa5989e7a9b5414cc16cf864e492e4527a06a1f9b985c02bcaca5989e7a9b5414c302087a847a403715675c47e0d15cd7807d63599e84e87e51d50e098a853b0cf24fb09e1eacd09612b2b2676b5392f6');console.log(JSON.parse(json_str))
  • JavaScript加密PHP解密(测试包含中文、英文、数字、表情)
echo Crypt::decrypt('$3e6811d12601019a4fc061345af6bf305bf16c8071fd4da404e7c49ce861dbe91d44ddc64d44a9311631d84ace9c047c998f55473e1a5da705c65176a26651e3333f876e66bc89e246f0fada77b97d1d8ee5df5cbb05714ce6d60ab43c5777ac3e34b466d55377983d6c08ce34a8bbfcc4b6a279c2147430ba98dc884107a955487e');
let obj = {"name":"张三","en_name":"SanZhang","age":20,"mood_emoji":"?"};console.log(Crypt.encrypt(JSON.stringify(obj)));

服务端与客户端(RSA非对称加密)

  • 两个大坑
    • 长度限制:RSA加密算法本身有长度限制,具体取决于密钥的大小。对于2048位的RSA密钥,能够加密的最大数据长度是245字节(2048位 / 8 - 11(这里的11是对于PKCS#1 v1.5填充)= 245字节)。如果需要加密超长的数据,直接使用RSA加密是不切实际的,对于4096位的RSA密钥,能够加密的最大数据长度是512字节(4096位 / 8 - 11 = 511字节)。并且RAS算法随着密钥长度(2048, 4096)的增加,加密和解密操作会变得越来越慢(大质数分解难题)。
    • 逆向成本:如果客户端保留有私钥被黑客获取,则可以在不同设备上通过私钥轻松的生成完全相同的公钥(实测)。
    • 结论:若真要用RSA,则适合小数据量(大数据得分批传送)的请求接口且是客户端加密服务端解密的场景。
  • 首先生成公私钥对
在Linux或Windows git-bash命令行执行以下命令可生成密钥对openssl genpkey -algorithm RSA -out private.key -pkeyopt rsa_keygen_bits:2048openssl rsa -in private.key -pubout -out public.key
  • PHP公钥加密,JavaScript私钥解密(治标不治本的加密策略)(PHP需要开启openssl扩展)
/** * @function RSA加密算法,使用公钥加密数据 * @param    $str     string 被加密的数据 * @param    $pub_key string 公钥内容 * @return   string */function encryptRsa($str, $pub_key) {    $encrypt_str = '';    if(! openssl_public_encrypt($str, $encrypt_str, $pub_key)) {        return '';    }    return base64_encode($encrypt_str);}echo encryptRsa(    json_encode(['code' => 0, 'msg' => 'success', 'data' => ['lists' => [1, 2, 3]]]),    file_get_contents('C:/Users/Administrator/DeskTop/public.key'));
  • JavaScript公钥加密,PHP私钥解密(PHP需要开启openssl扩展)
/** * @function RSA解密算法,使用私钥解密数据 * @param    $str         string 要解密的数据 * @param    $private_key string 私钥内容 * @return   string */function decryptRsa($str, $private_key) {    openssl_private_decrypt(base64_decode($str), $res, $private_key);    return $res;}echo decryptRsa(    '被解密的base64字符串',    file_get_contents('C:/Users/Administrator/DeskTop/private.key'));

服务端与服务端(AES对称加密)

  • 说明:由于服务端代码普通用户接触不到,所以可以减少考虑key和vi泄露问题(被入侵除外)。
  • 加密
/** * @function AES加密数据 * @param    $data string 要被加密的数据 * @param    $key  string 256位自定义key,是加密算法的核心,用于确保只有持有相同密钥的人才能解密数据,推荐随机生成32个随机字符,确保不可预测 * @return   string */function encrypt($data, $key) {    $cipher = "AES-256-CBC";    $ivlen = openssl_cipher_iv_length($cipher);    $iv = openssl_random_pseudo_bytes($ivlen);    $encrypted = openssl_encrypt($data, $cipher, $key, OPENSSL_RAW_DATA, $iv);    return base64_encode($iv . $encrypted);}$key  = 'ABCD1234ABCD1234ABCD1234ABCD1234'; //此处可以通过Redis,或数据库共享秘钥,或者通过自定义的算法数据生成$data = "Hello";echo encrypt($data, $key);
  • 解密
/** * @function AES解密数据 * @param    $data string 要被解密的数据 * @param    $key  string 256位自定义key,是加密算法的核心,用于确保只有持有相同密钥的人才能解密数据,推荐随机生成32个随机字符,确保不可预测 * @return   string */function decrypt($data, $key) {    $cipher = "AES-256-CBC";    $data = base64_decode($data);    $ivlen = openssl_cipher_iv_length($cipher);    $iv = substr($data, 0, $ivlen);    $encrypted = substr($data, $ivlen);    $decrypted = openssl_decrypt($encrypted, $cipher, $key, OPENSSL_RAW_DATA, $iv);    return $decrypted;}$key  = 'ABCD1234ABCD1234ABCD1234ABCD1234'; //此处可以通过Redis,或数据库共享秘钥,或者通过自定义的算法数据生成echo decrypt('TdeyO0y21HGC7MB0sPsLo0DcVmVHYrqjUGXiU6hThHc=', $key);

服务端与服务端(较为安全的自定义对称加密算法)

  • 加密
Crypt 参考上文“服务端与客户端(较为安全的自定义对称加密算法,避免AES算法的 key和vi在客户端存储问题)”,记得用Ctrl + F搜Crypt::encrypt('要加密的字符串');
  • 解密
Crypt 参考上文“服务端与客户端(较为安全的自定义对称加密算法,避免AES算法的 key和vi在客户端存储问题))”,记得用Ctrl + F搜Crypt::decrypt('要解密的字符串');

服务端与服务端(RSA非对称加密)

  • 加密
/** * @function RSA加密算法,使用公钥加密数据 * @param    $str     string 被加密的数据 * @param    $pub_key string 公钥内容 * @return   string */function encryptRsa($str, $pub_key) {    $encrypt_str = '';    if(! openssl_public_encrypt($str, $encrypt_str, $pub_key)) {        return '';    }    return base64_encode($encrypt_str);}echo encryptRsa(    '要加密的字符串',    file_get_contents('C:/Users/Administrator/DeskTop/public.key'));
  • 解密
/** * @function RSA解密算法,使用私钥解密数据 * @param    $str         string 要解密的数据 * @param    $private_key string 私钥内容 * @return   string */function decryptRsa($str, $private_key) {    openssl_private_decrypt(base64_decode($str), $res, $private_key);    return $res;}echo decryptRsa(    '被解密的base64字符串',    file_get_contents('C:/Users/Administrator/DeskTop/private.key'));

理论(密码学常见误区)

二进制安全

  • 网络安全领域:漏洞挖掘与利用、代码审计、逆向工程与渗透、恶意软件分析。
  • 密码学领域:能够正确且完整的处理任意形式的二进制数据,而不会因为数据的内容或格式而出现意外行为,上文用到的AES算法、RSA算法、Base64、自定义加密算法,都可以保证二进制安全

Base64是编码不是加密

Base64不是加密与加密,而是编码与解码。它用于将二进制数据转换为文本形式以便在仅支持文本的系统中传输或存储,可保证二进制安全。

数字签名用于防篡改而不是加密

  • 通俗概念:数字签名就是在原始数据(一般是尾部)再次追加一段使用某些算法根据源数据生成的字符数据(称之为签名)。源数据与签名数据都是明文保存的,它们用于防篡改,而不是加密,最典型的应用是JWT。
  • 适用场景:常用于会话验证。以及防止越权,验证层拦截暴力破解,防止穷举传参拉数据的场景。
  • 流程举例:假设源数据为a且没有签名,攻击者可以直接修改为b,发送给服务器造成欺骗。但是若在a后面添加个签名,这个签名是将a数据通过保密的算法生成的,那么攻击者若不知道算法,若服务端同时验证攻击者发送的b和签名,当不一致时就可以拦截掉这次请求,避免数据被篡改。
  • 模拟实现
    用极简的代码模拟签名生成过程:
//服务端源数据为a$init_data = 'a';//拼接自定义的盐$sign = md5($init_data . '自定义的盐');//返回给客户端echo $init_data . '.' . $sign;

假设攻击者将a修改成b,然后给服务端验签,服务端拦截过程如下:

$hacker_data = 'b.af997aac21a6eeaa354fa4d46eed2c58';$arr = explode('.', $hacker_data);$init_data = $arr[0];$sign      = $arr[1];//验签if(md5($init_data . '自定义的盐') != $sign) {    //验签不通过证明数据被篡改    return '验签不通过,拦截掉本次请求';}

由于数据的创建和验签都是在服务端执行,所以攻击者并不知道签名算法,篡改数据也就无法通过服务端验签,恶意请求就此被拦截。

信息摘要与哈希算法

  • 信息摘要:信息摘要是通过特定的算法对一段数据进行处理,生成一个固定长度的字符串,这个字符串被称为摘要或散列值。信息摘要的主要目的是提供数据的完整性检查,确保数据在传输或存储过程中没有被篡改。
  • 哈希算法:哈希算法是一种将任意长度的数据映射为固定长度值的算法,这个固定长度的值称为哈希值或散列值。哈希算法在数据唯一性检查、数据完整性检查、数据防篡改、等领域有广泛应用,例如常见的sha1,sha256,sha512,md5算法。
  • 注意两者可以单向加密数据,但是无法解密数据。信息摘要是结果,哈希算法是手段。

哈希碰撞

哈希碰撞(Hash Collision)是指在密码学和计算机科学中,两个不同的输入数据在经过哈希函数处理后,得到相同的哈希值,这个概率很低,但不代表没有。

彩虹表的出现针对md5等主流哈希算法的冲击与优化方案

  • 彩虹表(Rainbow Tables):是一种预计算表,通过不断的穷举源数据的哈希值来达到获取数据原始值的表。它们可以帮助攻击者在不使用暴力破解的情况下,更快地找到与哈希值对应的明文密码(挨个记录并存储一些字符的哈希数据,从而通过密文反推源数据)。
  • 解决方案:加长的盐(实现更长的密文,用于指数级增加彩虹表成功找到源数据的难度)。

crt、key、pem证书文件

  • crt 文件:存储数字证书,主要用于公钥认证。
  • key 文件:存储公钥与私钥,用于加解密和签名。
  • pem 文件:一种Base64编码的文本格式,可以包含证书、私钥或证书请求等多种类型的数据。