middleware('auth:api'); $this->middleware('permission:platform.accounts.view,api')->only(['index', 'show']); $this->middleware('permission:platform.accounts.manage,api')->only(['store', 'update', 'destroy', 'refreshToken', 'refreshTokenStatus', 'checkBastionLogin']); } #[Apidoc\Title('账号列表'), Apidoc\Method('GET'), Apidoc\Url('/accounts')] public function index(): JsonResponse { return response()->json(['code' => 0, 'message' => 'ok', 'data' => BastionAccount::query()->latest()->paginate(20)]); } #[Apidoc\Title('创建账号'), Apidoc\Method('POST'), Apidoc\Url('/accounts')] public function store(StoreBastionAccountRequest $request): JsonResponse { $account = BastionAccount::query()->create($request->validated()); $this->auditLog($request, 'account_create', ['bastion_account_id' => $account->id]); return response()->json(['code' => 0, 'message' => 'ok', 'data' => $account], 201); } #[Apidoc\Title('账号详情'), Apidoc\Method('GET'), Apidoc\Url('/accounts/{id}')] public function show(int $id): JsonResponse { return response()->json(['code' => 0, 'message' => 'ok', 'data' => BastionAccount::query()->findOrFail($id)]); } #[Apidoc\Title('更新账号'), Apidoc\Method('PUT'), Apidoc\Url('/accounts/{id}')] public function update(UpdateBastionAccountRequest $request, int $id): JsonResponse { $account = BastionAccount::query()->findOrFail($id); $account->update($request->validated()); $this->auditLog($request, 'account_update', ['bastion_account_id' => $account->id]); return response()->json(['code' => 0, 'message' => 'ok', 'data' => $account]); } #[Apidoc\Title('删除账号'), Apidoc\Method('DELETE'), Apidoc\Url('/accounts/{id}')] public function destroy(Request $request, int $id): JsonResponse { $account = BastionAccount::query()->findOrFail($id); $this->auditLog($request, 'account_delete', ['bastion_account_id' => $account->id]); $account->delete(); return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]); } #[Apidoc\Title('刷新账号token'), Apidoc\Method('POST'), Apidoc\Url('/accounts/{id}/refresh-token')] public function refreshToken(Request $request, int $id): JsonResponse { $account = BastionAccount::query()->findOrFail($id); $baseUrl = (string) config('services.bastion_token.base_url'); if ($baseUrl === '') { return response()->json([ 'code' => 500, 'message' => '未配置堡垒机 Token 服务地址,请先在 .env 中配置 BASTION_TOKEN_API_BASE_URL', 'data' => null, ], 500); } $submitEndpoint = (string) config('services.bastion_token.submit_endpoint', '/bastion_token'); $timeout = (int) config('services.bastion_token.timeout', 30); $taskTtlSeconds = (int) config('services.bastion_token.task_ttl_seconds', 1800); $service = (string) config('services.bastion_token.service', 'https://myapp.cdu.edu.cn/index.html'); $verifySsl = (bool) config('services.bastion_token.verify_ssl', false); try { $submitResponse = Http::baseUrl($baseUrl) ->acceptJson() ->timeout($timeout) ->retry(2, 300, throw: false) ->post($submitEndpoint, [ 'username' => $account->username, 'password' => $account->password, 'service' => $service, 'verify_ssl' => $verifySsl, ]); if (! $submitResponse->successful()) { return response()->json([ 'code' => 502, 'message' => '提交 Token 刷新任务失败', 'data' => ['response' => $submitResponse->json()], ], 502); } $taskId = (string) data_get($submitResponse->json(), 'task_id', ''); if ($taskId === '') { return response()->json([ 'code' => 502, 'message' => 'Token 服务返回任务ID为空', 'data' => ['response' => $submitResponse->json()], ], 502); } } catch (ConnectionException|RequestException $exception) { return response()->json([ 'code' => 502, 'message' => '调用 Token 服务失败:'.$exception->getMessage(), 'data' => null, ], 502); } $cacheKey = $this->tokenTaskCacheKey($taskId); Cache::put($cacheKey, [ 'task_id' => $taskId, 'account_id' => $account->id, 'finished' => false, ], now()->addSeconds(max(60, $taskTtlSeconds))); $this->auditLog($request, 'account_refresh_token_submit', ['bastion_account_id' => $account->id, 'task_id' => $taskId]); return response()->json([ 'code' => 0, 'message' => 'Token 刷新任务已提交', 'data' => [ 'task_id' => $taskId, 'status' => 'pending', ], ]); } #[Apidoc\Title('查询刷新账号token状态'), Apidoc\Method('GET'), Apidoc\Url('/accounts/{id}/refresh-token/{taskId}')] public function refreshTokenStatus(Request $request, int $id, string $taskId): JsonResponse { $account = BastionAccount::query()->findOrFail($id); $baseUrl = (string) config('services.bastion_token.base_url'); if ($baseUrl === '') { return response()->json([ 'code' => 500, 'message' => '未配置堡垒机 Token 服务地址,请先在 .env 中配置 BASTION_TOKEN_API_BASE_URL', 'data' => null, ], 500); } $cacheKey = $this->tokenTaskCacheKey($taskId); $taskMeta = Cache::get($cacheKey); if (! is_array($taskMeta) || (int) ($taskMeta['account_id'] ?? 0) !== $account->id) { return response()->json([ 'code' => 404, 'message' => '任务不存在或已过期', 'data' => ['task_id' => $taskId], ], 404); } $statusEndpoint = (string) config('services.bastion_token.status_endpoint', '/bastion_token/{task_id}'); $timeout = (int) config('services.bastion_token.timeout', 30); $statusUrl = str_replace('{task_id}', $taskId, $statusEndpoint); try { $statusResponse = Http::baseUrl($baseUrl) ->acceptJson() ->timeout($timeout) ->retry(2, 300, throw: false) ->get($statusUrl); } catch (ConnectionException|RequestException $exception) { return response()->json([ 'code' => 502, 'message' => '查询 Token 任务状态失败:'.$exception->getMessage(), 'data' => ['task_id' => $taskId], ], 502); } if (! $statusResponse->successful()) { return response()->json([ 'code' => 502, 'message' => '查询 Token 任务状态失败', 'data' => ['task_id' => $taskId, 'response' => $statusResponse->json()], ], 502); } $taskResult = $statusResponse->json(); $status = Str::lower((string) data_get($taskResult, 'status', 'pending')); if ($status !== 'success') { if ($status === 'error') { return response()->json([ 'code' => 502, 'message' => 'Token 刷新失败:'.((string) data_get($taskResult, 'message', '未知错误')), 'data' => ['task_id' => $taskId, 'status' => $status, 'result' => $taskResult], ], 502); } return response()->json([ 'code' => 0, 'message' => 'Token 刷新任务执行中', 'data' => ['task_id' => $taskId, 'status' => 'pending'], ]); } if (! (bool) ($taskMeta['finished'] ?? false)) { $usmAuthentication = (string) data_get($taskResult, 'data.bastion.token_cookies.USM-AUTHENTICATION', ''); $usm = (string) data_get($taskResult, 'data.bastion.token_cookies.USM', ''); // 向后兼容旧版任务服务返回结构 if ($usmAuthentication === '' || $usm === '') { $usmAuthentication = (string) data_get($taskResult, 'data.USM-AUTHENTICATION', $usmAuthentication); $usm = (string) data_get($taskResult, 'data.USM', $usm); } if ($usmAuthentication === '' || $usm === '') { return response()->json([ 'code' => 502, 'message' => 'Token 服务返回数据缺失', 'data' => ['task_id' => $taskId, 'result' => $taskResult], ], 502); } $account->update([ 'usm_authentication' => $usmAuthentication, 'usm' => $usm, 'last_token_refreshed_at' => now(), ]); $taskMeta['finished'] = true; Cache::put($cacheKey, $taskMeta, now()->addMinutes(10)); $this->auditLog($request, 'account_refresh_token', ['bastion_account_id' => $account->id, 'task_id' => $taskId]); } return response()->json([ 'code' => 0, 'message' => 'Token 刷新成功', 'data' => ['task_id' => $taskId, 'status' => 'success', 'account' => $account->fresh()], ]); } private function tokenTaskCacheKey(string $taskId): string { return 'bastion_token_task:'.$taskId; } #[Apidoc\Title('检测堡垒机登录有效性'), Apidoc\Method('GET'), Apidoc\Url('/accounts/check-login')] public function checkBastionLogin(Request $request): JsonResponse { $autoRefresh = $request->boolean('auto_refresh', false); $accounts = BastionAccount::query() ->where('is_active', true) ->whereNotNull('usm') ->where('usm', '!=', '') ->whereNotNull('usm_authentication') ->where('usm_authentication', '!=', '') ->orderBy('id') ->get(); $result = []; foreach ($accounts as $account) { $status = $this->checkAccountTokenStatus((string) $account->usm_authentication, (string) $account->usm); $refreshed = false; if ($status === -1 && $autoRefresh) { $refreshed = $this->refreshAccountTokenDirectly($account); if ($refreshed) { $account->refresh(); $status = $this->checkAccountTokenStatus((string) $account->usm_authentication, (string) $account->usm); } } $result[] = [ 'id' => $account->id, 'name' => $account->name, 'username' => $account->username, 'status' => $status, 'refreshed' => $refreshed, ]; } return response()->json([ 'code' => 0, 'message' => 'ok', 'data' => $result, ]); } private function checkAccountTokenStatus(string $usmAuthentication, string $usm): int { $url = 'https://172.16.254.2/index.php/extend/check_login'; $cookie = sprintf( 'LANG=zh; USM-AUTHENTICATION=%s; USM=%s; LOGON=1; AUTH_METHOD=oauth2', $usmAuthentication, $usm ); try { $response = Http::timeout(10) ->withOptions(['verify' => false]) ->withHeaders([ 'Cookie' => $cookie, 'Accept' => '*/*', ]) ->get($url); } catch (ConnectionException) { return -1; } if (! $response->successful()) { return -1; } return trim((string) $response->body()) === '0' ? 0 : -1; } private function refreshAccountTokenDirectly(BastionAccount $account): bool { $baseUrl = (string) config('services.bastion_token.base_url'); if ($baseUrl === '') { return false; } $submitEndpoint = (string) config('services.bastion_token.submit_endpoint', '/bastion_token'); $statusEndpoint = (string) config('services.bastion_token.status_endpoint', '/bastion_token/{task_id}'); $timeout = (int) config('services.bastion_token.timeout', 30); $service = (string) config('services.bastion_token.service', 'https://myapp.cdu.edu.cn/index.html'); $verifySsl = (bool) config('services.bastion_token.verify_ssl', false); try { $submitResponse = Http::baseUrl($baseUrl) ->acceptJson() ->timeout($timeout) ->retry(2, 300, throw: false) ->post($submitEndpoint, [ 'username' => $account->username, 'password' => $account->password, 'service' => $service, 'verify_ssl' => $verifySsl, ]); } catch (ConnectionException|RequestException) { return false; } if (! $submitResponse->successful()) { return false; } $taskId = (string) data_get($submitResponse->json(), 'task_id', ''); if ($taskId === '') { return false; } for ($i = 0; $i < 20; $i++) { usleep(500000); $statusUrl = str_replace('{task_id}', $taskId, $statusEndpoint); try { $statusResponse = Http::baseUrl($baseUrl) ->acceptJson() ->timeout($timeout) ->retry(2, 300, throw: false) ->get($statusUrl); } catch (ConnectionException|RequestException) { continue; } if (! $statusResponse->successful()) { continue; } $taskResult = $statusResponse->json(); $status = strtolower((string) data_get($taskResult, 'status', 'pending')); if ($status === 'pending') { continue; } if ($status === 'error') { return false; } $usmAuthentication = (string) data_get($taskResult, 'data.bastion.token_cookies.USM-AUTHENTICATION', ''); $usm = (string) data_get($taskResult, 'data.bastion.token_cookies.USM', ''); if ($usmAuthentication === '' || $usm === '') { $usmAuthentication = (string) data_get($taskResult, 'data.USM-AUTHENTICATION', $usmAuthentication); $usm = (string) data_get($taskResult, 'data.USM', $usm); } if ($usmAuthentication === '' || $usm === '') { return false; } $account->update([ 'usm_authentication' => $usmAuthentication, 'usm' => $usm, 'last_token_refreshed_at' => now(), ]); return true; } return false; } }