ApiCrypto.php 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Http\Middleware;
  4. use App\Exceptions\ApiCryptoException;
  5. use App\Services\ApiCryptoService;
  6. use Closure;
  7. use Illuminate\Http\Request;
  8. use JsonException;
  9. use RuntimeException;
  10. use Symfony\Component\HttpFoundation\JsonResponse;
  11. use Symfony\Component\HttpFoundation\Response;
  12. class ApiCrypto
  13. {
  14. private const HEADER = 'X-Api-Encrypted';
  15. public function handle(Request $request, Closure $next): Response
  16. {
  17. $method = $request->getRealMethod();
  18. if (!config('api_crypto.enabled') || $method === 'OPTIONS' || !$request->is(
  19. 'api/wallet/*',
  20. 'api/newPc', 'api/newPc/*',
  21. 'api/issue', 'api/issue/*',
  22. 'api/pcissue', 'api/pcissue/*',
  23. )) {
  24. return $next($request);
  25. }
  26. try {
  27. $crypto = new ApiCryptoService(
  28. (string) config('api_crypto.key'),
  29. (int) config('api_crypto.timestamp_tolerance', 300),
  30. (int) config('api_crypto.max_payload_bytes', 2097152),
  31. );
  32. } catch (RuntimeException) {
  33. return $this->error(5001, 'API 加密配置错误,请联系服务端管理员', 500)->prepare($request);
  34. }
  35. if ($request->headers->get(self::HEADER) !== '1') {
  36. if (config('api_crypto.allow_plaintext')) {
  37. return $next($request);
  38. }
  39. return $this->encryptResponse($this->error(4001, '请求必须使用加密协议', 400), $crypto, $request);
  40. }
  41. $queryMethod = in_array($method, ['GET', 'HEAD'], true);
  42. try {
  43. $payload = $queryMethod
  44. ? ($request->query->all()['payload'] ?? null)
  45. : ($request->isJson() ? $request->json('payload') : null);
  46. if (!is_string($payload) || $payload === '') {
  47. throw new ApiCryptoException('请求缺少加密 payload');
  48. }
  49. $parameters = $crypto->decrypt($payload, ApiCryptoService::DIRECTION_REQUEST);
  50. $this->replaceParameters($request, $parameters, $queryMethod);
  51. } catch (ApiCryptoException | JsonException $exception) {
  52. return $this->encryptResponse($this->error(4002, $exception->getMessage(), 400), $crypto, $request);
  53. }
  54. return $this->encryptResponse($next($request), $crypto, $request);
  55. }
  56. private function replaceParameters(Request $request, array $parameters, bool $queryMethod): void
  57. {
  58. $headers = $request->headers->all();
  59. $server = $request->server->all();
  60. $server['REQUEST_URI'] = explode('?', $request->getRequestUri(), 2)[0];
  61. $server['QUERY_STRING'] = '';
  62. // The HTTP verb belongs to the transport, not to the wrapped parameters.
  63. unset($parameters['_method'], $headers['x-http-method-override'], $server['HTTP_X_HTTP_METHOD_OVERRIDE']);
  64. $headers['accept'] = ['application/json'];
  65. $server['HTTP_ACCEPT'] = 'application/json';
  66. $body = $queryMethod ? '' : json_encode((object) $parameters, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
  67. unset($headers['content-length'], $server['CONTENT_LENGTH']);
  68. $request->initialize(
  69. $queryMethod ? $parameters : [],
  70. $queryMethod ? [] : $parameters,
  71. $request->attributes->all(),
  72. $request->cookies->all(),
  73. [],
  74. $server,
  75. $body,
  76. );
  77. $request->headers->replace($headers);
  78. $request->setJson($queryMethod ? $request->query : $request->request);
  79. }
  80. private function encryptResponse(Response $response, ApiCryptoService $crypto, Request $request): Response
  81. {
  82. $response->headers->set('Cache-Control', 'no-store');
  83. $response->headers->set(self::HEADER, '1');
  84. $response->headers->remove('Content-Length');
  85. $response->headers->remove('ETag');
  86. if ($request->getRealMethod() === 'HEAD' || $response->isEmpty()) {
  87. $response->setContent('');
  88. return $response;
  89. }
  90. try {
  91. $data = json_decode((string) $response->getContent(), false, 512, JSON_THROW_ON_ERROR);
  92. $envelope = ['encrypted' => true, 'payload' => $crypto->encrypt($data, ApiCryptoService::DIRECTION_RESPONSE)];
  93. if ($response instanceof JsonResponse) {
  94. $response->setData($envelope);
  95. } else {
  96. $response->setContent(json_encode($envelope, JSON_THROW_ON_ERROR));
  97. $response->headers->set('Content-Type', 'application/json');
  98. }
  99. return $response;
  100. } catch (ApiCryptoException | JsonException) {
  101. return $this->error(5002, 'API 响应加密失败', 500);
  102. }
  103. }
  104. private function error(int $code, string $message, int $status): JsonResponse
  105. {
  106. return new JsonResponse(['code' => $code, 'show' => 1, 'msg' => $message, 'data' => []], $status, [
  107. 'Cache-Control' => 'no-store',
  108. ]);
  109. }
  110. }