study-api-v2/app/Services/Auth/TokenAuthService.php

355 lines
9.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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'),
];
}
}