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
{
// Clean up expired auth tokens daily
$schedule->command('auth:cleanup-expired-tokens')->daily();
// 移除不必要的清理任務
}
/**

View file

@ -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] ?? '未知錯誤。';
}
}

View file

@ -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) {

View file

@ -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();
}
}

View file

@ -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 會自動清理過期的令牌,無需手動清理
// 這個方法保留用於日誌記錄或其他清理工作
}
}