接口安全加密的几种方式

简单签名

前台生成一个签名,当需要访问接口的时候,把时间戳,随机数,签名通过URL传递到后台。后台拿到时间戳,随机数后,通过一样的算法规则计算出签名,然后和传递过来的签名进行对比,一样的话,返回数据。

模拟前台生成接口签名链接

thinkphp5 模拟

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?php
namespace app\api\controller;

use think\Controller;

class Client extends Controller
{
const TOKEN='sui-token';

//模拟前台请求服务器api接口
public function getData()
{
//时间戳
$timeStamp = time();
//随机数
$randomStr = $this->nonce_str();
//生成签名
$signature = $this->sign($timeStamp,$randomStr);
//url地址
$url = "http://weixin.sadprincess.com/api/server/givedata?timeStamp={$timeStamp}&randomStr={$randomStr}&signature={$signature}";
// http://weixin.sadprincess.com/api/server/givedata?timeStamp=1558143935&randomStr=z4isxIQaj&signature=9384E621B20D227290EFB355871BAD29
$result = $this->http_get($url);
dump($result);
}
//随机生成字符串
public function nonce_str($length = 8)
{
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return "z".$str;
}

private function sign($timeStamp,$randomStr)
{
// 时间戳,随机数,口令按照首字母大小写顺序排序
// 然后拼接成字符串
// 进行sha1加密
// 再进行MD5加密
// 转换成大写。
$arr['timeStamp'] = $timeStamp;
$arr['randomStr'] = $randomStr;
$arr['token'] = self::TOKEN;
//按照首字母大小写顺序排序
sort($arr,SORT_STRING);
//拼接成字符串
$str = implode($arr);
//进行加密
$signature = md5(sha1($str));
//转换成大写
$signature = strtoupper($signature);
return $signature;
}

public function http_get($url)
{
$curl = curl_init();
curl_setopt($curl,CURLOPT_URL,$url);
curl_setopt($curl,CURLOPT_RETURNTRANSFER,1);
$result = curl_exec($curl);
curl_close($curl);
return $result;
}

}

具体需要前台按照统一算法约定生成 sign

后台处理

后台得到参数加密生成 sign 比对

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php
namespace app\api\controller;

use think\Controller;

class Server extends Controller
{
const TOKEN='sui-token';

//模拟前台请求服务器api接口
public function giveData()
{
$timeStamp = $_GET['timeStamp'];
$randomStr = $_GET['randomStr'];
$signature = $_GET['signature'];
$str = $this->sign($timeStamp,$randomStr);

if($str != $signature){
echo "签名错误";
exit;
}
//模拟数据
$arr['uid'] = '1';
$arr['name'] = 'sui';
echo json_encode($arr);
}
//随机生成字符串
public function nonce_str($length = 8)
{
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return "z".$str;
}

private function sign($timeStamp,$randomStr)
{
// 时间戳,随机数,口令按照首字母大小写顺序排序
// 然后拼接成字符串
// 进行sha1加密
// 再进行MD5加密
// 转换成大写。
$arr['timeStamp'] = $timeStamp;
$arr['randomStr'] = $randomStr;
$arr['token'] = self::TOKEN;
//按照首字母大小写顺序排序
sort($arr,SORT_STRING);
//拼接成字符串
$str = implode($arr);
//进行加密
$signature = md5(sha1($str));
//转换成大写
$signature = strtoupper($signature);
return $signature;
}

}

使用 openssl 对称加密算法加密接口

可用的对称加密算法(des/aes/3des): 使用命令行 openssl –help

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
# 查看对称加密算法类型
openssl enc --help

