Auth.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. <?php
  2. namespace common\components\delivery\platform\fengniao;
  3. use common\components\delivery\helpers\HttpClient;
  4. use Yii;
  5. use yii\helpers\Json;
  6. use linslin\yii2\curl;
  7. class Auth
  8. {
  9. // 固定参数
  10. const GRANT_TYPE_AUTH_CODE = 'authorization_code';
  11. const GRANT_TYPE_REFRESH = 'refresh_token';
  12. // 授权端点 - 正式环境
  13. const TOKEN_URL_PRODUCTION = 'https://open-anubis.ele.me/anubis-webapi/openapi/token';
  14. const REFRESH_TOKEN_URL_PRODUCTION = 'https://open-anubis.ele.me/anubis-webapi/openapi/refreshToken';
  15. // 授权端点 - 沙箱环境
  16. const TOKEN_URL_SANDBOX = 'https://exam-anubis.ele.me/anubis-webapi/openapi/token';
  17. const REFRESH_TOKEN_URL_SANDBOX = 'https://exam-anubis.ele.me/anubis-webapi/openapi/refreshToken';
  18. protected $appId;
  19. protected $appSecret;
  20. protected $merchantId;
  21. protected $isSandbox;
  22. /**
  23. * 初始化授权类
  24. * 根据环境获取配置信息
  25. */
  26. public function __construct()
  27. {
  28. $isProduction = getenv('YII_ENV') == 'production'; // TODO 先变更为测试 --- production / dev
  29. // 配置(需要替换为真实的appId和appSecret)
  30. $this->appId = '6587209115185920913';// 3659064244254722812 -- 6587209115185920913
  31. $this->appSecret = '3f935d5f-bf65-467e-a61c-72cfb1d53960'; // dce31e3b-32da-45ed-8353-f2f041b5288f -- 3f935d5f-bf65-467e-a61c-72cfb1d53960
  32. $this->merchantId = ''; // 需要从配置中获取或外部设置
  33. // 根据环境设置沙箱标志
  34. $this->isSandbox = !$isProduction;
  35. }
  36. /**
  37. * 生成授权URL
  38. * 用户需要通过浏览器访问此URL进行授权
  39. *
  40. * @param string $redirectUri 重定向地址(回调地址)
  41. * @return string 授权URL
  42. */
  43. public function generateAuthUrl($redirectUri)
  44. {
  45. //$redirectUri = urlencode($redirectUri);
  46. $params = [
  47. 'appId' => $this->appId,
  48. 'devId' => 133146508, // 蜂鸟开放平台 【开发者中心】->【应用管理】->【发起商户授权】获得的
  49. 'authCallbackUrl' => $redirectUri,
  50. ];
  51. // 构建URL
  52. $queryString = http_build_query($params);
  53. return 'https://open.ele.me/app-auth?' . $queryString;
  54. }
  55. /**
  56. * 设置商户ID
  57. *
  58. * @param string $merchantId 商户ID
  59. */
  60. public function setMerchantId($merchantId)
  61. {
  62. $this->merchantId = $merchantId;
  63. return $this;
  64. }
  65. /**
  66. * 生成签名
  67. * 根据峰鸟开放平台API文档,签名算法采用SHA-256
  68. *
  69. * @param array $params 参数数组
  70. * @return string 签名结果
  71. */
  72. protected function generateSignature(array $params)
  73. {
  74. // Step 1: 过滤空值 -- 不能清除,不然 code 就被排除了
  75. // $params = array_filter($params, function ($v) {
  76. // return $v !== null && $v !== '';
  77. // });
  78. // Step 2: 按字典序排序
  79. ksort($params);
  80. // Step 3: 拼接成字符串
  81. $paramStr = '';
  82. $first = true;
  83. foreach ($params as $key => $value) {
  84. // if ($value == "") {
  85. // $value = "''";
  86. // }
  87. if ($first) {
  88. $paramStr = "{$key}={$value}";
  89. $first = false;
  90. } else {
  91. $paramStr .= "&{$key}={$value}";
  92. }
  93. }
  94. // Step 4: 拼接 appSecret
  95. $signBefore = $this->appSecret . $paramStr;
  96. //$signBefore = '1111app_id=222&code=4444&grant_type=authorization_code&merchant_id=333&timestamp=1719297100558';
  97. // Step 5: 使用SHA-256加密
  98. $signature = hash('sha256', $signBefore);
  99. Yii::info("[FengniaAuth] Sign Before: {$signBefore}, Signature: {$signature}");
  100. return $signature;
  101. }
  102. /**
  103. * 获取 Token URL
  104. *
  105. * @return string
  106. */
  107. protected function getTokenUrl()
  108. {
  109. return $this->isSandbox ? self::TOKEN_URL_SANDBOX : self::TOKEN_URL_PRODUCTION;
  110. }
  111. /**
  112. * 获取刷新 Token URL
  113. *
  114. * @return string
  115. */
  116. protected function getRefreshTokenUrl()
  117. {
  118. return $this->isSandbox ? self::REFRESH_TOKEN_URL_SANDBOX : self::REFRESH_TOKEN_URL_PRODUCTION;
  119. }
  120. /**
  121. * 获取AccessToken
  122. * 使用授权码换取令牌
  123. *
  124. * 文档:获取token接口
  125. * 入参:grant_type, code, app_id, merchant_id, timestamp, signature
  126. * 出参:access_token, refresh_token, expire_in
  127. *
  128. * @param string $code 授权码(来自授权回调)
  129. * @param string $merchantId 商户ID(可选,如果已设置则使用已设置的)
  130. * @return array 返回格式:['success' => true/false, 'data' => [...], 'error' => '']
  131. */
  132. public function getAccessToken($code, $merchantId = null)
  133. {
  134. if ($merchantId) {
  135. $this->setMerchantId($merchantId);
  136. }
  137. if (empty($this->merchantId)) {
  138. return [
  139. 'success' => false,
  140. 'error' => '商户ID未设置',
  141. ];
  142. }
  143. $timestamp = (int)(microtime(true) * 1000); // 毫秒级时间戳
  144. $params = [
  145. 'grant_type' => self::GRANT_TYPE_AUTH_CODE,
  146. 'code' => $code,
  147. 'app_id' => $this->appId,
  148. 'merchant_id' => $this->merchantId,
  149. 'timestamp' => $timestamp,
  150. ];
  151. // 沙箱环境不传code
  152. if (getenv('YII_ENV') !== 'production') {
  153. //unset($params['code']);
  154. //$params['code'] = "";
  155. }
  156. // 生成签名
  157. $signature = $this->generateSignature($params);
  158. $params['signature'] = $signature;
  159. $url = $this->getTokenUrl();
  160. // 发送POST请求
  161. $response = HttpClient::post($url, $params, [
  162. 'Content-Type' => 'application/json',
  163. ]);
  164. // $curl = new curl\Curl();
  165. // $response = $curl->setOption(CURLOPT_POSTFIELDS, Json::encode($params))->post($url);
  166. // $response = Json::decode($response);
  167. Yii::info("[FengniaAuth] GetAccessToken Response: " . json_encode($response));
  168. return $this->parseResponse($response);
  169. }
  170. /**
  171. * 刷新AccessToken
  172. * 使用刷新令牌获取新的AccessToken
  173. *
  174. * 文档:刷新token接口
  175. * 入参:grant_type, app_id, merchant_id, timestamp, refresh_token, signature
  176. * 出参:access_token, refresh_token, expire_in
  177. *
  178. * @param string $refreshToken 刷新令牌
  179. * @param string $merchantId 商户ID(可选,如果已设置则使用已设置的)
  180. * @return array 返回格式:['success' => true/false, 'data' => [...], 'error' => '']
  181. */
  182. public function refreshAccessToken($refreshToken, $merchantId = null)
  183. {
  184. if ($merchantId) {
  185. $this->setMerchantId($merchantId);
  186. }
  187. if (empty($this->merchantId)) {
  188. return [
  189. 'success' => false,
  190. 'error' => '商户ID未设置',
  191. ];
  192. }
  193. $timestamp = (string)(time() * 1000); // 毫秒级时间戳
  194. $params = [
  195. 'grant_type' => self::GRANT_TYPE_REFRESH,
  196. 'app_id' => $this->appId,
  197. 'merchant_id' => $this->merchantId,
  198. 'timestamp' => $timestamp,
  199. 'refresh_token' => $refreshToken,
  200. ];
  201. // 生成签名
  202. $signature = $this->generateSignature($params);
  203. $params['signature'] = $signature;
  204. $url = $this->getRefreshTokenUrl();
  205. // 发送POST请求
  206. $response = HttpClient::post($url, $params, [
  207. 'Content-Type' => 'application/json',
  208. ]);
  209. Yii::info("[FengniaAuth] RefreshAccessToken Response: " . json_encode($response));
  210. return $this->parseResponse($response);
  211. }
  212. /**
  213. * 解析API响应
  214. *
  215. * 峰鸟开放平台 API 返回格式:
  216. * {
  217. * "sign": "返回值签名",
  218. * "code": "200|错误码",
  219. * "msg": "错误信息",
  220. * "business_data": {
  221. * "app_id": "应用id",
  222. * "merchant_id": "商户id",
  223. * "access_token": "凭证token",
  224. * "refresh_token": "刷新token",
  225. * "expire_in": "access_token剩余有效时间,单位:秒",
  226. * "re_expire_in": "refresh_token剩余有效时间"
  227. * }
  228. * }
  229. *
  230. * @param array $response HTTP响应
  231. * @return array 标准化的响应格式
  232. */
  233. protected function parseResponse($response)
  234. {
  235. // 检查 code 字段
  236. if (!isset($response['code'])) {
  237. return [
  238. 'success' => false,
  239. 'error' => $response['msg'] ?? '响应格式错误,缺少 code 字段',
  240. ];
  241. }
  242. // 如果 code 不等于 '200' 或 200,表示请求失败
  243. if ((string)$response['code'] !== '200') {
  244. return [
  245. 'success' => false,
  246. 'error' => $response['msg'] ?? 'API返回异常',
  247. 'code' => $response['code'],
  248. ];
  249. }
  250. // 提取 business_data 字段中的数据
  251. $businessData = $response['business_data'] ?? [];
  252. if (is_string($businessData)) {
  253. $businessData = json_decode($businessData, true);
  254. }
  255. // 检查是否包含必需的token信息
  256. if (empty($businessData['access_token'])) {
  257. return [
  258. 'success' => false,
  259. 'error' => $response['msg'] ?? '未获取到 access_token',
  260. ];
  261. }
  262. // 成功响应
  263. return [
  264. 'success' => true,
  265. 'data' => [
  266. 'access_token' => $businessData['access_token'] ?? '',
  267. 'refresh_token' => $businessData['refresh_token'] ?? '',
  268. 'app_id' => $businessData['app_id'] ?? '',
  269. 'merchant_id' => $businessData['merchant_id'] ?? '',
  270. 'expire_in' => $businessData['expire_in'] ?? 0,
  271. 're_expire_in' => $businessData['re_expire_in'] ?? 0,
  272. ],
  273. ];
  274. }
  275. /**
  276. * 获取AppId
  277. *
  278. * @return string
  279. */
  280. public function getAppId()
  281. {
  282. return $this->appId;
  283. }
  284. /**
  285. * 获取AppSecret
  286. *
  287. * @return string
  288. */
  289. public function getAppSecret()
  290. {
  291. return $this->appSecret;
  292. }
  293. /**
  294. * 获取商户ID
  295. *
  296. * @return string
  297. */
  298. public function getMerchantId()
  299. {
  300. return $this->merchantId;
  301. }
  302. /**
  303. * 获取是否为沙箱环境
  304. *
  305. * @return bool
  306. */
  307. public function isSandbox()
  308. {
  309. return $this->isSandbox;
  310. }
  311. }