token system change

This commit is contained in:
Jethro Lin 2024-12-04 13:28:22 +08:00
parent ae5a7662ca
commit 33dde9dc2c
6 changed files with 189 additions and 192 deletions

View file

@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\AuthToken;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class CleanupExpiredTokens extends Command
{
protected $signature = 'auth:cleanup-expired-tokens';
protected $description = 'Clean up expired auth tokens from the database';
public function handle(): void
{
try {
$count = AuthToken::where('expires_at', '<', now())->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.');
}
}
}

View file

@ -12,8 +12,7 @@ class Kernel extends ConsoleKernel
*/ */
protected function schedule(Schedule $schedule): void protected function schedule(Schedule $schedule): void
{ {
// Clean up expired auth tokens daily // 移除不必要的清理任務
$schedule->command('auth:cleanup-expired-tokens')->daily();
} }
/** /**

View file

@ -6,92 +6,68 @@
class ErrorCode class ErrorCode
{ {
// 通用错误 (1000-1999) // Authentication & Authorization
public const VALIDATION_ERROR = 'E1001'; public const VALIDATION_ERROR = 'validation_error';
public const UNAUTHORIZED = 'E1002'; public const SERVER_ERROR = 'server_error';
public const FORBIDDEN = 'E1003'; public const NOT_FOUND = 'not_found';
public const RESOURCE_NOT_FOUND = 'E1004'; public const UNAUTHORIZED = 'unauthorized';
public const METHOD_NOT_ALLOWED = 'E1005'; public const FORBIDDEN = 'forbidden';
public const TOO_MANY_REQUESTS = 'E1006'; public const TOKEN_INVALID = 'token_invalid';
public const SERVER_ERROR = 'E1007'; public const TOKEN_EXPIRED = 'token_expired';
public const SERVICE_UNAVAILABLE = 'E1008'; public const INVALID_CREDENTIALS = 'invalid_credentials';
// 认证相关错误 (2000-2999) // Resource Related
public const INVALID_CREDENTIALS = 'E2001'; public const RESOURCE_NOT_FOUND = 'resource_not_found';
public const TOKEN_EXPIRED = 'E2002'; public const RESOURCE_IN_USE = 'resource_in_use';
public const TOKEN_INVALID = 'E2003';
public const TOKEN_BLACKLISTED = 'E2004';
public const TOKEN_NOT_FOUND = 'E2005';
// 客户相关错误 (3000-3999) // Request Related
public const CLIENT_NOT_FOUND = 'E3001'; public const INVALID_REQUEST_FORMAT = 'invalid_request_format';
public const CLIENT_INACTIVE = 'E3002'; public const METHOD_NOT_ALLOWED = 'method_not_allowed';
public const CLIENT_RATE_LIMIT_EXCEEDED = 'E3003'; public const TOO_MANY_REQUESTS = 'too_many_requests';
public const CLIENT_QUOTA_EXCEEDED = 'E3004';
public const CLIENT_HAS_ACTIVE_TOKENS = 'E3005';
// LLM 提供商相关错误 (4000-4999) // Client Related
public const PROVIDER_NOT_FOUND = 'E4001'; public const CLIENT_INACTIVE = 'client_inactive';
public const PROVIDER_INACTIVE = 'E4002'; public const CLIENT_NOT_FOUND = 'client_not_found';
public const PROVIDER_ERROR = 'E4003'; public const CLIENT_HAS_ACTIVE_TOKENS = 'client_has_active_tokens';
public const PROVIDER_TIMEOUT = 'E4004';
public const PROVIDER_RATE_LIMIT = 'E4005';
// 业务逻辑错误 (5000-5999) // Provider Related
public const INVALID_REQUEST_FORMAT = 'E5001'; public const PROVIDER_ERROR = 'provider_error';
public const INVALID_RESPONSE_FORMAT = 'E5002'; public const LLM_PROVIDER_NOT_FOUND = 'llm_provider_not_found';
public const OPERATION_FAILED = 'E5003'; public const LLM_REQUEST_FAILED = 'llm_request_failed';
public const RESOURCE_ALREADY_EXISTS = 'E5004';
public const RESOURCE_IN_USE = 'E5005'; 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 public static function getMessage(string $code): string
{ {
return match ($code) { return self::$messages[$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 => '未知错误。',
};
} }
} }

View file

@ -22,10 +22,7 @@ public function __construct(
) {} ) {}
/** /**
* 获取访问令牌 * 獲取訪問令牌
*
* @param Request $request
* @return JsonResponse
*/ */
public function getAccessToken(Request $request): JsonResponse public function getAccessToken(Request $request): JsonResponse
{ {
@ -34,11 +31,28 @@ public function getAccessToken(Request $request): JsonResponse
'auth_token' => 'required|string|size:64', '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([ return $this->success([
'access_token' => $result['token'], 'access_token' => $result['access_token'],
'expires_at' => $result['expires_at'], 'expires_in' => $result['expires_in'],
'token_type' => $result['token_type'],
]); ]);
} catch (ValidationException $e) { } catch (ValidationException $e) {
@ -49,7 +63,7 @@ public function getAccessToken(Request $request): JsonResponse
); );
} catch (\InvalidArgumentException $e) { } catch (\InvalidArgumentException $e) {
return $this->error( return $this->error(
ErrorCode::TOKEN_INVALID, ErrorCode::TOKEN_EXPIRED,
$e->getMessage() $e->getMessage()
); );
} catch (\Exception $e) { } catch (\Exception $e) {

View file

@ -28,10 +28,11 @@ public function client(): BelongsTo
public function isValid(): bool public function isValid(): bool
{ {
if ($this->expires_at === null) { return $this->expires_at && $this->expires_at->isFuture();
return true; }
}
return $this->expires_at->isFuture(); public function isExpired(): bool
{
return !$this->isValid();
} }
} }