-aes-128-cbc -aes-128-cbc-hmac-sha1 -aes-128-cfb
-aes-128-cfb1 -aes-128-cfb8 -aes-128-ctr
-aes-128-ecb -aes-128-gcm -aes-128-ofb
-aes-128-xts -aes-192-cbc -aes-192-cfb
-aes-192-cfb1 -aes-192-cfb8 -aes-192-ctr
-aes-192-ecb -aes-192-gcm -aes-192-ofb
-aes-256-cbc -aes-256-cbc-hmac-sha1 -aes-256-cfb
-aes-256-cfb1 -aes-256-cfb8 -aes-256-ctr
-aes-256-ecb -aes-256-gcm -aes-256-ofb
-aes-256-xts -aes128 -aes192
-aes256 -bf -bf-cbc
-bf-cfb -bf-ecb -bf-ofb
-blowfish -camellia-128-cbc -camellia-128-cfb
-camellia-128-cfb1 -camellia-128-cfb8 -camellia-128-ecb
-camellia-128-ofb -camellia-192-cbc -camellia-192-cfb
-camellia-192-cfb1 -camellia-192-cfb8 -camellia-192-ecb
-camellia-192-ofb -camellia-256-cbc -camellia-256-cfb
-camellia-256-cfb1 -camellia-256-cfb8 -camellia-256-ecb
-camellia-256-ofb -camellia128 -camellia192
-camellia256 -cast -cast-cbc
-cast5-cbc -cast5-cfb -cast5-ecb
-cast5-ofb -des -des-cbc
-des-cfb -des-cfb1 -des-cfb8
-des-ecb -des-ede -des-ede-cbc
-des-ede-cfb -des-ede-ofb -des-ede3
-des-ede3-cbc -des-ede3-cfb -des-ede3-cfb1
-des-ede3-cfb8 -des-ede3-ofb -des-ofb
-des3 -desx -desx-cbc
-id-aes128-GCM -id-aes128-wrap -id-aes128-wrap-pad
-id-aes192-GCM -id-aes192-wrap -id-aes192-wrap-pad
-id-aes256-GCM -id-aes256-wrap -id-aes256-wrap-pad
-id-smime-alg-CMS3DESwrap -idea -idea-cbc
-idea-cfb -idea-ecb -idea-ofb
-rc2 -rc2-40-cbc -rc2-64-cbc
-rc2-cbc -rc2-cfb -rc2-ecb
-rc2-ofb -rc4 -rc4-40
-rc4-hmac-md5 -seed -seed-cbc
-seed-cfb -seed-ecb -seed-ofb

以DES对称加密 des-cbc/pksc5填充为例结果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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class CryptDes
{
private $key;

public function __construct($key)
{
$this->key = $key;
}

/**
* des加密
* @desc des加密CBC模式,PKCS5Padding填充
* @param $str
* @return string
*/
public function encrypt($str)
{
$str = $this->pkcs5Pad($str, 8);
if (strlen($str) % 8) {
$str = str_pad($str,
strlen($str) + 8 - strlen($str) % 8, "\0");
}
return base64_encode(openssl_encrypt($str, 'DES-ECB', substr($this->key, 0, 8), OPENSSL_RAW_DATA | OPENSSL_NO_PADDING));
}

/**
* des解密
* @param $str
* @return string
*/
public function decrypt($str)
{
$decode_str = openssl_decrypt(base64_decode($str), 'DES-ECB', substr($this->key, 0, 8), OPENSSL_RAW_DATA | OPENSSL_NO_PADDING);
return $this->pkcs5Unpad($decode_str);
}


/**
* PKCS5Padding填充
* @param $text
* @param $blocksize
* @return string
*/
private function pkcs5Pad($text, $blocksize)
{
$pad = $blocksize - (strlen($text) % $blocksize);
return $text . str_repeat(chr($pad), $pad);
}

/**
* PKCS5Padding填充逆向
* @param $text
* @return bool|string
*/
private function pkcs5Unpad($text)
{
$pad = ord($text{strlen($text) - 1});
if ($pad > strlen($text))
return false;
if (strspn($text, chr($pad), strlen($text) - $pad) != $pad)
return false;
return substr($text, 0, -1 * $pad);
}
}

$des = new CryptDes('sui-token-key');
$data = json_encode([
'uid' =>1,
'content' => 'sui content'
]);

$encode = $des->encrypt($data);

echo json_encode(['code'=> 1,'msg'=>'success','data'=>$encode]).PHP_EOL;
echo '解码后规则:'.$des->decrypt($encode);

// 结果
{"code":1,"msg":"success","data":"EZQ13GuP1wmxTLft6D2XzTkLojZxsvKlbNkoWkdX5WBOpFJL5iQSkA=="}
解码后规则:{"uid":1,"content":"sui content"}

RSA非对称加密

我一般服务器留私钥,给别人公钥,看个人吧,都可以.

命令行生成一对秘钥;php openssl_pkey_new() 也能生成一个新的私钥和公钥对

