APP 接口设计(加密/签名/TOKEN认证/接口版本)

接口安全问题:

  1. 请求来源是否合法
  2. 请求参数是否被篡改
  3. 请求是唯一的(即同样的请求, 只有一次会生效)
  4. 数据传输的安全性
客户端请求流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. 请求服务端,获取 RSA公钥(rsaPublicKey) 以及 接口密钥(key)
2. 生成 AES密钥(aesKey)
3. 定义 Params参数[TOKEN, PLATFORM, VERSION, NONCESTR, TIMESTAMP], 使用 AES密钥(aesKey) 加密, 得到加密后的数据(encryptParams)
4. 使用 RSA公钥(rsaPublicKey) 对 AES密钥(aesKey) 加密, 得到加密后的AES密钥(encryptAesKey)
5. 将 加密后的AES密钥(encryptAesKey) 作为HEADER参数之一, 完整的HEADER参数:[KEY => encryptAesKey, VALUE => encryptParams]
6. 提取请求数据相关参数, 拼接 接口密钥(key) 生成签名, 然后发送请求给服务端, 等待响应
7. 收到服务端的响应后, 使用请求时发送的 AES密钥(aesKey) 进行解密

Params参数说明:
TOKEN 用户登录凭证
PLATFORM 请求来源所属平台
VERSION 平台版本
NONCESTR 随机字符串,用于验证请求是否重复
TIMESTAMP 请求时间,用于验证请求是否超时
服务端处理流程
1
2
3
4
5
6
7
8
9
10
1. 响应客户端的请求, 获取客户端传输过来的HEADER参数[KEY, VALUE]
2. 使用 RSA私钥(rsaPrivateKey) 对 加密后的AES密钥(KEY) 进行RSA解密, 得到AES密钥(decryptKey)
3. 使用 解密后的AES密钥(decryptKey) 对 加密后的数据(VALUE) 进行AES解密, 得到解密后的JSON数据(decryptValue)
4. 对 解密后的JSON数据(decryptValue) 进行JSON解析
5. 若 解密后的AES密钥(decryptKey) 与 解密后的JSON数据(decryptValue) 不为空, 说明请求合法
6. 获取 请求时间(decryptKey[TIMESTAMP]) 与 服务端时间(serverTime), 判断时间差是否大于 请求有效时间(requestExpireTime), 若大于则说明请求已超时(无效)
7. 获取 随机字符串(decryptKey[NONCESTR]), 校验此记录是否存在 服务端的NONCESTR集合 内, 若存在则说明是重复请求(无效), 否则记录此NONCESTR, 并删除集合内超过 请求有效时间(requestExpireTime) 的NONCESTR (可以使用redis的expire, 新增nonce的同时设置它的超时失效时间)
8. 若当前请求需要验证用户是否登录, 则 用户登录凭证(decryptValue[TOKEN]) 不能为空, 并查询数据库, 判断 TOKEN 是否过期
9. 获取 请求数据(requestData), 使用 接口密钥(secretKey) 对请求数据验签
10. 返回数据时, 转为JSON格式后, 使用 解密后的AES密钥(decryptKey) 进行加密, 然后返回给客户端

框架:Symfony3.4.13
JS插件包:jsencryptCryptJS

若使用 WEB 模拟请求接口的话, 需注释 ApiRequestListener.php 第 91 行

