token system fix

This commit is contained in:
Jethro Lin 2024-12-04 13:32:14 +08:00
parent 33dde9dc2c
commit 4b1ed6ade3
3 changed files with 120 additions and 34 deletions

View file

@ -15,6 +15,7 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Illuminate\Support\Facades\Redis;
class ClientController extends Controller class ClientController extends Controller
{ {
@ -273,35 +274,109 @@ public function update(Request $request, int $id): JsonResponse
} }
/** /**
* 删除客户 * 檢查客戶是否有活躍的令牌
*
* @param Request $request
* @param int $id
* @return JsonResponse
*/ */
public function destroy(Request $request, int $id): JsonResponse private function hasActiveTokens(Client $client): bool
{
$pattern = 'auth_token:*';
$keys = Redis::keys($pattern);
foreach ($keys as $key) {
$data = Redis::get($key);
if ($data) {
$tokenData = json_decode($data, true);
if ($tokenData &&
$tokenData['client_id'] === $client->id &&
now()->isBefore($tokenData['expires_at'])) {
return true;
}
}
}
return false;
}
/**
* 停用客戶
*/
public function deactivate(int $id): JsonResponse
{ {
try { try {
$client = Client::findOrFail($id); $client = Client::findOrFail($id);
// 检查是否有活跃的认证令牌 if (!$this->admin->canManageClient($client->id)) {
if ($client->authTokens()->where('status', 'active')->exists()) {
return $this->error( return $this->error(
ErrorCode::CLIENT_HAS_ACTIVE_TOKENS, ErrorCode::FORBIDDEN,
'该客户有活跃的认证令牌,请先停用或删除令牌。' '您無權管理該客戶。'
);
}
if ($this->hasActiveTokens($client)) {
return $this->error(
ErrorCode::CLIENT_HAS_ACTIVE_TOKENS,
'該客戶有活躍的認證令牌,無法停用。'
);
}
$client->update(['status' => Client::STATUS_INACTIVE]);
$this->logService->logOperation(
'admin',
$this->admin->id,
"Deactivated client: {$client->name}"
);
return $this->success();
} catch (ModelNotFoundException $e) {
return $this->error(
ErrorCode::CLIENT_NOT_FOUND,
ErrorCode::getMessage(ErrorCode::CLIENT_NOT_FOUND)
);
} catch (\Exception $e) {
Log::error('Error deactivating client', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return $this->error(
ErrorCode::SERVER_ERROR,
ErrorCode::getMessage(ErrorCode::SERVER_ERROR)
);
}
}
/**
* 刪除客戶
*/
public function destroy(int $id): JsonResponse
{
try {
$client = Client::findOrFail($id);
if (!$this->admin->canManageClient($client->id)) {
return $this->error(
ErrorCode::FORBIDDEN,
'您無權管理該客戶。'
);
}
if ($this->hasActiveTokens($client)) {
return $this->error(
ErrorCode::CLIENT_HAS_ACTIVE_TOKENS,
'該客戶有活躍的認證令牌,無法刪除。'
); );
} }
$clientName = $client->name;
$client->delete(); $client->delete();
$this->logService->logOperation( $this->logService->logOperation(
'admin', 'admin',
$request->admin->id, $this->admin->id,
"Deleted client: {$clientName}" "Deleted client: {$client->name}"
); );
return $this->success(null, '客户已删除。'); return $this->success();
} catch (ModelNotFoundException $e) { } catch (ModelNotFoundException $e) {
return $this->error( return $this->error(
@ -312,7 +387,6 @@ public function destroy(Request $request, int $id): JsonResponse
Log::error('Error deleting client', [ Log::error('Error deleting client', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
'client_id' => $id,
]); ]);
return $this->error( return $this->error(

View file

@ -7,7 +7,6 @@
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Client extends Model class Client extends Model
{ {
@ -52,11 +51,6 @@ public function admins(): BelongsToMany
->withTimestamps(); ->withTimestamps();
} }
public function authTokens(): HasMany
{
return $this->hasMany(AuthToken::class);
}
/** /**
* 检查客户是否处于活跃状态 * 检查客户是否处于活跃状态
*/ */

View file

@ -4,7 +4,6 @@
namespace App\Services\Auth; namespace App\Services\Auth;
use App\Models\AuthToken;
use App\Models\Client; use App\Models\Client;
use App\Services\LogService; use App\Services\LogService;
use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Redis;
@ -22,7 +21,12 @@ public function __construct(
private readonly LogService $logService private readonly LogService $logService
) {} ) {}
public function generateAuthToken(Client $client, ?int $expiresInHours = null): AuthToken /**
* 生成認證令牌
*
* @return array{token: string, expires_at: string}
*/
public function generateAuthToken(Client $client, ?int $expiresInHours = null): array
{ {
// 確保不超過最大有效期 // 確保不超過最大有效期
$effectiveHours = min($expiresInHours ?? self::MAX_AUTH_TOKEN_HOURS, self::MAX_AUTH_TOKEN_HOURS); $effectiveHours = min($expiresInHours ?? self::MAX_AUTH_TOKEN_HOURS, self::MAX_AUTH_TOKEN_HOURS);
@ -38,6 +42,7 @@ public function generateAuthToken(Client $client, ?int $expiresInHours = null):
$effectiveSeconds, $effectiveSeconds,
json_encode([ json_encode([
'client_id' => $client->id, 'client_id' => $client->id,
'token' => $token,
'created_at' => now()->toIso8601String(), 'created_at' => now()->toIso8601String(),
'expires_at' => $expiresAt->toIso8601String(), 'expires_at' => $expiresAt->toIso8601String(),
]) ])
@ -50,14 +55,17 @@ public function generateAuthToken(Client $client, ?int $expiresInHours = null):
'Generated new auth token' 'Generated new auth token'
); );
// 返回令牌對象(僅用於 API 響應) return [
return new AuthToken([
'client_id' => $client->id,
'token' => $token, 'token' => $token,
'expires_at' => $expiresAt, 'expires_at' => $expiresAt->toIso8601String(),
]); ];
} }
/**
* 驗證認證令牌
*
* @return array{client_id: int, token: string, created_at: string, expires_at: string}|null
*/
public function validateAuthToken(string $token): ?array public function validateAuthToken(string $token): ?array
{ {
// 檢查黑名單 // 檢查黑名單
@ -80,6 +88,11 @@ public function validateAuthToken(string $token): ?array
return $data; return $data;
} }
/**
* 生成訪問令牌
*
* @return array{access_token: string, expires_in: int, token_type: string}
*/
public function generateAccessToken(array $authTokenData): array public function generateAccessToken(array $authTokenData): array
{ {
$accessToken = Str::random(64); $accessToken = Str::random(64);
@ -109,6 +122,11 @@ public function generateAccessToken(array $authTokenData): array
]; ];
} }
/**
* 驗證訪問令牌
*
* @return array{client_id: int, auth_token: string, created_at: string}|null
*/
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);
@ -119,6 +137,9 @@ public function validateAccessToken(string $token): ?array
return json_decode($tokenData, true); return json_decode($tokenData, true);
} }
/**
* 撤銷訪問令牌
*/
public function revokeAccessToken(string $token): void public function revokeAccessToken(string $token): void
{ {
$tokenData = Redis::get(self::ACCESS_TOKEN_PREFIX . $token); $tokenData = Redis::get(self::ACCESS_TOKEN_PREFIX . $token);
@ -137,6 +158,9 @@ public function revokeAccessToken(string $token): void
} }
} }
/**
* 撤銷認證令牌
*/
public function revokeAuthToken(string $token): void public function revokeAuthToken(string $token): void
{ {
$tokenData = Redis::get(self::AUTH_TOKEN_PREFIX . $token); $tokenData = Redis::get(self::AUTH_TOKEN_PREFIX . $token);
@ -165,10 +189,4 @@ public function revokeAuthToken(string $token): void
Redis::del(self::AUTH_TOKEN_PREFIX . $token); Redis::del(self::AUTH_TOKEN_PREFIX . $token);
} }
} }
public function cleanupExpiredTokens(): void
{
// Redis 會自動清理過期的令牌,無需手動清理
// 這個方法保留用於日誌記錄或其他清理工作
}
} }