1
2
3
4
# 生成私钥 // 默认长度 2048
openssl genrsa -out private_key
# 通过提取私钥生成公钥
openssl rsa -in private_key -pubout -out public_key
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
class RSA
{
public $encrypt_len;

public $public_key;

public $private_key;

public function __construct($encrypt_len, $public_key, $private_key)
{
$this->encrypt_len = $encrypt_len;
$this->public_key = $public_key;
$this->private_key = $private_key;
}

/**
* 私钥加密
* @param $data_content
* @return string
*/
public function encryptedByPrivateKey($data_content)
{
$data_content = base64_encode($data_content);
$encrypted = "";
$totalLen = strlen($data_content);
$encrypt_pos = 0;
while ($encrypt_pos < $totalLen) {
openssl_private_encrypt(substr($data_content, $encrypt_pos, $this->encrypt_len), $encrypt_data, $this->private_key);
$encrypted .= bin2hex($encrypt_data);
$encrypt_pos += $this->encrypt_len;
}
return $encrypted;
}

/**
* 公钥解密
* @param $encrypted
* @return bool|string
*/
public function decryptByPublicKey($encrypted)
{
$decrypt = "";
$totalLen = strlen($encrypted);
$decryptPos = 0;
while ($decryptPos < $totalLen) {
openssl_public_decrypt(hex2bin(substr($encrypted, $decryptPos, $this->encrypt_len * 8)), $decryptData, $this->public_key);
$decrypt .= $decryptData;
$decryptPos += $this->encrypt_len * 8;
}
//openssl_public_decrypt($encrypted, $decryptData, $this->public_key);
$decrypt = base64_decode($decrypt);
return $decrypt;
}

}
$public_key = <<<EOF
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx6frXec7GEuypYmx3F/O
t2DQ8Uu3z9HdHF9/EqfRcrYq2vLVFnKNad9W1U9+1N1EcQlt7LdzWLWLklZQGLp9
76Tgn+vXROP0qpqt1U8aoe5MOd8FBboslVn2QoRaCQs08MXTJJ/X8OS+Y9nyXgpM
HOBIBV6X5GU3lZK4h2d2IxRzRKFZPDWB/ie17xXiHma5I6BBaVjUhgNyM3o7C+dx
geP856lc/qyBdJXAmrnJM34BhL7O2xyFDKKO4ur73sN6dGVZnXTfpWUW2iFPqfhf
t2abxvV68vVkaCI8r9NJn5LQFM5jVA0tSAKAIcTodz8OamZlA0L3y2ildH39Po+5
WQIDAQAB
-----END PUBLIC KEY-----
EOF;