配置代码
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
// app.yml
parameters:
#服务端RSA公钥
server_rsa_public_key: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuPEVGUuo+0C6bXh6jjL7lSQ3UY3OqhN+5va+6h+sIsJpSIxRx2h8xlqWb8/BKQPUJL2WyQISOIDt/Mkdgzgy/5U/gA1iYvLdZanLAP/qgqEpVc2S++uVxQMMtS5/1ozECNtO3/jTKXkeAaow5JrstuJOkaSVJ/KkDVx/5DjyLgE0dMpJg7FgYbxZWLRKRz0B1yI4FdfhxSIJ6HQWPd7pqb5uiP1W5ftwYlNbZ8dGWrZgk2Ck15LrsNJLp4IFpNw1Wo8jxuOW7JaEck/gazyi082x1dpWQqrL4aH9F69+JsPBRbtFfj4K2jk0YQoOmW6TePnZG6yXq+MSxjMAOemA9wIDAQAB'
#服务端RSA私钥
server_rsa_private_key: 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC48RUZS6j7QLpteHqOMvuVJDdRjc6qE37m9r7qH6wiwmlIjFHHaHzGWpZvz8EpA9QkvZbJAhI4gO38yR2DODL/lT+ADWJi8t1lqcsA/+qCoSlVzZL765XFAwy1Ln/WjMQI207f+NMpeR4BqjDkmuy24k6RpJUn8qQNXH/kOPIuATR0ykmDsWBhvFlYtEpHPQHXIjgV1+HFIgnodBY93umpvm6I/Vbl+3BiU1tnx0ZatmCTYKTXkuuw0kunggWk3DVajyPG45bsloRyT+BrPKLTzbHV2lZCqsvhof0Xr34mw8FFu0V+PgraOTRhCg6ZbpN4+dkbrJer4xLGMwA56YD3AgMBAAECggEAMNp+WFBEMxrGJGTO+wE8tAj9E+4ByaucuiY0CGSVdBkm9qMadzKCw2Lqml6nB86bG5l5W1/QsFxegYge46rUze7+9zSR6NF+6nwPxBPWPuuTn7bOPP3eckx77uB5pJNKtYw5KbDxFuOHqajrgXfrT+Q4HQD85bCS5XSp0/+2+a+khp0oZodwKezspNdKQYhFrly/W0meWF3fQqVZ4Rh1BpPKxjAX7s7xAPApn1nTmo1Uy39oM1Ud6ATx4NnbVqm+C1ahmgA/Nbwz0XyEpaKjOY4Vr52MLtLessY+Q8kaNyfuqzH/MrLtyLKnxNSbVDjYy6s8j8bBlBTB6GFdif0WAQKBgQDqQGZJBIiUE3uPG8XPysQEXwoa7ASk0hNLVIelb9Z3xfhQWaWicB0sY8dtsl8bam2l4qZ4iQEYhUGNRiLxrfMeMmNxy7pyI9eP63vt0908T/1HZDMcWL6CYKccaA6AtjHDgJLEinWatrqiEq4TfH43MTa6tzpYZtCugRTYQvNvvwKBgQDKHLSyXVP/J35x9eTKkBKsN7QQ+l9mFpp2K8B8EPS3ds7aIitAiw97H6c3xXHTGH35QixlSWd0kq4sE5frBnNuYwPBWcXfX0poQfkAwWEph1f/iQdY9j16KORiqsm27e+ZK1vtt+RZfWU/n1RoSopuMe96RDhCqJYWtTefCuo8yQKBgEsh9Kyew5+a0BqKcdu/0TcFtJwF70deCcozhn5NbKBl4ssCtdlv1CuUpTZN66tDa3+1PmeSqcNPmkLRqAuUG1IoHzU0fsx1KoKCqPES7vaVQUtQnAQPgqsWjQLTbTNjPHrUFj7rmeTRjvLEwwiE+YaCRmeEtTX9ZBlUVXc3ohTJAoGAIyaW7qZ4o1m1DhDb97bS6IzPjlxdFx47Qu4dDfbM+NN66kkjCJim2p0IshRu1W3fCujNW9hGW+nezN+jfkai8MHbt1brqQujnzpKGi2HvndBgLnOQ1SgIIzYG6jkaCI9l4AI/vEKj93VLBmDzpeYN84LflI7DqzPXaeqwshdMLECgYEAs/7vspm7m/J58S+bJtDk1xc2/FwHgRLkCm6dXa9sORWGotVZQheF+rtwJ/N4DVqsTmwvHr/JI7P6DA4kE3cvuijB1e5DAv7LpkW9oFEjXDDlIp+9xkBjQ1LKgLAtjSCx+xx2fPPNwJk/ydSzvLyTi88uR0LDFlEPXlyUNax24EY='
#客户端RSA公钥
client_rsa_public_key: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm57YawNBbWEysl+ItxIzOyhP/k0M4gnfJRIGWWhxkJqWkKFd7sfHeXkzTvmTNsAXmSu6JtVdOqAIUdACHLy7+wGRLbJuYx2B9sYTKJvAOmjhq53khLQ3ucz6m4Bpcig1jyZt9Sd9maFLN8dyrDpXzMsLML360d6qVcZiezba3p+mopZ9KSXdyj4wfctlAlpZsdMit/S6m0IWMirIV+xT3gbwAMXDLGUgRoIYILwhpIb4uLFckBBtf3hKOKsjwBELzMxwmNgYBfx4kxBUbWM+sA8gi+EqojFk/HLGc058+qtyQMHn0tOCICk3rN96lMS/IFPoQsojDD9ifFFeDJJYWQIDAQAB'
#客户端RSA私钥
client_rsa_private_key: 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCbnthrA0FtYTKyX4i3EjM7KE/+TQziCd8lEgZZaHGQmpaQoV3ux8d5eTNO+ZM2wBeZK7om1V06oAhR0AIcvLv7AZEtsm5jHYH2xhMom8A6aOGrneSEtDe5zPqbgGlyKDWPJm31J32ZoUs3x3KsOlfMywswvfrR3qpVxmJ7Ntren6ailn0pJd3KPjB9y2UCWlmx0yK39LqbQhYyKshX7FPeBvAAxcMsZSBGghggvCGkhvi4sVyQEG1/eEo4qyPAEQvMzHCY2BgF/HiTEFRtYz6wDyCL4SqiMWT8csZzTnz6q3JAwefS04IgKTes33qUxL8gU+hCyiMMP2J8UV4MklhZAgMBAAECggEAZMB8oRvkeipZpj5PxybTYFODJsM/ugBmJhv7XFtQWyyamly+8d8J+E1NuK3Ab8wB+zriNE4jI9eES2N+Wpieo62qDondCfKKt/gZY0sjMy3AHVoGHxyGj5Z0EcUbf7skod9hhTziBlr01dIdHgBP49j5D7+P6dxdL1dXXypunX6A1lCgxrRC0Z0iydxb9VWXOLyCb5sIAmh90FNTKUGV5y5KtyUZQAw7JdI4+Se0cqErOakwQr/VpAWRo9AxwPZ5HGqvrEAJHYd7acw1aLe7tXW1DoA9ou4iknmlAJ/oHogHsyzvbq8mLYkyZROXlcs8qAas2hB/wMBvo/0HJK+MYQKBgQDNiZioL1ieOZ1fwwfONaFRpGAKM8UJMvKmK5JVHlbqfXpHBQ0m4HSRlPAKvkXq5Jbomvb8rpbvZzIYOXhyZF/3F9/u6UIMmeakftxFLhnNYJyT7y1xPdoKWgxI1hePsiVps+DE8sYxO1LBzfDuMnwr4DXzSOeeQyiaJOsk2C88LQKBgQDB0+AXqWS9TZvUS5vLHJi2fXsKchO5YxgoWvXsdTnaUat0nb62yejW48jqdGCIL/5c9yXFTcAnd5h+a6UBZznxj/z6N4KDF7WsZSaPuvdP/5eOMQ6iuSMPF3jxtV34k0b+jTamoufFQ2Xjb9sUlkyRDwhjVfi1/KZdWnjrnbDsXQKBgDDyXpdWxxzPDao7cMVrwiIGKhTj5T1ek6h84dlBY2NuREtbaZljhH8S3+M/ErlwfHuiQ8VC8pDKm4RnU0aynqPiXKKxi9giYmm0CFK1OtHM+xzDrae1GhKzBQ/nZC8FNqGog5ODWS1qOjgLCiA/h5CPUWnBZ98pkSa8Of9JOF51AoGAM0iX9iq/mMa8AEOxCOCcF0zEDVN5nId3kNXgU5wAnp8VOlmyaDKsBI9oTYBVOjNYnchWmgmkWczu8CQTGHfzgNKUILAnPAA99UseFNFnNiduNhUMxkkt2YRgX7OZFXgCRL+gQh7ALBVVFAQ4dw39XDQaCA5rK9uZOQIDFHQ4p5ECgYEAkYIhEhcKtVtziSaJjil1TznCZcLRYyxACHJQOBob7j+HehIA4OK2g3KwJFUJm06kSTrNirWruiGONpOn4El5/it3ir/EW7Kj1lUyGVOOsha+gIVcaNB4k8Jz9me9KCaCbuY6zmdTmp22qYsSh93RTZEvEHpj7DjmKs/0UmamAOI='

