在PHP7.1中使用openssl取代mcrypt

学着把眼泪像珍珠一样收藏,把眼泪都贮存在成功的那一天流淌,那一天,哪怕流它个大海汪洋。

The mcrypt extension has been abandonware for nearly a decade now, and was also fairly complex to use. It has therefore been deprecated in favour of OpenSSL, where it will be removed from the core and into PECL in PHP 7.2.

加密基础

加密算法一般分为两种:对称加密算法和非对称加密算法。

  • 对称加密
    对称加密算法是消息发送者和接收者使用同一个密匙,发送者使用密匙加密了文件,接收者使用同样的密匙解密,获取信息。常见的对称加密算法有:des/aes/3des.

对称加密算法的特点有:速度快,加密前后文件大小变化不大,但是密匙的保管是个大问题,因为消息发送方和接收方任意一方的密匙丢失,都会导致信息传输变得不安全。

  • 非对称加密
    与对称加密相对的是非对称加密,非对称加密的核心思想是使用一对相对的密匙,分为公匙和私匙,私匙自己安全保存,而将公匙公开。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密;如果用私钥对数据进行加密,那么只有用对应的公钥才能解密。发送数据前只需要使用接收方的公匙加密就行了。常见的非对称加密算法有RSA/DSA:

非对称加密虽然没有密匙保存问题,但其计算量大,加密速度很慢,有时候我们还需要对大块数据进行分块加密。

PHP的openssl扩展

openssl扩展使用openssl加密扩展包,封装了多个用于加密解密相关的PHP函数,极大地方便了对数据的加密解密。

非对称加密(RSA为例)

生成RSA私钥,可以指定长度,单位bit

1
openssl genrsa -out yangzie_private.pem 1024

生成对应的公钥

1
openssl rsa -pubout -in yangzie_private.pem -out yangzie_public.pem

你可以把这里生成的公钥拷贝到其他的服务端, 用户加密或者解密数据

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
$private_key_path = storage_path('yangzie_private.pem');
$public_key_path = storage_path('yangzie_public.pem');
$private_key = file_get_contents($private_key_path);
$public_key = file_get_contents($public_key_path);

//判断私钥是否是可用的
$pi_key = openssl_pkey_get_private($private_key);

//判断公钥是否是可用的
$pu_key = openssl_pkey_get_public($public_key);

//原始数据
$data = "hello";

// 加密后的数据
$encrypted = "";

// 解密后的数据
$decrypted = "";

//私钥加密,也可使用openssl_public_encrypt公钥加密,然后使用openssl_private_decrypt解密,加密后数据在$encrypted
openssl_private_encrypt($data, $encrypted, $pi_key);

//加密后的内容通常含有特殊字符,需要编码转换下,在网络间通过url传输时要注意base64编码是否是url安全的
$encrypted = base64_encode($encrypted);

dump($encrypted);

//私钥加密的内容通过公钥可解密出来,公钥加密的可用私钥解密。不能混淆
openssl_public_decrypt(base64_decode($encrypted),$decrypted,$pu_key);

dump($decrypted);

或者

1
2
3
4
5
6
//私钥加密
openssl_private_encrypt($data,$encrypted,$pi_key);
$encrypted = base64_encode($encrypted);
//公钥解密
openssl_public_decrypt(base64_decode($encrypted),$decrypted,$pu_key);
echo $decrypted; //hello

使用PHP自己也可生成一对公私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$config = array(
"digest_alg" => "sha512",
"private_key_bits" => 4096,
"private_key_type" => OPENSSL_KEYTYPE_RSA,
);
// 创建公私钥
$res = openssl_pkey_new($config);
// 获得私钥 $privKey
openssl_pkey_export($res, $privKey);
// 获得公钥 $pubKey
$pubKey = openssl_pkey_get_details($res);
$pubKey = $pubKey["key"];
$data = 'hello';
// 私钥加密
openssl_private_encrypt($data, $encrypted ,$privKey);
// 公钥解密
openssl_public_decrypt($encrypted, $decrypted, $pubKey);
echo $decrypted;

非对称加密的缺点是加密和解密花费时间长、速度慢,只适合对少量数据进行加密。如果既想有很快的加密速度又想保证数据比对称加密更加安全,可对数据进行对称加密,对秘钥做非对称加密,因为一般秘钥的长度会小于数据的长度。

对称加密(AES为例)