$private_key = <<<EOF
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAx6frXec7GEuypYmx3F/Ot2DQ8Uu3z9HdHF9/EqfRcrYq2vLV
FnKNad9W1U9+1N1EcQlt7LdzWLWLklZQGLp976Tgn+vXROP0qpqt1U8aoe5MOd8F
BboslVn2QoRaCQs08MXTJJ/X8OS+Y9nyXgpMHOBIBV6X5GU3lZK4h2d2IxRzRKFZ
PDWB/ie17xXiHma5I6BBaVjUhgNyM3o7C+dxgeP856lc/qyBdJXAmrnJM34BhL7O
2xyFDKKO4ur73sN6dGVZnXTfpWUW2iFPqfhft2abxvV68vVkaCI8r9NJn5LQFM5j
VA0tSAKAIcTodz8OamZlA0L3y2ildH39Po+5WQIDAQABAoIBAFx1AYTpyK72lxMb
kbb40xSOjBPBuaxOhjNWcdUky9RwUaY038LWZ3BI0cBUYOqYUNJ7yGDfj1XcNHce
61aYPiuU0cSdBYJMggfjO27VhALLBJekFIJTdcPv9h9VhRA9FVYOdjm2n9xZQWWi
Dx/HavnQTunVpUdKLBRi7nsdyVJym1q7PilBwecLanGqyPtZEQZqAxPYrgXnSybC
e90V1jIbVXkwEVgi6rTcJnGa6bArgOyRnB8ENTVm3W2eYLDC4HnMmGzDphHhqL/H
y+tz9rCs63KkMvajU4L56xuvNDXIH5lILCXe0xDI1qI3FPhUF/dTGBvqWFEj1ncT
5ImjkgECgYEA7EIvulkHJBXVLbqadiiNGO8QUq4dBIqrLtQNeB1iwZwfIUrq4I3i
Wqd2nzWY6m2W7IV9z/GcKdx5i/uzqoK43zXcyKlXsrSmwO0cMBKJBQWz/tti7P+w
9VPFp2RIXPGACcBsXxycwGTP0wzScCeH0L/dIGK3jHm1BWUct77zt8ECgYEA2FbD
+XXeuGTDpwbLjPmAjAHkQ3j+eQS0hueQTVk+N4+3jPdq/5bjpwslmQIfFFgmjl7h
GTmD1GFHHA2J8ksqjpX0j2x71BAILje3sv6fsfnqLtyCm1rs0xoEnsNCyZ1tiMhv
tAOn2oXsGoldIa2qot5Cp0Be+wi4DpqotmQIp5kCgYBBX1cBgxzcuUXRb27J/xJ7
sbqchhz1YsFXJcnMUKVA2Ugr2CUBmYDmgKvsBdKYG1bFf3J+lWYWlzDysOTX62/o
I2lCBMmFY0M44uMH0jkjsJ5LiLzowx9d4p4wYSk8vn4lNJ9H+gzNl5eB2GxqKCYG
dvBiw63yKsMSCRZXKXkagQKBgQCS44a40JFivfgiZni6xYXEilxEe9i8rrcsifnu
qtOVwh0st6mM1RTJjMYAd/JGS/wR2tYX3HdoMoynvYGWY/cHTUX6Q/xn5mLHjslJ
3ZzsqPWaz9pWRYxbYWUVrxqf7iY7I2zE216S/y9qbS4w+cvMPgEfspNK+4aNU/GN
taoysQKBgQC12HZu8NP53PN5pSoEPPFjErhIHB7CuCL7hpphxQjb0j++qlcYK1bO
tYe26eu6Wq38TO/hvJorCBbhJwHgSZY9cb/Htw+h1xbIrOhSX16OmowlKxd4xgbM
q/Zzxw1t8uqc9wHbJcjmbylgCV9vPtBIMcoeKJSpf3rhGYXt6GKaWA==
-----END RSA PRIVATE KEY-----
EOF;

// 默认长度 2048
$encrypt_len = 2048;

$rsa = new RSA($encrypt_len, $public_key, $private_key);
$data = json_encode([
'uid' =>1,
'content' => 'sui content'
]);
$encode_content = $rsa->encryptedByPrivateKey($data);
$decode_content = $rsa->decryptByPublicKey($encode_content);
echo json_encode(['code'=> 1,'msg'=>'success','data'=>$decode_content]).PHP_EOL;

结果:
{"code":1,"msg":"success","data":"{\"uid\":1,\"content\":\"sui content\"}"}

JWT 用户认证

介绍: http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

实现多点登录认证

安装扩展包

composer require firebase/php-jwt

基础示例

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?php
namespace app\index\controller;
use \Firebase\JWT\JWT;

class Index
{
private $key = 'sui-key';
public function index()
{

$phone = $_GET['phone'];
$passwd = $_GET['passwd'];
if($phone == '666' && $passwd == '123456')
{
// 签发 token
$access_token = $this->issue();
return json(['code'=>1,'msg'=>'success','data'=>['access_token'=>$access_token]]);
}else{
echo '登录失败';
}

}

//签发token
public function issue()
{
$key = $this->key;
// 省略数据库查询操作........
$userinfo = [
'uid' => 1,
'username'=>'随某人'
];
$token = array(
"iss" => "http://sui.com", //签发者
"aud" => "http://sui.com", //jwt所面向的用户
"iat" => time(), //jwt的签发时间
"nbf" => time(), //定义在什么时间之前,某个时间点后才能访问
"exp" => time() + 3600, // 1小时后过期
"data" => $userinfo
);
$access_toekn = JWT::encode($token, $key);
return $access_toekn;
}

// 验证 token
public function check()
{
$access_token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9zdWkuY29tIiwiYXVkIjoiaHR0cDpcL1wvc3VpLmNvbSIsImlhdCI6MTU1ODE2Mjc4MiwibmJmIjoxNTU4MTYyNzgyLCJleHAiOjE1NTgxNjYzODIsImRhdGEiOnsidWlkIjoxLCJ1c2VybmFtZSI6Ilx1OTY4Zlx1NjdkMFx1NGViYSJ9fQ.0cOoQGX286IgDa5KF3cDm6RJgsBUbHHi0txQiPoSk6g';

try {
JWT::$leeway = 60;//当前时间减去60,把时间留点余地
$decoded = JWT::decode($access_token, $this->key, ['HS256']); //HS256方式,这里要和签发的时候对应
$arr = (array)$decoded;
print_r($arr);
} catch(\Firebase\JWT\SignatureInvalidException $e) { //签名不正确
echo $e->getMessage();
}catch(\Firebase\JWT\BeforeValidException $e) { // 签名在某个时间点之后才能用
echo $e->getMessage();
}catch(\Firebase\JWT\ExpiredException $e) { // token过期
echo $e->getMessage();
// 重新请求 token;
}catch(Exception $e) { //其他错误
echo $e->getMessage();
}

}
}

