V2LakalaApi.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. <?php
  2. namespace SixShop\Lakala\OpenAPISDK\V2\Api;
  3. use SixShop\Lakala\OpenAPISDK\V2\V2ApiException;
  4. use SixShop\Lakala\OpenAPISDK\V2\V2ObjectSerializer;
  5. use SixShop\Lakala\OpenAPISDK\V2\V2Configuration;
  6. use SixShop\Lakala\OpenAPISDK\V2\Util\V2HttpService;
  7. use SixShop\Lakala\OpenAPISDK\V2\Util\V2LakalaSM4;
  8. use SixShop\Lakala\OpenAPISDK\V2\Model\V2ModelRequest;
  9. use SixShop\Lakala\OpenAPISDK\V2\Model\V2ModelResponse;
  10. /**
  11. * LakalaApi Class
  12. *
  13. * @category Class
  14. * @package Lakala\OpenAPISDK\V2\Api
  15. * @author lucongyu
  16. * @link https://o.lakala.com
  17. */
  18. class V2LakalaApi
  19. {
  20. /**
  21. * @var string 随机字符串集
  22. */
  23. protected $charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  24. /**
  25. * 算法
  26. * @var string
  27. */
  28. protected $schema = "LKLAPI-SHA256withRSA";
  29. // 请求响应body使用SM4加密类型
  30. protected $v2EncryptMode;
  31. protected $config;
  32. protected $v2HttpService;
  33. protected $resourcePath;
  34. public function __construct(V2Configuration $config, $v2EncryptMode = V2EncryptMode::NONE)
  35. {
  36. $this->v2EncryptMode = $v2EncryptMode;
  37. $this->config = $config;
  38. $this->v2HttpService = new \SixShop\Lakala\OpenAPISDK\V2\Util\V2HttpService();
  39. }
  40. public function setResourcePath($resourcePath)
  41. {
  42. $this->resourcePath = $resourcePath;
  43. return $this;
  44. }
  45. public function getResourcePath()
  46. {
  47. return $this->resourcePath;
  48. }
  49. public function tradeApi($resourcePath, V2ModelRequest $v2ModelRequest,
  50. $returnType = '\Lakala\OpenAPISDK\V2\Model\V2ModelResponse',
  51. $method = 'POST')
  52. {
  53. $headerParams = [];
  54. if (!$v2ModelRequest->valid()) {
  55. throw new V2ApiException(implode(',', $v2ModelRequest->listInvalidProperties()));
  56. }
  57. $httpBody = $v2ModelRequest->jsonSerialize();
  58. if ($httpBody !== null) {
  59. $httpBody = json_encode(V2ObjectSerializer::sanitizeForSerialization($httpBody));
  60. }
  61. return $this->apiWithBody($resourcePath, $httpBody, $headerParams, $method, $returnType);
  62. }
  63. public function apiWithBody($resourcePath, $httpBody, $headerParams = [], $method = 'POST',
  64. $returnType = '\Lakala\OpenAPISDK\V2\Model\V2ModelResponse')
  65. {
  66. $this->setResourcePath($resourcePath);
  67. if (!is_string($httpBody)) {
  68. $httpBody = json_encode($httpBody);
  69. }
  70. list($response, $statusCode, $httpHeader) = $this->callApi(
  71. $method,
  72. $httpBody,
  73. $headerParams,
  74. $returnType
  75. );
  76. if ($statusCode < 200 || $statusCode > 299) {
  77. throw new V2ApiException(
  78. sprintf('[%d] 连接API时出错 (%s)', $statusCode, $this->getResourcePath()),
  79. $statusCode,
  80. $httpHeader,
  81. $response
  82. );
  83. }
  84. return $response;
  85. }
  86. protected function callApi($method, $httpBody, $headerParams, $returnType)
  87. {
  88. $request = $this->prepareRequest($method, $httpBody, $headerParams);
  89. try {
  90. $options = $this->createRequestOptions();
  91. $options['header'] = $request['headers'];
  92. $options['data'] = $request['body'];
  93. $response = $this->v2HttpService->request($method, $request['url'], $options);
  94. } catch (Exception $e) {
  95. throw new V2ApiException("[{$e->getCode()}] {$e->getMessage()}", $e->getCode(), null, null);
  96. }
  97. $statusCode = $response['info']['http_code'];
  98. $responseHeaders = isset($response['header']) ? $response['header'] : null;
  99. if ($statusCode < 200 || $statusCode > 299) {
  100. throw new V2ApiException(
  101. sprintf('[%d] 连接API时出错 (%s)', $statusCode, $request['url']),
  102. $statusCode,
  103. $responseHeaders,
  104. $response['body']
  105. );
  106. }
  107. $responseVerifySign = $this->responseVerifySign($responseHeaders, $response['body']);
  108. if (!$responseVerifySign) {
  109. throw new V2ApiException(
  110. sprintf('[%d] 验证拉卡拉响应验签错误 (%s)', $statusCode, $request['url']),
  111. $statusCode,
  112. $responseHeaders,
  113. $response['body']
  114. );
  115. }
  116. // 请求咱解密
  117. if($this->v2EncryptMode == V2EncryptMode::RESPONSE || $this->v2EncryptMode == V2EncryptMode::BOTH) {
  118. // echo "\n<!-- \n" . $response['body'] . "\n--->\n";
  119. $sm4 = new V2LakalaSM4();
  120. $body = $sm4->decrypt(base64_decode($this->config->getSm4Key()), $response['body']);
  121. $response['content'] = json_decode($body);
  122. $response['body'] = $body;
  123. }
  124. $response['content']->originalText = $response['body'];
  125. return [
  126. V2ObjectSerializer::deserialize($response['content'], $returnType, $responseHeaders),
  127. $statusCode,
  128. $responseHeaders
  129. ];
  130. }
  131. protected function prepareRequest($method, $httpBody, $headerParams)
  132. {
  133. $url = $this->config->getHost() . $this->getResourcePath();
  134. // SM4加密请求体
  135. if($this->v2EncryptMode == V2EncryptMode::REQUEST || $this->v2EncryptMode == V2EncryptMode::BOTH) {
  136. $sm4 = new V2LakalaSM4();
  137. $httpBody = $sm4->encrypt(base64_decode($this->config->getSm4Key()), $httpBody);
  138. }
  139. $headers = $this->createHeaderParams($headerParams, $httpBody);
  140. return [
  141. 'method' => $method,
  142. 'url' => $url,
  143. 'headers' => $headers,
  144. 'body' => $httpBody,
  145. ];
  146. }
  147. protected function createHeaderParams($headerParams, $httpBody)
  148. {
  149. $headers = $this->config->getDefaultHeaders();
  150. if ($headerParams) {
  151. $headers = array_merge($headers, $headerParams);
  152. }
  153. $authorization = $this->getAuthorization($httpBody);
  154. $headers[] = 'Content-Type: application/json';
  155. $headers[] = "Authorization: $authorization";
  156. return $headers;
  157. }
  158. protected function createRequestOptions()
  159. {
  160. $options = [
  161. 'timeout' => 10,
  162. 'respond_type' => \Lakala\OpenAPISDK\V2\Util\V2HttpService::RESPOND_TYPE_ARRAY,
  163. ];
  164. return $options;
  165. }
  166. protected function getAuthorization($body)
  167. {
  168. $randomString = $this->getRandomString(12);
  169. $timestamp = time();
  170. $data = $this->config->getAppId() . "\n"
  171. . $this->config->getSerialNo() . "\n"
  172. . $timestamp . "\n"
  173. . $randomString . "\n"
  174. . $body . "\n";
  175. $sign = $this->rsaSign($data);
  176. $authorization = $this->schema . " appid=\"" . $this->config->getAppId() . "\","
  177. . "serial_no=\"" . $this->config->getSerialNo() . "\","
  178. . "timestamp=\"" . $timestamp . "\","
  179. . "nonce_str=\"" . $randomString . "\","
  180. . "signature=\"" . $sign . "\"";
  181. return $authorization;
  182. }
  183. protected function getRandomString($length = 10) {
  184. $randomString = '';
  185. $charsetLength = strlen($this->charset);
  186. // 生成随机字符串
  187. for ($i = 0; $i < $length; $i++) {
  188. $randomChar = $this->charset[rand(0, $charsetLength - 1)]; // 随机选择字符
  189. $randomString .= $randomChar;
  190. }
  191. return $randomString;
  192. }
  193. //生成 sha256WithRSA 签名
  194. protected function rsaSign($content) {
  195. $privateContent = file_get_contents($this->config->getMerchantPrivateKeyPath());
  196. $privateKey = openssl_pkey_get_private($privateContent);
  197. if (!$privateKey) {
  198. throw new V2ApiException('获取私钥失败');
  199. }
  200. $res = openssl_sign($content, $sign, $privateKey, OPENSSL_ALGO_SHA256);
  201. if (function_exists('openssl_free_key')) {
  202. openssl_free_key($privateKey);
  203. }
  204. if (!$res) {
  205. throw new V2ApiException('[10004] 拉卡拉字符串签名失败');
  206. }
  207. return base64_encode($sign);
  208. }
  209. protected function responseVerifySign($headers, $body) {
  210. $sign = $headers['Lklapi-Signature'];
  211. $sign = base64_decode($sign);
  212. $data = $headers['Lklapi-Appid'] . "\n"
  213. . $headers['Lklapi-Serial'] . "\n"
  214. . $headers['Lklapi-Timestamp'] . "\n"
  215. . $headers['Lklapi-Nonce'] . "\n"
  216. . $body . "\n";
  217. // $dir = dirname(__FILE__);
  218. // $dir = str_replace('/src/Api', '', $dir);
  219. $certContent = file_get_contents($this->config->getLklCertificatePath());
  220. $key = openssl_pkey_get_public($certContent);
  221. $result = openssl_verify($data, $sign, $key, OPENSSL_ALGO_SHA256) === 1;
  222. return $result;
  223. }
  224. }