Auth.php 9.8 KB

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