#API请求过期时间(单位:秒)
api_request_expire_time: 1200
#API接口密钥
api_secret_key: 'Lx8cQ659aM!T6zvKcwcoybA0^ltPbNqT'
#API SESSION前缀
api_session_prefix: 'API_'

#用户登录凭证TOKEN过期时间(单位:秒)
api_user_token_expire_time: 1200

#REDIS配置
redis_config:
host: 127.0.0.1
port: 6379
auth: 123456
注册服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
services:
_defaults:
autowire: true
public: true

#============================HEADER解密服务==============================
api_request_listener:
class: Bundles\ApiBundle\Listener\ApiRequestListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 10 }

#============================Response加密服务==============================
api_response_listener:
class: Bundles\ApiBundle\Listener\ApiResponseListener
tags:
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: 10 }
路由配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//routing_user.yml
#========================================API V1.0 START========================================
#用户登录
user_login:
path: /login
defaults: { _controller: ApiBundle:V1\User\Center:login }
methods: [GET, POST]

#获取用户信息
user_get_user_info:
path: /getUserInfo
defaults: { _controller: ApiBundle:V1\User\Center:getUserInfo }
methods: POST
#========================================API V1.0 END========================================
服务端请求校验
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
<?php

namespace Bundles\ApiBundle\Listener;

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Util\Crypt\RSA;
use Util\Crypt\AES;
use Util\Redis\RedisHandler;

