355 lines
9.7 KiB
PHP
355 lines
9.7 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Auth;
|
||
|
||
use App\Models\User;
|
||
use Laravel\Sanctum\PersonalAccessToken;
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Carbon\Carbon;
|
||
use Exception;
|
||
|
||
/**
|
||
* Token认证服务
|
||
*
|
||
* 提供token验证、用户信息缓存、缓存清理等功能
|
||
* 减少数据库查询次数,提高API响应速度
|
||
*/
|
||
class TokenAuthService
|
||
{
|
||
/**
|
||
* 缓存键前缀
|
||
*/
|
||
const CACHE_PREFIX = 'auth_token:';
|
||
|
||
/**
|
||
* 用户缓存键前缀
|
||
*/
|
||
const USER_CACHE_PREFIX = 'auth_user:';
|
||
|
||
/**
|
||
* 默认缓存时间(分钟)
|
||
*/
|
||
const DEFAULT_CACHE_MINUTES = 5;
|
||
|
||
/**
|
||
* 最大缓存时间(分钟)
|
||
*/
|
||
const MAX_CACHE_MINUTES = 30;
|
||
|
||
/**
|
||
* 缓存是否可用
|
||
*/
|
||
private bool $cacheAvailable = true;
|
||
|
||
/**
|
||
* 验证token并获取用户信息(带缓存)
|
||
*
|
||
* @param string $token
|
||
* @param string $guard
|
||
* @return User|null
|
||
*/
|
||
public function validateTokenAndGetUser(string $token, string $guard = 'sanctum'): ?User
|
||
{
|
||
// 检查缓存可用性
|
||
if (!$this->checkCacheHealth()) {
|
||
Log::warning('缓存不可用,直接从数据库验证token');
|
||
return $this->validateTokenFromDatabase($token, $guard);
|
||
}
|
||
|
||
// 生成缓存键
|
||
$cacheKey = $this->getTokenCacheKey($token);
|
||
|
||
// 尝试从缓存获取用户信息
|
||
try {
|
||
$cachedUser = Cache::get($cacheKey);
|
||
if ($cachedUser && $cachedUser instanceof User) {
|
||
Log::debug('Token认证使用缓存', ['token_hash' => hash('sha256', $token)]);
|
||
return $cachedUser;
|
||
}
|
||
} catch (Exception $e) {
|
||
Log::warning('缓存读取失败,使用数据库验证', ['error' => $e->getMessage()]);
|
||
}
|
||
|
||
// 缓存未命中或失败,从数据库查询
|
||
$user = $this->validateTokenFromDatabase($token, $guard);
|
||
|
||
if ($user && $this->cacheAvailable) {
|
||
try {
|
||
// 计算缓存时间并存储
|
||
$cacheMinutes = $this->calculateCacheTime($token);
|
||
Cache::put($cacheKey, $user, now()->addMinutes($cacheMinutes));
|
||
|
||
Log::debug('Token认证结果已缓存', [
|
||
'user_id' => $user->id,
|
||
'cache_minutes' => $cacheMinutes,
|
||
'token_hash' => hash('sha256', $token)
|
||
]);
|
||
} catch (Exception $e) {
|
||
Log::warning('缓存存储失败', ['error' => $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
return $user;
|
||
}
|
||
|
||
/**
|
||
* 从数据库验证token
|
||
*
|
||
* @param string $token
|
||
* @param string $guard
|
||
* @return User|null
|
||
*/
|
||
protected function validateTokenFromDatabase(string $token, string $guard): ?User
|
||
{
|
||
try {
|
||
// 使用Sanctum验证token
|
||
$accessToken = PersonalAccessToken::findToken($token);
|
||
|
||
if (!$accessToken) {
|
||
return null;
|
||
}
|
||
|
||
// 检查token是否过期
|
||
if ($accessToken->expires_at && $accessToken->expires_at->isPast()) {
|
||
return null;
|
||
}
|
||
|
||
// 获取token对应的用户
|
||
$user = $accessToken->tokenable;
|
||
|
||
if (!$user || !($user instanceof User)) {
|
||
return null;
|
||
}
|
||
|
||
// 检查用户状态
|
||
if (!$user->is_active) {
|
||
return null;
|
||
}
|
||
|
||
// 更新最后使用时间
|
||
$accessToken->update(['last_used_at' => now()]);
|
||
|
||
return $user;
|
||
|
||
} catch (\Exception $e) {
|
||
Log::error('Token验证失败', [
|
||
'error' => $e->getMessage(),
|
||
'token_hash' => hash('sha256', $token)
|
||
]);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算缓存时间
|
||
* 根据token过期时间来决定缓存时间
|
||
*
|
||
* @param string $token
|
||
* @return int 缓存分钟数
|
||
*/
|
||
protected function calculateCacheTime(string $token): int
|
||
{
|
||
try {
|
||
$accessToken = PersonalAccessToken::findToken($token);
|
||
|
||
if (!$accessToken || !$accessToken->expires_at) {
|
||
return self::DEFAULT_CACHE_MINUTES;
|
||
}
|
||
|
||
// 计算token剩余时间(分钟)
|
||
$remainingMinutes = now()->diffInMinutes($accessToken->expires_at, false);
|
||
|
||
if ($remainingMinutes <= 0) {
|
||
return 1; // token即将过期,缓存1分钟
|
||
}
|
||
|
||
// 取剩余时间的1/4作为缓存时间,但不超过最大缓存时间
|
||
$cacheMinutes = min(
|
||
max(1, floor($remainingMinutes / 4)),
|
||
self::MAX_CACHE_MINUTES
|
||
);
|
||
|
||
return (int) $cacheMinutes;
|
||
|
||
} catch (\Exception $e) {
|
||
Log::warning('计算缓存时间失败', ['error' => $e->getMessage()]);
|
||
return self::DEFAULT_CACHE_MINUTES;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清空指定token的缓存
|
||
*
|
||
* @param string $token
|
||
* @return bool
|
||
*/
|
||
public function clearTokenCache(string $token): bool
|
||
{
|
||
$cacheKey = $this->getTokenCacheKey($token);
|
||
$result = Cache::forget($cacheKey);
|
||
|
||
Log::info('清空Token缓存', [
|
||
'token_hash' => hash('sha256', $token),
|
||
'success' => $result
|
||
]);
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 清空指定用户的所有token缓存
|
||
*
|
||
* @param int $userId
|
||
* @return int 清空的缓存数量
|
||
*/
|
||
public function clearUserTokenCache(int $userId): int
|
||
{
|
||
$user = User::find($userId);
|
||
if (!$user) {
|
||
return 0;
|
||
}
|
||
|
||
$clearedCount = 0;
|
||
|
||
// 由于无法直接获取原始token,这里使用模式匹配清空缓存
|
||
// 实际应用中建议维护一个用户token的映射关系
|
||
try {
|
||
// 获取所有以用户ID结尾的token缓存键(这是一个简化的方案)
|
||
// 更好的方案是在缓存时同时维护用户->token的映射
|
||
$pattern = self::CACHE_PREFIX . '*';
|
||
|
||
// 注意:这种方法效率不高,建议在实际应用中优化
|
||
// 可以考虑维护一个用户token列表的缓存
|
||
Log::info('清空用户Token缓存(注意:当前实现会在用户登出时调用clearCacheOnLogout方法)', [
|
||
'user_id' => $userId,
|
||
'note' => '建议使用clearCacheOnLogout方法传入具体token'
|
||
]);
|
||
|
||
} catch (\Exception $e) {
|
||
Log::error('清空用户Token缓存失败', [
|
||
'user_id' => $userId,
|
||
'error' => $e->getMessage()
|
||
]);
|
||
}
|
||
|
||
return $clearedCount;
|
||
}
|
||
|
||
/**
|
||
* 清空所有认证相关缓存
|
||
*
|
||
* @return bool
|
||
*/
|
||
public function clearAllAuthCache(): bool
|
||
{
|
||
try {
|
||
// 清空所有token相关缓存
|
||
$tags = ['auth_tokens'];
|
||
Cache::tags($tags)->flush();
|
||
|
||
Log::info('清空所有认证缓存');
|
||
return true;
|
||
|
||
} catch (\Exception $e) {
|
||
Log::error('清空所有认证缓存失败', ['error' => $e->getMessage()]);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 在用户登出时清空token缓存
|
||
*
|
||
* @param User $user
|
||
* @param string|null $currentToken 当前token,如果提供则只清空当前token
|
||
* @return int 清空的缓存数量
|
||
*/
|
||
public function clearCacheOnLogout(User $user, ?string $currentToken = null): int
|
||
{
|
||
if ($currentToken) {
|
||
// 只清空当前token缓存
|
||
return $this->clearTokenCache($currentToken) ? 1 : 0;
|
||
}
|
||
|
||
// 清空用户所有token缓存
|
||
return $this->clearUserTokenCache($user->id);
|
||
}
|
||
|
||
/**
|
||
* 获取token缓存键
|
||
*
|
||
* @param string $token
|
||
* @return string
|
||
*/
|
||
protected function getTokenCacheKey(string $token): string
|
||
{
|
||
return self::CACHE_PREFIX . hash('sha256', $token);
|
||
}
|
||
|
||
/**
|
||
* 获取用户缓存键
|
||
*
|
||
* @param int $userId
|
||
* @return string
|
||
*/
|
||
protected function getUserCacheKey(int $userId): string
|
||
{
|
||
return self::USER_CACHE_PREFIX . $userId;
|
||
}
|
||
|
||
/**
|
||
* 检查缓存健康状态
|
||
*
|
||
* @return bool
|
||
*/
|
||
protected function checkCacheHealth(): bool
|
||
{
|
||
if (!$this->cacheAvailable) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
// 测试缓存连接
|
||
$testKey = 'cache_health_check_' . time();
|
||
$testValue = 'test';
|
||
|
||
Cache::put($testKey, $testValue, 60);
|
||
$result = Cache::get($testKey);
|
||
Cache::forget($testKey);
|
||
|
||
$isHealthy = ($result === $testValue);
|
||
|
||
if (!$isHealthy) {
|
||
$this->cacheAvailable = false;
|
||
Log::error('缓存健康检查失败:读写测试不一致');
|
||
}
|
||
|
||
return $isHealthy;
|
||
|
||
} catch (Exception $e) {
|
||
$this->cacheAvailable = false;
|
||
Log::error('缓存健康检查失败', ['error' => $e->getMessage()]);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取缓存统计信息
|
||
*
|
||
* @return array
|
||
*/
|
||
public function getCacheStats(): array
|
||
{
|
||
$health = $this->checkCacheHealth();
|
||
|
||
return [
|
||
'cache_prefix' => self::CACHE_PREFIX,
|
||
'default_cache_minutes' => self::DEFAULT_CACHE_MINUTES,
|
||
'max_cache_minutes' => self::MAX_CACHE_MINUTES,
|
||
'cache_available' => $this->cacheAvailable,
|
||
'cache_health' => $health,
|
||
'cache_store' => config('cache.default'),
|
||
];
|
||
}
|
||
}
|