V2LakalaApi.php 9.2 KB

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