class ApiRequestListener
{
/**
* @var ContainerInterface
*/
protected $container;

private static $controllerNameSpace = 'Bundles\ApiBundle\Controller\\';

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

/**
* @param GetResponseEvent $event
*
* @throws \Exception
*/
public function onKernelRequest(GetResponseEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}

$request = $event->getRequest();
$attributes = $request->attributes;
$currentRoute = $attributes->get('_route');
$serverTime = new \DateTime();

// 无需认证的路由
$ignoreAuthRoute = array_values($this->container->getParameter('ignore_auth_route'));
if (!in_array($currentRoute, $ignoreAuthRoute)) {
/**
* TOKEN 用户登录凭证,可为空
* PLATFORM 平台
* VERSION APP版本
* NONCESTR 随机字符串
* TIMESTAMP 请求时间,用于验证请求是否超时
*/
$headerParams = ['TOKEN', 'PLATFORM', 'VERSION', 'TIMESTAMP'];
// RSA加密后的AES KEY
$encryptKey = $request->server->get('HTTP_KEY');
// AES加密后的数据
$encryptValue = $request->server->get('HTTP_VALUE');
$decryptKey = $decryptValue = null;
if (!empty($encryptKey) && !empty($encryptValue)) {
$decryptKey = RSA::decrypt($encryptKey, $this->container->getParameter('server_rsa_private_key'));
$decryptValue = json_decode(AES::decrypt($encryptValue, $decryptKey), true);
foreach ($headerParams as $headerParam) {
if (!isset($decryptValue[$headerParam]) || ($headerParam != 'TOKEN' && empty($decryptValue[$headerParam]))) {
$decryptValue = null;
break;
}
}

// 请求有效时间
$requestExpireTime = $this->container->getParameter('api_request_expire_time');
$requestTime = (new \DateTime($decryptValue['TIMESTAMP']))->add(new \DateInterval("PT{$requestExpireTime}S"));
// 请求超时
if ($requestTime < $serverTime) {
$this->respond($this->container->getParameter('MSG002'));
}

// 初始化Redis
$redis = new RedisHandler($this->container->getParameter('redis_config'));
// 重复请求
if ($redis->getRedis()->get($decryptValue['NONCESTR'])) {
$this->respond($this->container->getParameter('MSG003'));
}

// 保存NonceStr至Redis,并设置过期时间
$redis->getRedis()->setex($decryptValue['NONCESTR'], $requestExpireTime, $decryptValue['TIMESTAMP']);

// 保存AES KEY到attributes
$attributes->set('AESKEY', $decryptKey);
}

// HEADER参数错误,非法请求
if (empty($decryptKey) || empty($decryptValue)) {
$this->respond($this->container->getParameter('MSG006'));
}

// 无需验证登录的路由
$ignoreLoginRoute = array_reduce(array_values($this->container->getParameter('ignore_login_route')), 'array_merge', []);
if (!in_array($currentRoute, $ignoreLoginRoute)) {
$user = null;
if (!empty($decryptValue['TOKEN'])) {
$userRepository = $this->container->get('doctrine')->getRepository('CommonBundle:Api:User');
$user = $userRepository->findOneBy(['token' => $decryptValue['TOKEN']]);
// 用户不存在或被禁用
if (empty($user) || $user->getAccountStatus() != 1) {
$this->respond($this->container->getParameter('MSG009'));
}

// TOKEN已过期
if ($user->getTokenExpireTime() < $serverTime) {
$user = null;
}

// 保存用户信息至SESSION
$request->getSession()->set($this->container->getParameter('api_session_prefix') . 'USER', $user);
}

if (empty($user)) {
$this->respond($this->container->getParameter('MSG008'));
}
}

// 需要验签的路由
$verifySignRoute = array_reduce(array_values($this->container->getParameter('verify_sign_route')), 'array_merge', []);
if (in_array($currentRoute, $verifySignRoute) && $request->isMethod('POST')) {
// 请求数据
$requestData = $request->request->all();

// 第一种签名校验,使用客户端的RSA公钥验签
//$verifySignResult = RSA::verifySign($requestData, $this->container->getParameter('client_rsa_public_key'));
// 第二种签名校验,使用服务端的密钥重新生成签名比对
$verifySignResult = RSA::verifySignNoneOpenssl($requestData, $this->container->getParameter('api_secret_key'));

// 签名校验失败
if (!$verifySignResult) {
$this->respond($this->container->getParameter('MSG007'));
}
}

// 匹配路由并转发
$version = $request->server->get('HTTP_APIVER');
$this->matchRouteAndForward($request, $attributes->get('_controller'), $version);
}
}