AES加密时需要统一四个参数:

  • 密钥长度 (Key Size)
  • 加密模式 (Cipher Mode)
  • 填充方式 (Padding)
  • 初始向量 (Initialization Vector)

由于前后端开发所使用的语言不统一,导致经常出现前后端之间互相不能解密的情况出现,其实,无论什么语言系统,AES的算法总是相同的,导致结果不一致的原因在于上述的四个参数不一致,下面就来了解一下这四个参数的含义

  • 密钥长度
    AES算法下,key的长度有三种:128、192、256 bits,三种不同密钥长度就需要我们传入的key传入不同长度的字符串,例如我们选择AES-128,那我们定的key需要是长度为16的字符串
  • 加密模式
    AES属于块加密,块加密中有CBC、ECB、CTR、OFB、CFB等几种工作模式,为了保持前后端统一,我们选择ECB模式
  • 填充方式
    由于块加密只能对特定长度的数据块进行加密,因此CBC、ECB模式需要在最后一数据块加密前进行数据填充
  • 初始向量
    使用除ECB以外的其他加密模式均需要传入一个初始向量,其大小与Block Size相等

参考Laravel中的加密类vendor\laravel\framework\src\Illuminate\Encryption\Encrypter.php

1
2
3
4
5
6
7
8
9
10
// 加密
$name = 'yangguoqi';
$secret = encrypt($name);

$a = 'eyJpdiI6Im42Z3IzeVNmTDNtXC9nT0NvaGV0NjNRPT0iLCJ2YWx1ZSI6IjhlTGRGYyt6aWhubjREM1BTM0EybExpdlhuMU1FTFwvSUhzUTVnNHRwekJNPSIsIm1hYyI6IjlmZTY3MDczMDg0NzJiMDUwNTRiMGNiYWM3YTAzYzQ4Y2EwOTA4MjJkMDM2OWEyZmVhODcwODUxZWNjMDRkYWEifQ==';
$b = 'eyJpdiI6InJcL1IwcklzbFcyTUZsSVhNS1k3RXFBPT0iLCJ2YWx1ZSI6IlwvV1dCcWdXS21hbERmWnFPWDVYa1hlN0RadkJlQUt4Nkh4bzczS2JuOEFRPSIsIm1hYyI6IjhhN2E0OWE2ZTc3ZTU4YzQzOGYzYmUzZDA3NWYwNTVlOTllNzQ1ZWY5YTE0NzE4NTUzM2E2MDY4ZDVkMzNhOTYifQ==';

// 解密
dump(decrypt($a));
dump(decrypt($b));

题外话

如何限制放在公网的服务器只有局域网内部的人才可以使用或者登录?

一开始想到的就是针对IP的限制, 但是由于公司使用网络的出口 IP 并不固定, 而且 IP 太多, 一个个设置太过麻烦, 然后在想想是否可以通过这样的方式, 比如进入系统的第一步就是登录, 我们就在登录这做一些手脚, 登录的时候在JS访问一个放置在局域网内的脚本, 可以返回一些加密过后约定的内容, 连同登录表单一起提交到系统后台验证, 因为外网的人无法访问到这个脚本, 既然也无法拿到我们事先约定好的加密数据, 自然无法登录.
JS脚本

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
<script>
$(function () {
$.ajax({
url: 'http://192.168.1.6/zq_check_login/auth.php',
dataType: 'jsonp', jsonp: 'hash',
data: {get: 1}
}).done(function ( data ) {
if (data.code === 200) {
var token = $("<input/>").attr({
name: 'access_token',
type: 'hidden'
}).val(data.token);

var ip = $("<input/>").attr({
name: 'access_private_ip',
type: 'hidden'
}).val(data.ip);

$("form").prepend(token, ip);

$("button[type='submit']").attr('disabled', false);
}
});
});
</script>

登录验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$publicKey = file_get_contents(storage_path('app/public.pem'));
$key = openssl_pkey_get_public($publicKey);
$encode = base64_decode($request['access_token']);
$result = openssl_public_decrypt($encode, $decrypted, $key);
if (!$result) {
return flash_message('身份验证失败!');
}

$arr = json_decode($decrypted, true);
$rt = substr(now()->toDateTimeString(), 0, 13);

if (!starts_with($arr['date'], $rt)) {
return flash_message('身份验证失败,请重试。');
}

if (!starts_with($arr['ip'], '192.168') && !starts_with($arr['ip'], '127.')) {
return flash_message('身份验证失败,请重试!');
}