host = env('DY_API_URL'); $this->app_id = env('DY_APPID'); $this->app_secret = env('DY_APPSECRET'); } /** * @param $url * @param array $data * @param array $extra * @return array * @throws \GuzzleHttp\Exception\GuzzleException */ public function send($url, array $data = [], $extra = []): array { $signData = [ 'Api-App-Key' => $this->app_id, 'Api-Time-Stamp' => time() * 1000, ]; // 组装请求数据 $reqData = $data; // 请求头加上签名 // $header['Api-Sign'] = $this->getSign($signData, $reqData); $header['access-token'] = $this->getAccessToken(); if ($this->app_id == 'sandbox289') { $header['x-sandbox-token'] = 1; } return $this->request($url, $reqData, $header, $extra); } /** * 获取token * @return bool|mixed|string * @throws Exception * @throws \GuzzleHttp\Exception\GuzzleException */ private function getAccessToken() { // 测试token if ($this->app_id == 'sandbox289') { return 'ka.F9tiX0EN5QB5plK43W22p3cDgji9O9ZTAigryyd1A7AlbDwTmDxEN0PwjfI9'; } $key = self::DY_TOKEN_KEY; if ($token = Redis::get($key)) { return $token; } $reqData = [ 'client_key' => $this->app_id, 'client_secret' => $this->app_secret, 'grant_type' => 'client_credential', ]; $res = $this->request(self::CLIENT_TOKEN, $reqData); if (!$res['flag'] || !isset($res['data']['access_token'])) { throw new Exception('获取token失败'); } Redis::setEx($key, $res['data']['expires_in'], $res['data']['access_token']); return $res['data']['access_token']; } /** * 获取签名 */ public function getSign($signData, $postData): string { // $signData = array_merge($signData, $postData); $buff = ""; ksort($signData); // 将key和value按照顺序直接拼接到一起 foreach ($signData as $k => $v){ if (!empty($v)) { if (is_array($v)) { $v = json_encode(ksort($v), JSON_UNESCAPED_UNICODE); } $buff .= $k . $v; } } // 签名步骤三:在上一步的结果后直接拼secretKey $str = $buff . $this->app_secret; // 在上一步结果后直接拼接,经过去除空白字符(\s)的HTTP Body原始字符串 $str .= preg_replace('/\s+/', '', json_encode($postData)); // 对上一步结果进行sha1加密,得到16进制字符串 $str = sha1($str); // 对上一步结果进行md5加密,得到16进制字符串 // 将上一步结果转为大写 return strtoupper(md5($str)); } /** * @param string $url * @param array $data * @param array $header * @param array $extra * @return array * @throws \GuzzleHttp\Exception\GuzzleException */ private function request(string $url, array $data = [], $header = []): array { $client = new \GuzzleHttp\Client(); try { $reqUrl = $this->host . $url; $header['Content-Type'] = 'application/json'; $header['charset'] = 'UTF-8'; $options['headers'] = $header; $options['json'] = $data; $method = 'POST'; $response = $client->request($method, $reqUrl, $options); Log::info(sprintf('dy url:%s, req:%s, response:%s', $reqUrl, json_encode($data), $response->getBody())) ; $result = ['code' => $response->getStatusCode(), 'body' => json_decode($response->getBody(), true, 512, JSON_BIGINT_AS_STRING)]; if (isset($result['code']) && $result['code'] == 200) { if ($result['body']['data']['error_code'] == '0') { return ['flag' => true, 'data' => $result['body']['data'] ?? [], 'message' => $result['body']['data']['description '] ?? '']; } else { return ['flag' => false, 'data' => $result['body']['data'] ?? [], 'message' => $result['body']['data']['description '] ?? '']; } } return ['flag' => false, 'message' => $result['body']['data']['description'] ?? '', 'data' => []]; } catch (\Exception $exception) { Log::info(sprintf('dy req fail:%s, req:%s,res:%s', $exception->getMessage(), $exception->getFile(), $exception->getLine())) ; return ['flag' => false, 'message' => $exception->getMessage(), 'data' => []]; } } /** 1.根据ClientKev找到ClientSecret,将ClientSecret向左右使用字符补齐32位/裁剪至32位,补齐:补位字符:#,先补左侧再补右侧再补左侧………直到补满32位。 裁剪:先裁剪左侧再裁右侧再裁左侧………直到剩余32位。(正常不需要补齐,secret默认为32位,此举是为了 以防万一) 2.将ClientSecret作为Key,右侧16位为向量IV 3.将密文进行base64解码。 4.使用AES-256-CBC模式解密解码后的密文,对齐使用PKCS5Padding方式 * @param $cipherText * @param $clientSecret * @return false|string */ public function decrypt($cipherText, $clientSecret) { Log::info('$cipherText:' . $cipherText); // 补齐或裁剪 ClientSecret 为 32 位 $clientSecret = $this->padOrTruncate($clientSecret); Log::info('$cipherText step 2:' . $clientSecret); // 使用 ClientSecret 作为密钥 $key = $clientSecret; // 右侧16位为初始化向量IV $iv = substr($key, 16, 16); // 将密文 base64 解码 $cipherTextDecoded = base64_decode($cipherText); // 使用 AES-256-CBC 解密 return openssl_decrypt($cipherTextDecoded, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv); } public function encrypt($cipherText, $clientSecret) { Log::info('$cipherText:' . $cipherText); // 补齐或裁剪 ClientSecret 为 32 位 $clientSecret = $this->padOrTruncate($clientSecret); Log::info('$cipherText step 2:' . $clientSecret); // 使用 ClientSecret 作为密钥 $key = $clientSecret; // 右侧16位为初始化向量IV $iv = substr($key, 16, 16); // 使用 AES-256-CBC 解密 return base64_encode(openssl_encrypt($cipherText, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv)); } /** * @param $clientSecret * @return false|string */ private function padOrTruncate($clientSecret) { // 1. 如果 clientSecret 长度为32,直接返回 if (strlen($clientSecret) == 32) { return $clientSecret; } // 2. 如果 length < 32,补齐至32位 if (strlen($clientSecret) < 32) { // 补齐过程:先补左侧,再补右侧,再补左侧,直到长度为32 $paddingChar = '#'; $padded = $clientSecret; // 左侧补齐 $left = true; while (strlen($padded) < 32) { if ($left) { $padded = $paddingChar . $padded; $left = false; } else { $padded = $padded . $paddingChar; $left = true; } } return substr($padded, 0, 32); // 确保返回32位 } // 3. 如果 length > 32,裁剪至32位 if (strlen($clientSecret) > 32) { // 裁剪过程:先裁左侧,再裁右侧,再裁左侧,直到长度为32 $cropped = $clientSecret; // 左侧裁剪 while (strlen($cropped) > 32) { $cropped = substr($cropped, 1); } // 右侧裁剪 while (strlen($cropped) > 32) { $cropped = substr($cropped, 0, -1); } // 左侧裁剪(再一次) while (strlen($cropped) > 32) { $cropped = substr($cropped, 1); } return substr($cropped, 0, 32); // 确保返回32位 } return $clientSecret; } }