/**
* 匹配路由并转发
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @param string $controller
* @param string $version
*/
private function matchRouteAndForward($request, $controller, $version)
{
if (is_numeric($version) && $version) {
$delimiter = '::';
if (strpos($controller, $delimiter) !== false) {
if (strpos($version, '.') !== false) {
// 主版本
$major = (int)substr($version, 0, strpos($version, '.'));
// 次版本
$minor = (int)substr($version, strpos($version, '.') + 1);
} else {
$major = (int)$version;
$minor = null;
}

// 主版本为空或版本号为 1或1.0
if (empty($major) || $major === 1 && empty($minor)) {
return;
}

list($controllerName, $actionName) = explode($delimiter, $controller, 2);
// 获取版本对应的控制器
$controllerName = str_replace(self::$controllerNameSpace, '', $controllerName);
$controllerName = self::$controllerNameSpace . "V{$major}\\" . substr($controllerName, strpos($controllerName, '\\') + 1);
// 次版本不为空,则重新定义Action
if (!empty($minor)) {
$actionName = substr($actionName, 0, -6) . "M{$minor}";
}

// 根据版本号,更改路由对应的控制器方法
$request->attributes->set('_controller', $controllerName . $delimiter . $actionName);
}
}
}

/**
* Respond.
*
* @param array $message
* @param array $data
*
* @return mixed
*/
private function respond($message, $data = [])
{
$data = ['data' => empty($data) ? (object)$data : $data];
$respond = array_merge($message, $data);

die(json_encode($respond));
}
}
服务端接口处理
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
<?php

namespace Bundles\ApiBundle\Controller\V1\User;

use Bundles\ApiBundle\Controller\BaseController;
use Bundles\ApiBundle\Traits\User\CenterAbstractTrait;
use Bundles\ApiBundle\Controller\V1\User\Traits\CenterTrait;
use Util\DateTime\DateHelper;
use Util\Helper\Helper;

