192 lines
5.4 KiB
PHP
192 lines
5.4 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Services\Auth;
|
||
|
||
use App\Models\Client;
|
||
use App\Services\LogService;
|
||
use Illuminate\Support\Facades\Redis;
|
||
use Illuminate\Support\Str;
|
||
|
||
class TokenService
|
||
{
|
||
private const AUTH_TOKEN_PREFIX = 'auth_token:';
|
||
private const ACCESS_TOKEN_PREFIX = 'access_token:';
|
||
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
|
||
) {}
|
||
|
||
/**
|
||
* 生成認證令牌
|
||
*
|
||
* @return array{auth_token: string, expires_at: string, client_id: int}
|
||
*/
|
||
public function generateAuthToken(Client $client, ?int $expiresInHours = null): array
|
||
{
|
||
// 確保不超過最大有效期
|
||
$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,
|
||
'token' => $token,
|
||
'created_at' => now()->toIso8601String(),
|
||
'expires_at' => $expiresAt->toIso8601String(),
|
||
])
|
||
);
|
||
|
||
// 記錄操作日誌
|
||
$this->logService->logOperation(
|
||
'client',
|
||
$client->id,
|
||
'Generated new auth token'
|
||
);
|
||
|
||
return [
|
||
'auth_token' => $token,
|
||
'expires_at' => $expiresAt->toIso8601String(),
|
||
'client_id' => $client->id,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 驗證認證令牌
|
||
*
|
||
* @return array{client_id: int, token: string, created_at: string, expires_at: string}|null
|
||
*/
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 生成訪問令牌
|
||
*
|
||
* @return array{access_token: string, expires_in: int}
|
||
*/
|
||
public function generateAccessToken(array $authTokenData): array
|
||
{
|
||
$accessToken = Str::random(64);
|
||
|
||
// 存儲到 Redis,包含更多信息以便追蹤
|
||
Redis::setex(
|
||
self::ACCESS_TOKEN_PREFIX . $accessToken,
|
||
self::ACCESS_TOKEN_TTL,
|
||
json_encode([
|
||
'client_id' => $authTokenData['client_id'],
|
||
'auth_token' => $authTokenData['token'],
|
||
'created_at' => now()->toIso8601String(),
|
||
])
|
||
);
|
||
|
||
// 記錄操作日誌
|
||
$this->logService->logOperation(
|
||
'client',
|
||
$authTokenData['client_id'],
|
||
'Generated new access token'
|
||
);
|
||
|
||
return [
|
||
'access_token' => $accessToken,
|
||
'expires_in' => self::ACCESS_TOKEN_TTL,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 驗證訪問令牌
|
||
*
|
||
* @return array{client_id: int, auth_token: string, created_at: string}|null
|
||
*/
|
||
public function validateAccessToken(string $token): ?array
|
||
{
|
||
$tokenData = Redis::get(self::ACCESS_TOKEN_PREFIX . $token);
|
||
if (!$tokenData) {
|
||
return null;
|
||
}
|
||
|
||
return json_decode($tokenData, true);
|
||
}
|
||
|
||
/**
|
||
* 撤銷訪問令牌
|
||
*/
|
||
public function revokeAccessToken(string $token): void
|
||
{
|
||
$tokenData = Redis::get(self::ACCESS_TOKEN_PREFIX . $token);
|
||
if ($tokenData) {
|
||
$data = json_decode($tokenData, true);
|
||
if ($data) {
|
||
// 記錄操作日誌
|
||
$this->logService->logOperation(
|
||
'client',
|
||
$data['client_id'],
|
||
'Access token revoked'
|
||
);
|
||
}
|
||
// 從 Redis 中刪除
|
||
Redis::del(self::ACCESS_TOKEN_PREFIX . $token);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 撤銷認證令牌
|
||
*/
|
||
public function revokeAuthToken(string $token): void
|
||
{
|
||
$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);
|
||
}
|
||
}
|
||
}
|