230 lines
7.3 KiB
PHP
230 lines
7.3 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace app\api\cache;
|
||
|
||
use support\think\Cache;
|
||
|
||
/**
|
||
* API 用户信息 Redis 缓存
|
||
* key = base64(user_id),value = 加密后的用户信息 JSON
|
||
*/
|
||
class UserCache
|
||
{
|
||
private static function prefix(): string
|
||
{
|
||
return config('api.user_cache_prefix', 'api:user:');
|
||
}
|
||
|
||
private static function expire(): int
|
||
{
|
||
return (int) config('api.user_cache_expire', 604800);
|
||
}
|
||
|
||
private static function encryptKey(): string
|
||
{
|
||
$key = config('api.user_encrypt_key', 'dafuweng_api_user_cache_key_32');
|
||
return str_pad($key, 32, '0', STR_PAD_RIGHT);
|
||
}
|
||
|
||
/** 加密 */
|
||
public static function encrypt(string $data): string
|
||
{
|
||
$key = self::encryptKey();
|
||
$iv = substr(md5($key), 0, 16);
|
||
return base64_encode(openssl_encrypt($data, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv));
|
||
}
|
||
|
||
/** 解密 */
|
||
public static function decrypt(string $data): string
|
||
{
|
||
$key = self::encryptKey();
|
||
$iv = substr(md5($key), 0, 16);
|
||
$dec = openssl_decrypt(base64_decode($data, true), 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
|
||
return $dec !== false ? $dec : '';
|
||
}
|
||
|
||
/**
|
||
* 写入用户信息到 Redis
|
||
* @param int $userId
|
||
* @param array $userInfo 从数据库读取的用户信息(可含敏感字段,会加密存储)
|
||
*/
|
||
public static function setUser(int $userId, array $userInfo): bool
|
||
{
|
||
$key = self::prefix() . base64_encode((string) $userId);
|
||
$value = self::encrypt(json_encode($userInfo));
|
||
return Cache::set($key, $value, self::expire());
|
||
}
|
||
|
||
/**
|
||
* 从 Redis 读取用户信息
|
||
* @return array 解密后的用户信息,不存在或失败返回空数组
|
||
*/
|
||
public static function getUser(int $userId): array
|
||
{
|
||
$key = self::prefix() . base64_encode((string) $userId);
|
||
$value = Cache::get($key);
|
||
if ($value === null || $value === '') {
|
||
return [];
|
||
}
|
||
$dec = self::decrypt($value);
|
||
if ($dec === '') {
|
||
return [];
|
||
}
|
||
$data = json_decode($dec, true);
|
||
return is_array($data) ? $data : [];
|
||
}
|
||
|
||
/**
|
||
* 仅从缓存读取用户平台币 coin(不查库,低延迟)
|
||
* @return int|float|null 余额,缓存未命中返回 null(缓存中 coin 可能为字符串,统一转为数值)
|
||
*/
|
||
public static function getUserCoin(int $userId): int|float|null
|
||
{
|
||
$user = self::getUser($userId);
|
||
if (empty($user) || !array_key_exists('coin', $user)) {
|
||
return null;
|
||
}
|
||
$coin = $user['coin'];
|
||
if (is_int($coin) || is_float($coin)) {
|
||
return $coin;
|
||
}
|
||
if (is_string($coin) && is_numeric($coin)) {
|
||
return str_contains($coin, '.') ? (float) $coin : (int) $coin;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** 删除用户缓存 */
|
||
public static function deleteUser(int $userId): bool
|
||
{
|
||
$key = self::prefix() . base64_encode((string) $userId);
|
||
return Cache::delete($key);
|
||
}
|
||
|
||
/** user-token 黑名单前缀(退出登录后使 token 失效) */
|
||
private static function blacklistPrefix(): string
|
||
{
|
||
return config('api.user_cache_prefix', 'api:user:') . 'token_blacklist:';
|
||
}
|
||
|
||
/**
|
||
* 将 user-token 加入黑名单(退出登录)
|
||
* @param string $token 完整 token 字符串
|
||
* @param int $ttl 黑名单过期时间(秒),建议为 token 剩余有效期
|
||
*/
|
||
public static function addTokenToBlacklist(string $token, int $ttl = 86400): bool
|
||
{
|
||
if ($ttl <= 0) {
|
||
return true;
|
||
}
|
||
$key = self::blacklistPrefix() . md5($token);
|
||
return Cache::set($key, '1', $ttl);
|
||
}
|
||
|
||
/**
|
||
* 检查 user-token 是否在黑名单中(已退出)
|
||
*/
|
||
public static function isTokenBlacklisted(string $token): bool
|
||
{
|
||
$key = self::blacklistPrefix() . md5($token);
|
||
$val = Cache::get($key);
|
||
return $val !== null && $val !== '';
|
||
}
|
||
|
||
/** 当前有效 user-token 按用户存储的 key 前缀(重新登录/注册后覆盖,保证单用户单 token) */
|
||
private static function currentTokenPrefix(): string
|
||
{
|
||
return config('api.user_token_current_prefix', 'api:user:current_token:');
|
||
}
|
||
|
||
private static function userTokenExpire(): int
|
||
{
|
||
return (int) config('api.user_token_exp', 604800);
|
||
}
|
||
|
||
/**
|
||
* 设置该用户当前唯一有效的 user-token(登录/注册时调用,会覆盖该用户之前的 token)
|
||
* @param int $userId 用户 ID
|
||
* @param string $token 完整 user-token 字符串
|
||
*/
|
||
public static function setCurrentUserToken(int $userId, string $token): bool
|
||
{
|
||
if ($userId <= 0 || $token === '') {
|
||
return false;
|
||
}
|
||
$key = self::currentTokenPrefix() . $userId;
|
||
return Cache::set($key, $token, self::userTokenExpire());
|
||
}
|
||
|
||
/**
|
||
* 获取该用户当前在服务端登记的有效 user-token,不存在或已过期返回 null
|
||
*/
|
||
public static function getCurrentUserToken(int $userId): ?string
|
||
{
|
||
if ($userId <= 0) {
|
||
return null;
|
||
}
|
||
$key = self::currentTokenPrefix() . $userId;
|
||
$value = Cache::get($key);
|
||
return $value !== null && $value !== '' ? (string) $value : null;
|
||
}
|
||
|
||
/**
|
||
* 校验请求中的 token 是否为该用户当前唯一有效 token
|
||
*/
|
||
public static function isCurrentUserToken(int $userId, string $token): bool
|
||
{
|
||
$current = self::getCurrentUserToken($userId);
|
||
return $current !== null && $current === $token;
|
||
}
|
||
|
||
/** 按 username 的登录会话 key 前缀(token 中间件:存在即视为已登录) */
|
||
private static function sessionUsernamePrefix(): string
|
||
{
|
||
return config('api.session_username_prefix', 'api:user:session:');
|
||
}
|
||
|
||
private static function sessionExpire(): int
|
||
{
|
||
return (int) config('api.session_expire', 604800);
|
||
}
|
||
|
||
/** 设置 username 当前有效 token(JWT),重新登录会覆盖,实现单点登录 */
|
||
public static function setSessionByUsername(string $username, string $token): bool
|
||
{
|
||
if ($username === '' || $token === '') {
|
||
return false;
|
||
}
|
||
$key = self::sessionUsernamePrefix() . $username;
|
||
return Cache::set($key, $token, self::sessionExpire());
|
||
}
|
||
|
||
/** 获取 username 当前在服务端登记的有效 token(JWT),不存在返回 null */
|
||
public static function getSessionTokenByUsername(string $username): ?string
|
||
{
|
||
if ($username === '') {
|
||
return null;
|
||
}
|
||
$key = self::sessionUsernamePrefix() . $username;
|
||
$val = Cache::get($key);
|
||
return $val !== null && $val !== '' ? (string) $val : null;
|
||
}
|
||
|
||
/** 检查 username 是否已有登录会话(Redis 中是否存在当前 token) */
|
||
public static function hasSessionByUsername(string $username): bool
|
||
{
|
||
return self::getSessionTokenByUsername($username) !== null;
|
||
}
|
||
|
||
/** 删除 username 登录会话(退出登录时调用) */
|
||
public static function deleteSessionByUsername(string $username): bool
|
||
{
|
||
if ($username === '') {
|
||
return false;
|
||
}
|
||
$key = self::sessionUsernamePrefix() . $username;
|
||
return Cache::delete($key);
|
||
}
|
||
}
|