刷新

客户端通过用户名密码登录以后,服务端返回给客户端两个token:access_token和refresh_token

举个例子:比如access_token设置2个小时过期,refresh_token设置7天过期,2小时候后,access_token过期,但是refresh_token还在7天以内,那么客户端通过refresh_token来服务端刷新,服务端重新生成一个access_token;如果refresh_token也超过了7天,那么客户端需要重新登录获取access_token和refresh_token。

为了区分两个token,我们在载荷(payload)加一个字段 scopes :作用域。

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?php
namespace app\index\controller;
use think\Request;
use \Firebase\JWT\JWT;

class Index
{
private $key = 'sui-key';
public function index()
{

$phone = $_GET['phone'];
$passwd = $_GET['passwd'];
if($phone == '666' && $passwd == '123456')
{
// 签发 token
$token_arr = $this->issue();
return json(['code'=>1,'msg'=>'success','data'=>$token_arr]);
}else{
echo '登录失败';
}

}

//签发token
public function issue()
{
$key = $this->key;
// 省略数据库查询操作........
$userinfo = [
'uid' => 1,
'username'=>'随某人'
];
$token = array(
"iss" => "http://sui.com", //签发者
"aud" => "http://sui.com", //jwt所面向的用户
"iat" => time(), //jwt的签发时间
"nbf" => time(), //定义在什么时间之前,某个时间点后才能访问
"exp" => time() + 3600, // 1小时后过期
"data" => $userinfo
);

$access_token = $token;
$access_token['scopes'] = 'role_access'; //token标识,请求接口的token
$access_token['exp'] = time() + 7200; //access_token过期时间,这里设置2个小时

$refresh_token = $token;
$refresh_token['scopes'] = 'role_refresh'; //token标识,刷新access_token
$refresh_token['exp'] = time() + (86400 * 30); //access_token过期时间,这里设置30天

$token_arr = [
'access_token'=>JWT::encode($access_token,$key),
'refresh_token'=>JWT::encode($refresh_token,$key),
'token_type'=>'bearer' //token_type:表示令牌类型,该值大小写不敏感,这里用bearer
];
return $token_arr;
}

// 验证 token
public function check()
{
// halt($_SERVER);
// header 头获取或者 post 接过来
$access_token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9zdWkuY29tIiwiYXVkIjoiaHR0cDpcL1wvc3VpLmNvbSIsImlhdCI6MTU1ODE2ODg3NiwibmJmIjoxNTU4MTY4ODc2LCJleHAiOjE1NTgxNzYwNzYsImRhdGEiOnsidWlkIjoxLCJ1c2VybmFtZSI6Ilx1OTY4Zlx1NjdkMFx1NGViYSJ9LCJzY29wZXMiOiJyb2xlX2FjY2VzcyJ9.5aUVKj0ruXLqsUUAaDYkZIovr8ndzXu1hkMBhGXE78k';
try {
JWT::$leeway = 60;//当前时间减去60,把时间留点余地
$decoded = JWT::decode($access_token, $this->key, ['HS256']); //HS256方式,这里要和签发的时候对应
$arr = (array)$decoded;
print_r($arr);
} catch(\Firebase\JWT\SignatureInvalidException $e) { //签名不正确
echo $e->getMessage();
}catch(\Firebase\JWT\BeforeValidException $e) { // 签名在某个时间点之后才能用
echo $e->getMessage();
}catch(\Firebase\JWT\ExpiredException $e) { // token过期
echo $e->getMessage();
// 重新请求 token;
}catch(Exception $e) { //其他错误
echo $e->getMessage();
}

}
}

ps:token 可以添加到 header头,也可以 post body 体里面

OAUTH2 授权

纵有疾风起,人生不言弃!