class CenterController extends BaseController
{
use CenterAbstractTrait, CenterTrait;

/**
* 用户登录
*
* @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\Response
*
* @throws \Exception
*/
public function loginAction()
{
if ($this->isPost()) {
$request = $this->getRequest();
$account = $request->request->get('account');
$password = $request->request->get('password');
$userRepository = $this->getApiRepository('User');
$user = $userRepository->findOneBy([
'account' => $account,
'accountStatus' => 1
]);
if (!empty($user)) {
if (md5($password) === $user->getPassword()) {
$nowTime = DateHelper::getNowDateTime();
$data = [
'token' => Helper::generateToken(),
'tokenExpireTime' => $nowTime->add(new \DateInterval("PT{$this->getCfgParameter('api_user_token_expire_time')}S"))
];
/** @var \Bundles\CommonBundle\Entity\User $user */
$user = $userRepository->saveEntity($data, $user);

return $this->objMessage($this->getCfgParameter('MSG001'), ['token' => $user->getToken()]);
}
}

return $this->objMessage($this->getCfgParameter('MSG000'));
}

return $this->render('@Api/User/login.html.twig');
}

/**
* 获取用户信息
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
public function getUserInfoAction()
{
if ($this->isPost()) {
$user = $this->getUserInfo();
return $this->objMessage($this->getCfgParameter('MSG001'), [
'user_id' => $user->getId(),
]);
}

return $this->objMessage($this->getCfgParameter('MSG000'));
}
}
服务端返回数据
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
<?php

namespace Bundles\ApiBundle\Listener;

use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Util\Crypt\AES;

class ApiResponseListener
{
/**
* @var ContainerInterface
*/
protected $container;

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

public function onKernelResponse(FilterResponseEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}

$request = $event->getRequest();
$currentRoute = $request->attributes->get('_route');
// 无需认证的路由
$ignoreAuthRoute = array_values($this->container->getParameter('ignore_auth_route'));
if (!in_array($currentRoute, $ignoreAuthRoute)) {
$response = $event->getResponse();
if ($response instanceof JsonResponse) {
// 无需加密数据的路由
$ignoreEncryptRoute = array_reduce(array_values($this->container->getParameter('ignore_encrypt_route')), 'array_merge', []);
if (!in_array($currentRoute, $ignoreEncryptRoute)) {
$content = json_decode($response->getContent(), true);
// AES加密
$content['data'] = AES::encrypt(base64_encode(json_encode($content['data'])), $request->attributes->get('AESKEY'));
$response->setData($content);
}
}
}

return;
}
}
WEB 模拟请求接口
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
{% block javascripts %}
<script src="{{ asset('/static/js/jquery-3.3.1.min.js') }}"></script>
<script src="{{ asset('/static/js/jsencrypt.min.js') }}"></script>
<script src="{{ asset('/static/js/CryptoJS v3.1.2/components/core-min.js') }}"></script>
<script src="{{ asset('/static/js/CryptoJS v3.1.2/rollups/aes.js') }}"></script>
<script src="{{ asset('/static/js/CryptoJS v3.1.2/rollups/sha256.js') }}"></script>
<script src="{{ asset('/static/js/base64.js') }}"></script>
{% endblock %}

