From 33dde9dc2cf45ea765739f5e68e348d0243e8955 Mon Sep 17 00:00:00 2001 From: Jethro Lin Date: Wed, 4 Dec 2024 13:28:22 +0800 Subject: [PATCH] token system change --- app/Console/Commands/CleanupExpiredTokens.php | 35 ---- app/Console/Kernel.php | 3 +- app/Constants/ErrorCode.php | 136 ++++++-------- app/Http/Controllers/Api/AuthController.php | 30 +++- app/Models/AuthToken.php | 9 +- app/Services/Auth/TokenService.php | 168 +++++++++++------- 6 files changed, 189 insertions(+), 192 deletions(-) delete mode 100644 app/Console/Commands/CleanupExpiredTokens.php diff --git a/app/Console/Commands/CleanupExpiredTokens.php b/app/Console/Commands/CleanupExpiredTokens.php deleted file mode 100644 index 4a12f56..0000000 --- a/app/Console/Commands/CleanupExpiredTokens.php +++ /dev/null @@ -1,35 +0,0 @@ -delete(); - - if ($count > 0) { - Log::info("Cleaned up {$count} expired auth tokens"); - $this->info("Successfully cleaned up {$count} expired auth tokens."); - } else { - $this->info('No expired auth tokens found.'); - } - } catch (\Exception $e) { - Log::error('Error cleaning up expired auth tokens', [ - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ]); - $this->error('Failed to clean up expired auth tokens.'); - } - } -} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 454280e..5d3e810 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -12,8 +12,7 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule): void { - // Clean up expired auth tokens daily - $schedule->command('auth:cleanup-expired-tokens')->daily(); + // 移除不必要的清理任務 } /** diff --git a/app/Constants/ErrorCode.php b/app/Constants/ErrorCode.php index 9047a1d..1211c59 100644 --- a/app/Constants/ErrorCode.php +++ b/app/Constants/ErrorCode.php @@ -6,92 +6,68 @@ class ErrorCode { - // 通用错误 (1000-1999) - public const VALIDATION_ERROR = 'E1001'; - public const UNAUTHORIZED = 'E1002'; - public const FORBIDDEN = 'E1003'; - public const RESOURCE_NOT_FOUND = 'E1004'; - public const METHOD_NOT_ALLOWED = 'E1005'; - public const TOO_MANY_REQUESTS = 'E1006'; - public const SERVER_ERROR = 'E1007'; - public const SERVICE_UNAVAILABLE = 'E1008'; + // Authentication & Authorization + public const VALIDATION_ERROR = 'validation_error'; + public const SERVER_ERROR = 'server_error'; + public const NOT_FOUND = 'not_found'; + public const UNAUTHORIZED = 'unauthorized'; + public const FORBIDDEN = 'forbidden'; + public const TOKEN_INVALID = 'token_invalid'; + public const TOKEN_EXPIRED = 'token_expired'; + public const INVALID_CREDENTIALS = 'invalid_credentials'; - // 认证相关错误 (2000-2999) - public const INVALID_CREDENTIALS = 'E2001'; - public const TOKEN_EXPIRED = 'E2002'; - public const TOKEN_INVALID = 'E2003'; - public const TOKEN_BLACKLISTED = 'E2004'; - public const TOKEN_NOT_FOUND = 'E2005'; + // Resource Related + public const RESOURCE_NOT_FOUND = 'resource_not_found'; + public const RESOURCE_IN_USE = 'resource_in_use'; - // 客户相关错误 (3000-3999) - public const CLIENT_NOT_FOUND = 'E3001'; - public const CLIENT_INACTIVE = 'E3002'; - public const CLIENT_RATE_LIMIT_EXCEEDED = 'E3003'; - public const CLIENT_QUOTA_EXCEEDED = 'E3004'; - public const CLIENT_HAS_ACTIVE_TOKENS = 'E3005'; + // Request Related + public const INVALID_REQUEST_FORMAT = 'invalid_request_format'; + public const METHOD_NOT_ALLOWED = 'method_not_allowed'; + public const TOO_MANY_REQUESTS = 'too_many_requests'; - // LLM 提供商相关错误 (4000-4999) - public const PROVIDER_NOT_FOUND = 'E4001'; - public const PROVIDER_INACTIVE = 'E4002'; - public const PROVIDER_ERROR = 'E4003'; - public const PROVIDER_TIMEOUT = 'E4004'; - public const PROVIDER_RATE_LIMIT = 'E4005'; + // Client Related + public const CLIENT_INACTIVE = 'client_inactive'; + public const CLIENT_NOT_FOUND = 'client_not_found'; + public const CLIENT_HAS_ACTIVE_TOKENS = 'client_has_active_tokens'; - // 业务逻辑错误 (5000-5999) - public const INVALID_REQUEST_FORMAT = 'E5001'; - public const INVALID_RESPONSE_FORMAT = 'E5002'; - public const OPERATION_FAILED = 'E5003'; - public const RESOURCE_ALREADY_EXISTS = 'E5004'; - public const RESOURCE_IN_USE = 'E5005'; + // Provider Related + public const PROVIDER_ERROR = 'provider_error'; + public const LLM_PROVIDER_NOT_FOUND = 'llm_provider_not_found'; + public const LLM_REQUEST_FAILED = 'llm_request_failed'; + + private static array $messages = [ + // Authentication & Authorization + self::VALIDATION_ERROR => '請求參數驗證失敗。', + self::SERVER_ERROR => '服務器內部錯誤。', + self::NOT_FOUND => '請求的資源不存在。', + self::UNAUTHORIZED => '未授權,請先登錄。', + self::FORBIDDEN => '禁止訪問,權限不足。', + self::TOKEN_INVALID => '令牌無效。', + self::TOKEN_EXPIRED => '令牌已過期。', + self::INVALID_CREDENTIALS => '認證憑據無效。', + + // Resource Related + self::RESOURCE_NOT_FOUND => '請求的資源不存在。', + self::RESOURCE_IN_USE => '資源正在使用中,無法執行操作。', + + // Request Related + self::INVALID_REQUEST_FORMAT => '請求格式無效。', + self::METHOD_NOT_ALLOWED => '請求方法不允許。', + self::TOO_MANY_REQUESTS => '請求過於頻繁,請稍後再試。', + + // Client Related + self::CLIENT_INACTIVE => '客戶帳戶未啟用。', + self::CLIENT_NOT_FOUND => '客戶不存在。', + self::CLIENT_HAS_ACTIVE_TOKENS => '客戶有活躍的認證令牌。', + + // Provider Related + self::PROVIDER_ERROR => '提供商服務錯誤。', + self::LLM_PROVIDER_NOT_FOUND => 'LLM 提供商不存在。', + self::LLM_REQUEST_FAILED => 'LLM 請求失敗。', + ]; - /** - * 获取错误代码对应的默认消息 - * - * @param string $code 错误代码 - * @return string 错误消息 - */ public static function getMessage(string $code): string { - return match ($code) { - // 通用错误 - self::VALIDATION_ERROR => '请求参数验证失败。', - self::UNAUTHORIZED => '未经授权的访问。', - self::FORBIDDEN => '无权访问该资源。', - self::RESOURCE_NOT_FOUND => '请求的资源不存在。', - self::METHOD_NOT_ALLOWED => '请求方法不允许。', - self::TOO_MANY_REQUESTS => '请求过于频繁,请稍后重试。', - self::SERVER_ERROR => '服务器内部错误。', - self::SERVICE_UNAVAILABLE => '服务暂时不可用。', - - // 认证相关错误 - self::INVALID_CREDENTIALS => '无效的认证凭据。', - self::TOKEN_EXPIRED => '认证令牌已过期。', - self::TOKEN_INVALID => '无效的认证令牌。', - self::TOKEN_BLACKLISTED => '认证令牌已被禁用。', - self::TOKEN_NOT_FOUND => '认证令牌不存在。', - - // 客户相关错误 - self::CLIENT_NOT_FOUND => '客户不存在。', - self::CLIENT_INACTIVE => '客户状态为非活跃。', - self::CLIENT_RATE_LIMIT_EXCEEDED => '已超过请求速率限制。', - self::CLIENT_QUOTA_EXCEEDED => '已超过配额限制。', - self::CLIENT_HAS_ACTIVE_TOKENS => '客户存在活跃的认证令牌。', - - // LLM 提供商相关错误 - self::PROVIDER_NOT_FOUND => 'LLM 提供商不存在。', - self::PROVIDER_INACTIVE => 'LLM 提供商状态为非活跃。', - self::PROVIDER_ERROR => 'LLM 提供商服务错误。', - self::PROVIDER_TIMEOUT => 'LLM 提供商服务超时。', - self::PROVIDER_RATE_LIMIT => 'LLM 提供商限制请求速率。', - - // 业务逻辑错误 - self::INVALID_REQUEST_FORMAT => '无效的请求格式。', - self::INVALID_RESPONSE_FORMAT => '无效的响应格式。', - self::OPERATION_FAILED => '操作失败。', - self::RESOURCE_ALREADY_EXISTS => '资源已存在。', - self::RESOURCE_IN_USE => '资源正在使用中。', - - default => '未知错误。', - }; + return self::$messages[$code] ?? '未知錯誤。'; } } diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php index 9510f39..effad93 100644 --- a/app/Http/Controllers/Api/AuthController.php +++ b/app/Http/Controllers/Api/AuthController.php @@ -22,10 +22,7 @@ public function __construct( ) {} /** - * 获取访问令牌 - * - * @param Request $request - * @return JsonResponse + * 獲取訪問令牌 */ public function getAccessToken(Request $request): JsonResponse { @@ -34,11 +31,28 @@ public function getAccessToken(Request $request): JsonResponse 'auth_token' => 'required|string|size:64', ]); - $result = $this->tokenService->generateAccessToken($validated['auth_token']); + $authTokenData = $this->tokenService->validateAuthToken($validated['auth_token']); + + if (!$authTokenData) { + return $this->error( + ErrorCode::TOKEN_INVALID, + '認證令牌無效。' + ); + } + + if (now()->isAfter($authTokenData['expires_at'])) { + return $this->error( + ErrorCode::TOKEN_EXPIRED, + '認證令牌已過期,請重新獲取。' + ); + } + + $result = $this->tokenService->generateAccessToken($authTokenData); return $this->success([ - 'access_token' => $result['token'], - 'expires_at' => $result['expires_at'], + 'access_token' => $result['access_token'], + 'expires_in' => $result['expires_in'], + 'token_type' => $result['token_type'], ]); } catch (ValidationException $e) { @@ -49,7 +63,7 @@ public function getAccessToken(Request $request): JsonResponse ); } catch (\InvalidArgumentException $e) { return $this->error( - ErrorCode::TOKEN_INVALID, + ErrorCode::TOKEN_EXPIRED, $e->getMessage() ); } catch (\Exception $e) { diff --git a/app/Models/AuthToken.php b/app/Models/AuthToken.php index c370fda..5be39ad 100644 --- a/app/Models/AuthToken.php +++ b/app/Models/AuthToken.php @@ -28,10 +28,11 @@ public function client(): BelongsTo public function isValid(): bool { - if ($this->expires_at === null) { - return true; - } + return $this->expires_at && $this->expires_at->isFuture(); + } - return $this->expires_at->isFuture(); + public function isExpired(): bool + { + return !$this->isValid(); } } diff --git a/app/Services/Auth/TokenService.php b/app/Services/Auth/TokenService.php index bb8491c..59c54ce 100644 --- a/app/Services/Auth/TokenService.php +++ b/app/Services/Auth/TokenService.php @@ -12,98 +12,111 @@ class TokenService { + private const AUTH_TOKEN_PREFIX = 'auth_token:'; private const ACCESS_TOKEN_PREFIX = 'access_token:'; - private const ACCESS_TOKEN_TTL = 3600; // 1 hour in seconds + private const BLACKLIST_PREFIX = 'token_blacklist:'; + private const MAX_AUTH_TOKEN_HOURS = 8; // 最大 8 小時有效期 + private const ACCESS_TOKEN_TTL = 3600; // 1 小時有效期 public function __construct( private readonly LogService $logService ) {} - public function generateAuthToken(Client $client, ?int $expiresInDays = null): AuthToken + public function generateAuthToken(Client $client, ?int $expiresInHours = null): AuthToken { - $token = AuthToken::create([ - 'client_id' => $client->id, - 'token' => Str::random(64), - 'expires_at' => $expiresInDays ? now()->addDays($expiresInDays) : null, - ]); + // 確保不超過最大有效期 + $effectiveHours = min($expiresInHours ?? self::MAX_AUTH_TOKEN_HOURS, self::MAX_AUTH_TOKEN_HOURS); + $effectiveSeconds = $effectiveHours * 3600; + // 生成新令牌 + $token = Str::random(64); + $expiresAt = now()->addHours($effectiveHours); + + // 存儲到 Redis + Redis::setex( + self::AUTH_TOKEN_PREFIX . $token, + $effectiveSeconds, + json_encode([ + 'client_id' => $client->id, + 'created_at' => now()->toIso8601String(), + 'expires_at' => $expiresAt->toIso8601String(), + ]) + ); + + // 記錄操作日誌 $this->logService->logOperation( 'client', $client->id, 'Generated new auth token' ); - return $token; + // 返回令牌對象(僅用於 API 響應) + return new AuthToken([ + 'client_id' => $client->id, + 'token' => $token, + 'expires_at' => $expiresAt, + ]); } - public function generateAccessToken(AuthToken $authToken): array + public function validateAuthToken(string $token): ?array + { + // 檢查黑名單 + if (Redis::exists(self::BLACKLIST_PREFIX . $token)) { + return null; + } + + // 檢查令牌是否存在且有效 + $tokenData = Redis::get(self::AUTH_TOKEN_PREFIX . $token); + if (!$tokenData) { + return null; + } + + $data = json_decode($tokenData, true); + if (!$data || now()->isAfter($data['expires_at'])) { + Redis::del(self::AUTH_TOKEN_PREFIX . $token); + return null; + } + + return $data; + } + + public function generateAccessToken(array $authTokenData): array { $accessToken = Str::random(64); - $expiresAt = now()->addSeconds(self::ACCESS_TOKEN_TTL); - - $tokenData = [ - 'client_id' => $authToken->client_id, - 'expires_at' => $expiresAt->toIso8601String(), - ]; + // 存儲到 Redis,包含更多信息以便追蹤 Redis::setex( self::ACCESS_TOKEN_PREFIX . $accessToken, self::ACCESS_TOKEN_TTL, - json_encode($tokenData) + json_encode([ + 'client_id' => $authTokenData['client_id'], + 'auth_token' => $authTokenData['token'], + 'created_at' => now()->toIso8601String(), + ]) ); + // 記錄操作日誌 $this->logService->logOperation( 'client', - $authToken->client_id, + $authTokenData['client_id'], 'Generated new access token' ); return [ 'access_token' => $accessToken, 'expires_in' => self::ACCESS_TOKEN_TTL, + 'token_type' => 'Bearer', ]; } - public function validateAuthToken(string $token): ?AuthToken - { - $authToken = AuthToken::where('token', $token)->first(); - - if (!$authToken || !$authToken->isValid()) { - if ($authToken) { - $this->logService->logOperation( - 'client', - $authToken->client_id, - 'Invalid auth token attempt' - ); - } - return null; - } - - return $authToken; - } - public function validateAccessToken(string $token): ?array { $tokenData = Redis::get(self::ACCESS_TOKEN_PREFIX . $token); - if (!$tokenData) { return null; } - $data = json_decode($tokenData, true); - $expiresAt = \DateTime::createFromFormat(\DateTime::ISO8601, $data['expires_at']); - - if ($expiresAt < now()) { - $this->revokeAccessToken($token); - $this->logService->logOperation( - 'client', - $data['client_id'], - 'Access token expired' - ); - return null; - } - - return $data; + return json_decode($tokenData, true); } public function revokeAccessToken(string $token): void @@ -111,22 +124,51 @@ public function revokeAccessToken(string $token): void $tokenData = Redis::get(self::ACCESS_TOKEN_PREFIX . $token); if ($tokenData) { $data = json_decode($tokenData, true); - $this->logService->logOperation( - 'client', - $data['client_id'], - 'Access token revoked' - ); + if ($data) { + // 記錄操作日誌 + $this->logService->logOperation( + 'client', + $data['client_id'], + 'Access token revoked' + ); + } + // 從 Redis 中刪除 + Redis::del(self::ACCESS_TOKEN_PREFIX . $token); } - Redis::del(self::ACCESS_TOKEN_PREFIX . $token); } - public function revokeAuthToken(AuthToken $authToken): void + public function revokeAuthToken(string $token): void { - $this->logService->logOperation( - 'client', - $authToken->client_id, - 'Auth token revoked' - ); - $authToken->delete(); + $tokenData = Redis::get(self::AUTH_TOKEN_PREFIX . $token); + if ($tokenData) { + $data = json_decode($tokenData, true); + if ($data) { + // 計算剩餘有效期 + $expiresAt = now()->parse($data['expires_at']); + $remainingSeconds = max(1, $expiresAt->diffInSeconds(now())); + + // 加入黑名單 + Redis::setex( + self::BLACKLIST_PREFIX . $token, + $remainingSeconds, + 'revoked' + ); + + // 記錄操作日誌 + $this->logService->logOperation( + 'client', + $data['client_id'], + 'Auth token revoked' + ); + } + // 從 Redis 中刪除 + Redis::del(self::AUTH_TOKEN_PREFIX . $token); + } + } + + public function cleanupExpiredTokens(): void + { + // Redis 會自動清理過期的令牌,無需手動清理 + // 這個方法保留用於日誌記錄或其他清理工作 } }