View file

@ -12,98 +12,111 @@
class TokenService class TokenService
{ {
private const AUTH_TOKEN_PREFIX = 'auth_token:';
private const ACCESS_TOKEN_PREFIX = 'access_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( public function __construct(
private readonly LogService $logService 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, $effectiveHours = min($expiresInHours ?? self::MAX_AUTH_TOKEN_HOURS, self::MAX_AUTH_TOKEN_HOURS);
'token' => Str::random(64), $effectiveSeconds = $effectiveHours * 3600;
'expires_at' => $expiresInDays ? now()->addDays($expiresInDays) : null,
]);
// 生成新令牌
$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( $this->logService->logOperation(
'client', 'client',
$client->id, $client->id,
'Generated new auth token' '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); $accessToken = Str::random(64);
$expiresAt = now()->addSeconds(self::ACCESS_TOKEN_TTL);
$tokenData = [
'client_id' => $authToken->client_id,
'expires_at' => $expiresAt->toIso8601String(),
];
// 存儲到 Redis包含更多信息以便追蹤
Redis::setex( Redis::setex(
self::ACCESS_TOKEN_PREFIX . $accessToken, self::ACCESS_TOKEN_PREFIX . $accessToken,
self::ACCESS_TOKEN_TTL, 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( $this->logService->logOperation(
'client', 'client',
$authToken->client_id, $authTokenData['client_id'],
'Generated new access token' 'Generated new access token'
); );
return [ return [
'access_token' => $accessToken, 'access_token' => $accessToken,
'expires_in' => self::ACCESS_TOKEN_TTL, '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 public function validateAccessToken(string $token): ?array
{ {
$tokenData = Redis::get(self::ACCESS_TOKEN_PREFIX . $token); $tokenData = Redis::get(self::ACCESS_TOKEN_PREFIX . $token);
if (!$tokenData) { if (!$tokenData) {
return null; return null;
} }
$data = json_decode($tokenData, true); return 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;
} }
public function revokeAccessToken(string $token): void public function revokeAccessToken(string $token): void
@ -111,22 +124,51 @@ public function revokeAccessToken(string $token): void
$tokenData = Redis::get(self::ACCESS_TOKEN_PREFIX . $token); $tokenData = Redis::get(self::ACCESS_TOKEN_PREFIX . $token);
if ($tokenData) { if ($tokenData) {
$data = json_decode($tokenData, true); $data = json_decode($tokenData, true);
$this->logService->logOperation( if ($data) {
'client', // 記錄操作日誌
$data['client_id'], $this->logService->logOperation(
'Access token revoked' '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( $tokenData = Redis::get(self::AUTH_TOKEN_PREFIX . $token);
'client', if ($tokenData) {
$authToken->client_id, $data = json_decode($tokenData, true);
'Auth token revoked' if ($data) {
); // 計算剩餘有效期
$authToken->delete(); $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 會自動清理過期的令牌,無需手動清理
// 這個方法保留用於日誌記錄或其他清理工作
} }
} }