CredentialLogic.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. <?php
  2. namespace App\Module\ThirdParty\Logics;
  3. use App\Module\ThirdParty\Models\ThirdPartyService;
  4. use App\Module\ThirdParty\Models\ThirdPartyCredential;
  5. use App\Module\ThirdParty\Enums\AUTH_TYPE;
  6. use Illuminate\Support\Collection;
  7. use Illuminate\Support\Facades\Crypt;
  8. /**
  9. * 第三方服务认证凭证业务逻辑类
  10. */
  11. class CredentialLogic
  12. {
  13. /**
  14. * 创建新的认证凭证
  15. *
  16. * @param array $data
  17. * @return ThirdPartyCredential
  18. * @throws \Exception
  19. */
  20. public static function createCredential(array $data): ThirdPartyCredential
  21. {
  22. // 验证服务是否存在
  23. $service = ThirdPartyService::find($data['service_id']);
  24. if (!$service) {
  25. throw new \Exception("服务不存在: {$data['service_id']}");
  26. }
  27. // 验证认证类型
  28. $authType = AUTH_TYPE::tryFrom($data['type']);
  29. if (!$authType) {
  30. throw new \Exception("不支持的认证类型: {$data['type']}");
  31. }
  32. // 验证认证类型与服务匹配
  33. if ($data['type'] !== $service->auth_type) {
  34. throw new \Exception("认证类型 {$data['type']} 与服务要求的 {$service->auth_type} 不匹配");
  35. }
  36. // 验证凭证配置完整性
  37. $validation = static::validateCredentialData($authType, $data['credentials'] ?? []);
  38. if (!$validation['valid']) {
  39. throw new \Exception("凭证配置不完整,缺少字段: " . implode(', ', $validation['missing_fields']));
  40. }
  41. // 检查同环境下是否已有激活的凭证
  42. $environment = $data['environment'] ?? 'production';
  43. $existingActive = ThirdPartyCredential::where('service_id', $data['service_id'])
  44. ->where('environment', $environment)
  45. ->where('is_active', true)
  46. ->exists();
  47. if ($existingActive && ($data['is_active'] ?? true)) {
  48. throw new \Exception("环境 {$environment} 下已有激活的凭证,请先停用现有凭证");
  49. }
  50. // 设置默认值
  51. $data = array_merge([
  52. 'environment' => 'production',
  53. 'is_active' => true,
  54. 'usage_count' => 0,
  55. ], $data);
  56. // 创建凭证
  57. $credential = new ThirdPartyCredential();
  58. $credential->fill($data);
  59. // 加密敏感信息
  60. if (isset($data['credentials'])) {
  61. $credential->setEncryptedCredentials($data['credentials']);
  62. }
  63. $credential->save();
  64. return $credential;
  65. }
  66. /**
  67. * 更新认证凭证
  68. *
  69. * @param ThirdPartyCredential $credential
  70. * @param array $data
  71. * @return bool
  72. * @throws \Exception
  73. */
  74. public static function updateCredential(ThirdPartyCredential $credential, array $data): bool
  75. {
  76. // 如果更新认证类型,验证类型有效性
  77. if (isset($data['type'])) {
  78. $authType = AUTH_TYPE::tryFrom($data['type']);
  79. if (!$authType) {
  80. throw new \Exception("不支持的认证类型: {$data['type']}");
  81. }
  82. // 验证认证类型与服务匹配
  83. if ($data['type'] !== $credential->service->auth_type) {
  84. throw new \Exception("认证类型 {$data['type']} 与服务要求的 {$credential->service->auth_type} 不匹配");
  85. }
  86. }
  87. // 如果更新凭证信息,验证完整性
  88. if (isset($data['credentials'])) {
  89. $authType = AUTH_TYPE::from($data['type'] ?? $credential->type);
  90. $validation = static::validateCredentialData($authType, $data['credentials']);
  91. if (!$validation['valid']) {
  92. throw new \Exception("凭证配置不完整,缺少字段: " . implode(', ', $validation['missing_fields']));
  93. }
  94. }
  95. // 如果要激活凭证,检查同环境下是否已有其他激活的凭证
  96. if (isset($data['is_active']) && $data['is_active']) {
  97. $environment = $data['environment'] ?? $credential->environment;
  98. $existingActive = ThirdPartyCredential::where('service_id', $credential->service_id)
  99. ->where('environment', $environment)
  100. ->where('is_active', true)
  101. ->where('id', '!=', $credential->id)
  102. ->exists();
  103. if ($existingActive) {
  104. throw new \Exception("环境 {$environment} 下已有其他激活的凭证,请先停用其他凭证");
  105. }
  106. }
  107. // 更新基本信息
  108. $updateData = array_except($data, ['credentials']);
  109. if (!empty($updateData)) {
  110. $credential->update($updateData);
  111. }
  112. // 更新凭证信息
  113. if (isset($data['credentials'])) {
  114. $credential->setEncryptedCredentials($data['credentials']);
  115. $credential->save();
  116. }
  117. return true;
  118. }
  119. /**
  120. * 删除认证凭证
  121. *
  122. * @param ThirdPartyCredential $credential
  123. * @return bool
  124. * @throws \Exception
  125. */
  126. public static function deleteCredential(ThirdPartyCredential $credential): bool
  127. {
  128. // 检查凭证是否正在使用
  129. if ($credential->is_active) {
  130. throw new \Exception("激活状态的凭证无法删除,请先停用凭证");
  131. }
  132. // 检查最近是否有使用记录
  133. if ($credential->last_used_at && $credential->last_used_at->gt(now()->subHours(1))) {
  134. throw new \Exception("凭证在1小时内有使用记录,无法删除");
  135. }
  136. return $credential->delete();
  137. }
  138. /**
  139. * 激活/停用凭证
  140. *
  141. * @param ThirdPartyCredential $credential
  142. * @param bool $active
  143. * @return bool
  144. * @throws \Exception
  145. */
  146. public static function toggleCredentialStatus(ThirdPartyCredential $credential, bool $active): bool
  147. {
  148. if ($active) {
  149. // 激活凭证前检查
  150. if ($credential->isExpired()) {
  151. throw new \Exception("凭证已过期,无法激活");
  152. }
  153. // 检查同环境下是否已有其他激活的凭证
  154. $existingActive = ThirdPartyCredential::where('service_id', $credential->service_id)
  155. ->where('environment', $credential->environment)
  156. ->where('is_active', true)
  157. ->where('id', '!=', $credential->id)
  158. ->exists();
  159. if ($existingActive) {
  160. throw new \Exception("环境 {$credential->environment} 下已有其他激活的凭证,请先停用其他凭证");
  161. }
  162. }
  163. return $credential->update(['is_active' => $active]);
  164. }
  165. /**
  166. * 获取凭证列表
  167. *
  168. * @param array $filters
  169. * @param array $options
  170. * @return Collection
  171. */
  172. public static function getCredentialList(array $filters = [], array $options = []): Collection
  173. {
  174. $query = ThirdPartyCredential::with('service');
  175. // 应用过滤条件
  176. if (isset($filters['service_id'])) {
  177. $query->where('service_id', $filters['service_id']);
  178. }
  179. if (isset($filters['type'])) {
  180. $query->where('type', $filters['type']);
  181. }
  182. if (isset($filters['environment'])) {
  183. $query->where('environment', $filters['environment']);
  184. }
  185. if (isset($filters['is_active'])) {
  186. $query->where('is_active', $filters['is_active']);
  187. }
  188. if (isset($filters['expired'])) {
  189. if ($filters['expired']) {
  190. $query->where('expires_at', '<=', now());
  191. } else {
  192. $query->where(function ($q) {
  193. $q->whereNull('expires_at')
  194. ->orWhere('expires_at', '>', now());
  195. });
  196. }
  197. }
  198. if (isset($filters['search'])) {
  199. $search = $filters['search'];
  200. $query->where(function ($q) use ($search) {
  201. $q->where('name', 'like', "%{$search}%")
  202. ->orWhereHas('service', function ($sq) use ($search) {
  203. $sq->where('name', 'like', "%{$search}%")
  204. ->orWhere('code', 'like', "%{$search}%");
  205. });
  206. });
  207. }
  208. // 应用排序
  209. $sortBy = $options['sort_by'] ?? 'created_at';
  210. $sortOrder = $options['sort_order'] ?? 'desc';
  211. $query->orderBy($sortBy, $sortOrder);
  212. return $query->get();
  213. }
  214. /**
  215. * 获取即将过期的凭证
  216. *
  217. * @param int $days 提前天数
  218. * @return Collection
  219. */
  220. public static function getExpiringCredentials(int $days = 7): Collection
  221. {
  222. return ThirdPartyCredential::with('service')
  223. ->where('is_active', true)
  224. ->whereNotNull('expires_at')
  225. ->whereBetween('expires_at', [now(), now()->addDays($days)])
  226. ->orderBy('expires_at')
  227. ->get();
  228. }
  229. /**
  230. * 获取未使用的凭证
  231. *
  232. * @param int $days 天数
  233. * @return Collection
  234. */
  235. public static function getUnusedCredentials(int $days = 30): Collection
  236. {
  237. return ThirdPartyCredential::with('service')
  238. ->where('is_active', true)
  239. ->where(function ($query) use ($days) {
  240. $query->whereNull('last_used_at')
  241. ->orWhere('last_used_at', '<', now()->subDays($days));
  242. })
  243. ->orderBy('created_at')
  244. ->get();
  245. }
  246. /**
  247. * 验证凭证数据完整性
  248. *
  249. * @param AUTH_TYPE $authType
  250. * @param array $credentials
  251. * @return array
  252. */
  253. public static function validateCredentialData(AUTH_TYPE $authType, array $credentials): array
  254. {
  255. $requiredFields = $authType->getRequiredFields();
  256. $missingFields = [];
  257. foreach ($requiredFields as $field) {
  258. if (empty($credentials[$field])) {
  259. $missingFields[] = $field;
  260. }
  261. }
  262. return [
  263. 'valid' => empty($missingFields),
  264. 'missing_fields' => $missingFields,
  265. 'required_fields' => $requiredFields,
  266. ];
  267. }
  268. /**
  269. * 测试凭证连接
  270. *
  271. * @param ThirdPartyCredential $credential
  272. * @return array
  273. */
  274. public static function testCredential(ThirdPartyCredential $credential): array
  275. {
  276. $result = [
  277. 'success' => false,
  278. 'message' => '',
  279. 'details' => [],
  280. ];
  281. try {
  282. // 检查凭证状态
  283. if (!$credential->isUsable()) {
  284. $result['message'] = '凭证不可用';
  285. return $result;
  286. }
  287. // 验证凭证配置
  288. $validation = $credential->validateCredentials();
  289. if (!$validation['valid']) {
  290. $result['message'] = '凭证配置不完整: ' . implode(', ', $validation['missing_fields']);
  291. return $result;
  292. }
  293. // 生成认证头
  294. $headers = $credential->generateAuthHeaders();
  295. if (empty($headers)) {
  296. $result['message'] = '无法生成认证头';
  297. return $result;
  298. }
  299. $result['success'] = true;
  300. $result['message'] = '凭证测试成功';
  301. $result['details'] = [
  302. 'auth_type' => $credential->getTypeLabel(),
  303. 'environment' => $credential->environment,
  304. 'expires_at' => $credential->expires_at?->toDateTimeString(),
  305. 'usage_count' => $credential->usage_count,
  306. ];
  307. } catch (\Exception $e) {
  308. $result['message'] = '凭证测试失败: ' . $e->getMessage();
  309. }
  310. return $result;
  311. }
  312. /**
  313. * 轮换凭证
  314. *
  315. * @param ThirdPartyCredential $credential
  316. * @param array $newCredentials
  317. * @return ThirdPartyCredential
  318. * @throws \Exception
  319. */
  320. public static function rotateCredential(ThirdPartyCredential $credential, array $newCredentials): ThirdPartyCredential
  321. {
  322. // 验证新凭证数据
  323. $authType = AUTH_TYPE::from($credential->type);
  324. $validation = static::validateCredentialData($authType, $newCredentials);
  325. if (!$validation['valid']) {
  326. throw new \Exception("新凭证配置不完整,缺少字段: " . implode(', ', $validation['missing_fields']));
  327. }
  328. // 创建新凭证
  329. $newCredential = static::createCredential([
  330. 'service_id' => $credential->service_id,
  331. 'name' => $credential->name . '_rotated_' . now()->format('YmdHis'),
  332. 'type' => $credential->type,
  333. 'credentials' => $newCredentials,
  334. 'environment' => $credential->environment,
  335. 'is_active' => false, // 先创建为非激活状态
  336. 'expires_at' => $credential->expires_at,
  337. ]);
  338. // 停用旧凭证
  339. $credential->update(['is_active' => false]);
  340. // 激活新凭证
  341. $newCredential->update(['is_active' => true]);
  342. return $newCredential;
  343. }
  344. /**
  345. * 获取凭证统计信息
  346. *
  347. * @return array
  348. */
  349. public static function getCredentialStats(): array
  350. {
  351. $total = ThirdPartyCredential::count();
  352. $active = ThirdPartyCredential::where('is_active', true)->count();
  353. $expired = ThirdPartyCredential::where('expires_at', '<=', now())->count();
  354. $expiringSoon = ThirdPartyCredential::where('is_active', true)
  355. ->whereNotNull('expires_at')
  356. ->whereBetween('expires_at', [now(), now()->addDays(7)])
  357. ->count();
  358. $envStats = ThirdPartyCredential::selectRaw('environment, COUNT(*) as count')
  359. ->groupBy('environment')
  360. ->pluck('count', 'environment')
  361. ->toArray();
  362. $typeStats = ThirdPartyCredential::selectRaw('type, COUNT(*) as count')
  363. ->groupBy('type')
  364. ->pluck('count', 'type')
  365. ->toArray();
  366. return [
  367. 'total' => $total,
  368. 'active' => $active,
  369. 'expired' => $expired,
  370. 'expiring_soon' => $expiringSoon,
  371. 'by_environment' => $envStats,
  372. 'by_type' => $typeStats,
  373. 'health_rate' => $total > 0 ? round((($active - $expired) / $total) * 100, 2) : 0,
  374. ];
  375. }
  376. }