{% block content %}
{{ getConfigParameter('app_name') }}
<form method="post">
<input type="text" name="account" required="required" />
<input type="password" name="password" required="required" />
<input type="submit" value="Login" />
</form>
<script>
$(function() {
// 生成HEADER参数
function generateHeader(token, apiver)
{
// 服务端公钥,用于加密AES KEY
let serverRsaPublicKey = '-----BEGIN PUBLIC KEY-----{{ getConfigParameter('server_rsa_public_key') }}-----END PUBLIC KEY-----';
let RSA = new JSEncrypt();
// 设置公钥
RSA.setPublicKey(serverRsaPublicKey);

// AES KEY
let aesKey = generateRandom();
// AES IV
let aesIv = CryptoJS.MD5(aesKey).toString().substring(0, 16);
// 请求参数
let params = {
'TOKEN': token || '',
'PLATFORM': 'WEB',
'VERSION': '1.0',
'NONCESTR': generateRandom(16),
'TIMESTAMP': new Date().format('yyyy-MM-dd hh:mm:ss'),
};

return {
aes: {
key: aesKey,
iv: aesIv,
},
header: {
// 使用 RSA 对 aesKey 加密
KEY: RSA.encrypt(aesKey),
// 使用 AES 对 params 加密
VALUE: aesEncrypt(params, aesKey, aesIv),
// 接口版本号
APIVER: apiver || '1.0',
}
};
}

$('form').on('submit', function() {
let postData = {
account: $('input[name="account"]').val(),
password: $('input[name="password"]').val(),
nonece: generateRandom(16),
timestamp: new Date().format('yyyy-MM-dd hh:mm:ss'),
};

/*// 第一种加签方式:使用客户端的RSA私钥加签
// 客户端私钥,用于生成签名
let clientRsaPrivateKey = '-----BEGIN PRIVATE KEY-----{{ getConfigParameter('client_rsa_private_key') }}-----END PRIVATE KEY-----';
// 客户端公钥,用于验签
let clientRsaPublicKey = '-----BEGIN PUBLIC KEY-----{{ getConfigParameter('client_rsa_public_key') }}-----END PUBLIC KEY-----';
// 生成签名
let postStr = generateSign(postData);
let signRSA = new JSEncrypt();
signRSA.setPrivateKey(clientRsaPrivateKey);
postData.sign = signRSA.sign(postStr, CryptoJS.SHA256, "sha256");
// 验证签名
let verifyRSA = new JSEncrypt();
verifyRSA.setPublicKey(clientRsaPublicKey);
let verifySign = verifyRSA.verify(postStr, postData.sign, CryptoJS.SHA256);
console.log(verifySign);*/

// 第二种加签方式:使用服务端的密钥拼接生成签名
postData.sign = generateSign(postData, '{{ getConfigParameter('api_secret_key') }}');

let headerJson = generateHeader();
$.ajax({
type: 'POST',
url: '',
data: postData,
dataType: 'JSON',
headers: headerJson.header,
}).done(function(res) {
if (res.code === 1) {
let data = JSON.parse(aesDecrypt(res.data, headerJson.aes.key, headerJson.aes.iv));
// 获取用户信息
headerJson = generateHeader(data.token);
$.ajax({
type: 'POST',
url: '{{ path('user_get_user_info') }}',
data: {},
dataType: 'JSON',
headers: headerJson.header,
}).done(function(res) {
if (res.code === 1) {
let data = JSON.parse(aesDecrypt(res.data, headerJson.aes.key, headerJson.aes.iv));
alert(JSON.stringify(data));
}
});
} else {
alert(res.message);
}
});

return false;
});

// AES加密
function aesEncrypt(data, key, iv) {
key = CryptoJS.enc.Utf8.parse(key);
iv = CryptoJS.enc.Utf8.parse(iv);
if (typeof(data) === 'object') {
data = JSON.stringify(data);
}

let string = new Base64().encode(data);
let encrypted = CryptoJS.AES.encrypt(string, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});

return encrypted.ciphertext.toString();
}

// AES解密
function aesDecrypt(string, key, iv) {
key = CryptoJS.enc.Utf8.parse(key);
iv = CryptoJS.enc.Utf8.parse(iv);
let encryptedHexStr = CryptoJS.enc.Hex.parse(string);
let srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr);
let decrypt = CryptoJS.AES.decrypt(srcs, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
decryptedStr = new Base64().decode(decryptedStr);

return decryptedStr;
}

// 生成签名
function generateSign(postData, secretKey) {
secretKey = secretKey || '';
postData = ksort(postData);
let postStr = '';
for (let key in postData) {
if (postData[key] !== '' && typeof postData[key] !== 'undefined' && key !== 'sign') {
postStr += key + '=' + postData[key] + '&';
}
}

postStr = postStr.substring(0, postStr.length - 1);
if (secretKey !== '' && typeof secretKey !== 'undefined') {
postStr += '&key=' + secretKey;
}

postStr = CryptoJS.MD5(postStr).toString().toUpperCase();

return postStr;
}

// 日期格式化
Date.prototype.format = function(format)
{
let o = {
"M+": this.getMonth() + 1,
"d+": this.getDate(),
"h+": this.getHours(),
"m+": this.getMinutes(),
"s+": this.getSeconds(),
"q+": Math.floor((this.getMonth() + 3) / 3),
"S": this.getMilliseconds()
};

if(/(y+)/.test(format)) {
format = format.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
}

for(let k in o) {
if(new RegExp("("+ k +")").test(format)) {
format = format.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length));
}
}

return format;
};

// ksort 排序
let ksort = function(inputArr, sort_flags) {
var tmp_arr = {},
keys = [],
sorter, i, k, that = this,
strictForIn = false,
populateArr = {};

switch (sort_flags) {
case 'SORT_STRING':
// compare items as strings
sorter = function (a, b) {
return that.strnatcmp(a, b);
};
break;
case 'SORT_LOCALE_STRING':
// compare items as strings, original by the current locale (set with i18n_loc_set_default() as of PHP6)
var loc = this.i18n_loc_get_default();
sorter = this.php_js.i18nLocales[loc].sorting;
break;
case 'SORT_NUMERIC':
// compare items numerically
sorter = function (a, b) {
return ((a + 0) - (b + 0));
};
break;
// case 'SORT_REGULAR': // compare items normally (don't change types)
default:
sorter = function (a, b) {
var aFloat = parseFloat(a),
bFloat = parseFloat(b),
aNumeric = aFloat + '' === a,
bNumeric = bFloat + '' === b;
if (aNumeric && bNumeric) {
return aFloat > bFloat ? 1 : aFloat < bFloat ? -1 : 0;
} else if (aNumeric && !bNumeric) {
return 1;
} else if (!aNumeric && bNumeric) {
return -1;
}
return a > b ? 1 : a < b ? -1 : 0;
};
break;
}

// Make a list of key names
for (k in inputArr) {
if (inputArr.hasOwnProperty(k)) {
keys.push(k);
}
}
keys.sort(sorter);

// BEGIN REDUNDANT
this.php_js = this.php_js || {};
this.php_js.ini = this.php_js.ini || {};
// END REDUNDANT
strictForIn = this.php_js.ini['phpjs.strictForIn'] && this.php_js.ini['phpjs.strictForIn'].local_value && this.php_js
.ini['phpjs.strictForIn'].local_value !== 'off';
populateArr = strictForIn ? inputArr : populateArr;

// Rebuild array with sorted key names
for (i = 0; i < keys.length; i++) {
k = keys[i];
tmp_arr[k] = inputArr[k];
if (strictForIn) {
delete inputArr[k];
}
}
for (i in tmp_arr) {
if (tmp_arr.hasOwnProperty(i)) {
populateArr[i] = tmp_arr[i];
}
}

return strictForIn || populateArr;
};

// 生成随机字符串
let generateRandom = function(len) {
len = len || 32;
let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let length = chars.length;
let str = '';
for (i = 0; i < len; i++) {
str += chars.charAt(Math.floor(Math.random() * length));
}

return str;
};
});
</script>
{% endblock content %}
PHP 模拟生成HEADER请求头及请求数据
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
/**
* 生成HEADER请求头
*/
// 请求参数
$params = [
// 用户登录凭证
'TOKEN' => '',
// 请求来源所属平台
'PLATFORM' => 'iOS',
// 平台版本
'VERSION' => '1.0',
// 随机字符串,用于验证请求是否重复
'NONCESTR' => Helper::generateRandom(16),
// 请求时间,用于验证请求是否超时
'TIMESTAMP' => date('Y-m-d H:i:s'),
];
// 生成AES KEY
$aesKey = Helper::generateRandom(32);
// 生成HEADER请求参数
$header = [
// RSA公钥加密$aesKey
'KEY' => RSA::encrypt($aesKey, $this->getCfgParameter('server_rsa_public_key')),
// AES加密$params
'VALUE' => AES::encrypt(base64_encode(json_encode($params)), $aesKey),
// API接口版本
'APIVER' => 2.2,
];

/**
* 生成请求数据
*/
$data = [
'account' => '13512345678',
'password' => '123456',
// 随机字符串,防止生成签名时重复
'noncestr' => Helper::generateRandom(),
// 请求时间,防止生成签名时重复
'timestamp' => date('Y-m-d H:i:s'),
];
// 第一种加签方式:使用客户端的RSA私钥加签
//$data['sign'] = RSA::generateSign($data, $this->getCfgParameter('client_rsa_private_key'));
// 第二种加签方式:使用服务端的密钥拼接生成签名
$data['sign'] = RSA::generateSignNoneOpenssl($data, $this->getCfgParameter('api_secret_key'));
0%