856 lines
24 KiB
PHP
856 lines
24 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace core\jwt;
|
||
|
||
use core\jwt\enum\ClientType;
|
||
use core\jwt\enum\LoginMode;
|
||
use core\jwt\enum\TokenStatus;
|
||
use core\jwt\enum\TokenType;
|
||
use core\jwt\ex\JwtException;
|
||
use core\jwt\ex\JwtRefreshTokenExpiredException;
|
||
use core\jwt\ex\JwtTokenExpiredException;
|
||
use core\jwt\ex\JwtTokenInBlacklistException;
|
||
use core\jwt\ex\JwtTokenInvalidException;
|
||
use core\jwt\interfaces\BlacklistStorageInterface;
|
||
use core\jwt\interfaces\TokenStorageInterface;
|
||
use core\jwt\storage\RedisTokenStorage;
|
||
use core\jwt\storage\RedisBlacklistStorage;
|
||
use Firebase\JWT\BeforeValidException;
|
||
use Firebase\JWT\ExpiredException;
|
||
use Firebase\JWT\JWT;
|
||
use Firebase\JWT\Key;
|
||
use Firebase\JWT\SignatureInvalidException;
|
||
use UnexpectedValueException;
|
||
|
||
class JwtToken
|
||
{
|
||
/**
|
||
* @var string 密钥
|
||
*/
|
||
protected string $secret;
|
||
|
||
/**
|
||
* @var string 加密算法
|
||
*/
|
||
protected string $algo;
|
||
|
||
/**
|
||
* @var array 配置
|
||
*/
|
||
protected array $config;
|
||
|
||
/**
|
||
* @var TokenStorageInterface Token 存储
|
||
*/
|
||
protected TokenStorageInterface $tokenStorage;
|
||
|
||
/**
|
||
* @var BlacklistStorageInterface 黑名单存储
|
||
*/
|
||
protected BlacklistStorageInterface $blacklistStorage;
|
||
|
||
/**
|
||
* @var Token|null 当前 Token
|
||
*/
|
||
protected ?Token $currentToken = null;
|
||
|
||
/**
|
||
* @var array|null 当前载荷缓存
|
||
*/
|
||
protected ?array $payloadCache = null;
|
||
|
||
public function __construct(
|
||
?TokenStorageInterface $tokenStorage = null,
|
||
?BlacklistStorageInterface $blacklistStorage = null,
|
||
array $config = []
|
||
)
|
||
{
|
||
$this->config = $config ?: config('core.jwt.app', []);
|
||
$this->secret = $this->config['secret'] ?? '';
|
||
$this->algo = $this->config['algo'] ?? 'HS256';
|
||
|
||
if (empty($this->secret) || $this->secret === 'your-secret-key-change-in-production') {
|
||
$this->secret = 'default-secret-key-for-development';
|
||
}
|
||
|
||
$this->tokenStorage = $tokenStorage ?? new RedisTokenStorage($this->config);
|
||
$this->blacklistStorage = $blacklistStorage ?? new RedisBlacklistStorage($this->config);
|
||
}
|
||
|
||
/**
|
||
* 获取 Token 存储
|
||
*/
|
||
public function getTokenStorage(): TokenStorageInterface
|
||
{
|
||
return $this->tokenStorage;
|
||
}
|
||
|
||
/**
|
||
* 获取黑名单存储
|
||
*/
|
||
public function getBlacklistStorage(): BlacklistStorageInterface
|
||
{
|
||
return $this->blacklistStorage;
|
||
}
|
||
|
||
/**
|
||
* 生成 Token
|
||
*
|
||
* @param string $id 用户ID(雪花ID,需用string)
|
||
* @param string $clientType 客户端类型(可扩展)
|
||
* @param array $extraData 额外数据
|
||
* @param string|null $loginMode 登录模式(null 使用配置默认)
|
||
*
|
||
* @return Token
|
||
* @throws \core\jwt2\ex\JwtException
|
||
*/
|
||
public function generate(
|
||
string $id,
|
||
string $clientType,
|
||
array $extraData = [],
|
||
?string $loginMode = null
|
||
): Token
|
||
{
|
||
// 验证客户端类型
|
||
if (!ClientType::isValid($clientType)) {
|
||
throw new JwtException("Invalid client type: {$clientType}");
|
||
}
|
||
|
||
$loginMode = $loginMode ?? ($this->config['login_mode'] ?? LoginMode::MULTI->value);
|
||
|
||
// 根据登录模式处理现有 Token
|
||
$this->handleLoginMode($id, $clientType, $loginMode);
|
||
|
||
// 生成 JTI
|
||
$jti = $this->generateJti();
|
||
|
||
// 生成 Access Token
|
||
$accessTtl = $this->config['ttl']['access'] ?? 7200;
|
||
$accessPayload = $this->buildPayload(
|
||
id: $id,
|
||
client: $clientType,
|
||
jti: $jti,
|
||
type: TokenType::ACCESS,
|
||
extra: $extraData
|
||
);
|
||
$accessToken = $this->makeToken($accessPayload);
|
||
|
||
// 生成 Refresh Token
|
||
$refreshTtl = $this->config['ttl']['refresh'] ?? 604800;
|
||
$refreshJti = $this->generateJti();
|
||
$refreshPayload = $this->buildPayload(
|
||
id: $id,
|
||
client: $clientType,
|
||
jti: $refreshJti,
|
||
type: TokenType::REFRESH,
|
||
extra: $extraData
|
||
);
|
||
$refreshToken = $this->makeToken($refreshPayload);
|
||
|
||
// 存储 Token 信息
|
||
$this->tokenStorage->save($jti, [
|
||
'id' => $id,
|
||
'client_type' => $clientType,
|
||
'refresh_jti' => $refreshJti,
|
||
'extra' => $extraData,
|
||
'login_mode' => $loginMode,
|
||
'created_at' => time(),
|
||
], $accessTtl);
|
||
|
||
// 创建 Token 对象
|
||
$fullPayload = array_merge($accessPayload, ['id' => $id]);
|
||
return Token::create($accessToken, $refreshToken, $accessTtl, $fullPayload);
|
||
}
|
||
|
||
/**
|
||
* 刷新 Token
|
||
*
|
||
* @param string|null $refreshToken 刷新令牌(null 使用请求中的)
|
||
*
|
||
* @return Token
|
||
* @throws \core\jwt\ex\JwtException
|
||
* @throws \core\jwt\ex\JwtRefreshTokenExpiredException
|
||
* @throws \core\jwt\ex\JwtTokenInBlacklistException
|
||
*/
|
||
public function refresh(?string $refreshToken = null): Token
|
||
{
|
||
$token = $refreshToken ?? $this->getRefreshTokenFromRequest();
|
||
|
||
if (empty($token)) {
|
||
throw new JwtException('Refresh token is required');
|
||
}
|
||
|
||
// 解析 Token
|
||
try {
|
||
$payload = $this->parseToken($token);
|
||
} catch (ExpiredException $e) {
|
||
throw new JwtRefreshTokenExpiredException('Refresh token has expired');
|
||
}
|
||
|
||
// 验证 Token 类型
|
||
if (($payload['type'] ?? '') !== TokenType::REFRESH->value) {
|
||
throw new JwtException('Invalid token type for refresh');
|
||
}
|
||
|
||
$id = (string)($payload['id'] ?? '');
|
||
$clientType = $payload['client'] ?? '';
|
||
$oldJti = $payload['jti'] ?? '';
|
||
|
||
if (empty($id)) {
|
||
throw new JwtException('Invalid user in token');
|
||
}
|
||
|
||
// 检查刷新令牌是否在黑名单
|
||
if ($this->blacklistStorage->has($oldJti)) {
|
||
throw new JwtTokenInBlacklistException('Token has been revoked');
|
||
}
|
||
|
||
// 通过 refresh_jti 反查对应的 access token 会话
|
||
[$oldAccessJti, $oldTokenInfo] = $this->findAccessTokenByRefreshJti($id, $clientType, $oldJti);
|
||
if ($oldTokenInfo === null) {
|
||
throw new JwtRefreshTokenExpiredException('Refresh token has been revoked');
|
||
}
|
||
|
||
// 获取额外数据
|
||
$extraData = $oldTokenInfo['extra'] ?? [];
|
||
$loginMode = $this->config['login_mode'] ?? $oldTokenInfo['login_mode'] ?? LoginMode::MULTI->value;
|
||
|
||
// 将旧 access_token 和 refresh_token 加入黑名单
|
||
$blacklistTtl = $this->config['ttl']['refresh'] ?? 604800;
|
||
if ($oldAccessJti !== '') {
|
||
$this->blacklistStorage->add($oldAccessJti, $blacklistTtl);
|
||
$this->tokenStorage->delete($oldAccessJti);
|
||
}
|
||
$this->blacklistStorage->add($oldJti, $blacklistTtl);
|
||
|
||
// 生成新 Token
|
||
return $this->generate($id, $clientType, $extraData, $loginMode);
|
||
}
|
||
|
||
/**
|
||
* 解析 Token
|
||
*
|
||
* @param string $token
|
||
*
|
||
* @return array
|
||
*/
|
||
public function parse(string $token): array
|
||
{
|
||
return $this->parseToken($token);
|
||
}
|
||
|
||
/**
|
||
* 获取当前用户
|
||
*
|
||
* @return array|null
|
||
*/
|
||
public function user(): ?array
|
||
{
|
||
$payload = $this->getPayloadFromRequest();
|
||
if (empty($payload)) {
|
||
return null;
|
||
}
|
||
|
||
return [
|
||
'id' => (string)($payload['id'] ?? ''),
|
||
'client_type' => $payload['client'] ?? '',
|
||
'jti' => $payload['jti'] ?? '',
|
||
'extra' => $payload['extra'] ?? [],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 获取当前用户ID(雪花ID)
|
||
*/
|
||
public function id(): ?string
|
||
{
|
||
$user = $this->user();
|
||
return $user ? $user['id'] : null;
|
||
}
|
||
|
||
/**
|
||
* 获取当前 Token
|
||
*/
|
||
public function getToken(): ?Token
|
||
{
|
||
if ($this->currentToken !== null) {
|
||
return $this->currentToken;
|
||
}
|
||
|
||
$payload = $this->getPayloadFromRequest();
|
||
if (empty($payload)) {
|
||
return null;
|
||
}
|
||
|
||
$accessToken = $this->getAccessTokenFromRequest();
|
||
if (empty($accessToken)) {
|
||
return null;
|
||
}
|
||
|
||
$accessTtl = $this->config['ttl']['access'] ?? 7200;
|
||
$this->currentToken = Token::create($accessToken, '', $accessTtl, $payload);
|
||
|
||
return $this->currentToken;
|
||
}
|
||
|
||
/**
|
||
* 验证 Token
|
||
*
|
||
* @param string|null $token
|
||
*
|
||
* @return array|null
|
||
* @throws \core\jwt\ex\JwtException
|
||
* @throws \core\jwt\ex\JwtTokenExpiredException
|
||
* @throws \core\jwt\ex\JwtTokenInBlacklistException
|
||
* @throws \core\jwt\ex\JwtTokenInvalidException
|
||
*/
|
||
public function verify(?string $token = null): ?array
|
||
{
|
||
$token = $token ?? $this->getAccessTokenFromRequest();
|
||
|
||
if (empty($token)) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
$payload = $this->parseToken($token);
|
||
|
||
// 检查是否在黑名单
|
||
$jti = $payload['jti'] ?? '';
|
||
if (!empty($jti) && $this->blacklistStorage->has($jti)) {
|
||
throw new JwtTokenInBlacklistException('Token has been revoked');
|
||
}
|
||
|
||
// 检查 Token 类型
|
||
if (($payload['type'] ?? '') !== TokenType::ACCESS->value) {
|
||
throw new JwtException('Invalid token type');
|
||
}
|
||
|
||
// 检查用户是否存在
|
||
$id = (string)($payload['id'] ?? '');
|
||
if (empty($id)) {
|
||
throw new JwtException('Invalid user in token');
|
||
}
|
||
|
||
return $payload;
|
||
} catch (ExpiredException $e) {
|
||
throw new JwtTokenExpiredException('Token has expired');
|
||
} catch (SignatureInvalidException $e) {
|
||
throw new JwtTokenInvalidException('Invalid token signature');
|
||
} catch (BeforeValidException $e) {
|
||
throw new JwtTokenInvalidException('Token not yet valid');
|
||
} catch (UnexpectedValueException $e) {
|
||
throw new JwtTokenInvalidException('Invalid token format');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 退出登录(当前设备)
|
||
*
|
||
* @param string|null $token
|
||
*
|
||
* @return bool
|
||
*/
|
||
public function logout(?string $token = null): bool
|
||
{
|
||
$token = $token ?? $this->getAccessTokenFromRequest();
|
||
|
||
if (empty($token)) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
$payload = $this->parseToken($token);
|
||
$jti = $payload['jti'] ?? '';
|
||
|
||
if (!empty($jti)) {
|
||
// 将 Token 加入黑名单
|
||
$this->blacklistStorage->add($jti, $this->config['ttl']['access'] ?? 7200);
|
||
|
||
// 获取并删除 refresh_token
|
||
$tokenInfo = $this->tokenStorage->get($jti);
|
||
if ($tokenInfo && !empty($tokenInfo['refresh_jti'])) {
|
||
$this->tokenStorage->delete($tokenInfo['refresh_jti']);
|
||
}
|
||
|
||
// 删除 Token 信息
|
||
$this->tokenStorage->delete($jti);
|
||
}
|
||
|
||
return true;
|
||
} catch (\Throwable $e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 退出所有设备
|
||
*
|
||
* @param string $id
|
||
* @param string|null $clientType
|
||
*
|
||
* @return int 退出设备数
|
||
*/
|
||
/**
|
||
* 退出所有设备
|
||
*
|
||
* @param string $id
|
||
* @param string|null $clientType
|
||
*
|
||
* @return int
|
||
*/
|
||
public function logoutAll(string $id, ?string $clientType = null): int
|
||
{
|
||
// 获取用户的所有 token 信息
|
||
$tokens = $this->tokenStorage->getUserTokens($id, $clientType);
|
||
|
||
if (empty($tokens)) {
|
||
return 0;
|
||
}
|
||
|
||
$count = 0;
|
||
// 将所有 token 加入黑名单并删除
|
||
foreach ($tokens as $jti => $info) {
|
||
// 将 access_token 加入黑名单(使用 refresh_ttl 确保足够长)
|
||
$blacklistTtl = $this->config['ttl']['refresh'] ?? 604800;
|
||
$this->blacklistStorage->add($jti, $blacklistTtl);
|
||
|
||
// 将 refresh_token 也加入黑名单
|
||
if (!empty($info['refresh_jti'])) {
|
||
$this->blacklistStorage->add($info['refresh_jti'], $blacklistTtl);
|
||
// 删除 refresh_token 存储
|
||
$this->tokenStorage->delete($info['refresh_jti']);
|
||
}
|
||
|
||
// 删除 access_token 存储
|
||
$this->tokenStorage->delete($jti);
|
||
$count++;
|
||
}
|
||
|
||
// 清理用户索引(deleteByUser 只会清理索引,不会删除已删除的 token)
|
||
$this->tokenStorage->deleteByUser($id, $clientType);
|
||
|
||
return $count;
|
||
}
|
||
|
||
/**
|
||
* 退出其他设备(保留当前)
|
||
*
|
||
* @param string|null $id
|
||
* @param string|null $clientType
|
||
* @param string|null $exceptJti 保留的 JTI
|
||
*
|
||
* @return int 退出设备数
|
||
*/
|
||
public function logoutOthers(?string $id = null, ?string $clientType = null, ?string $exceptJti = null): int
|
||
{
|
||
$id = $id ?? $this->id();
|
||
if ($id === null) {
|
||
return 0;
|
||
}
|
||
|
||
$exceptJti = $exceptJti ?? ($this->getPayloadFromRequest()['jti'] ?? null);
|
||
|
||
// 获取用户的所有 token 信息
|
||
$tokens = $this->tokenStorage->getUserTokens($id, $clientType);
|
||
|
||
if (empty($tokens)) {
|
||
return 0;
|
||
}
|
||
|
||
$blacklistTtl = $this->config['ttl']['refresh'] ?? 604800;
|
||
$count = 0;
|
||
|
||
// 遍历并处理需要删除的 token
|
||
foreach ($tokens as $jti => $info) {
|
||
// 跳过保留的 JTI
|
||
if ($exceptJti !== null && $jti === $exceptJti) {
|
||
continue;
|
||
}
|
||
|
||
// 将 access_token 加入黑名单
|
||
$this->blacklistStorage->add($jti, $blacklistTtl);
|
||
|
||
// 将 refresh_token 加入黑名单并删除
|
||
if (!empty($info['refresh_jti'])) {
|
||
$this->blacklistStorage->add($info['refresh_jti'], $blacklistTtl);
|
||
$this->tokenStorage->delete($info['refresh_jti']);
|
||
}
|
||
|
||
// 删除 access_token 存储
|
||
$this->tokenStorage->delete($jti);
|
||
$count++;
|
||
}
|
||
|
||
// 清理用户索引
|
||
$this->tokenStorage->deleteByUser($id, $clientType, $exceptJti);
|
||
|
||
return $count;
|
||
}
|
||
|
||
/**
|
||
* 获取用户的所有会话列表
|
||
*
|
||
* @param string $id
|
||
* @param string|null $clientType
|
||
*
|
||
* @return array
|
||
*/
|
||
public function getSessions(string $id, ?string $clientType = null): array
|
||
{
|
||
// 使用 tokenStorage 获取用户的所有 token 信息
|
||
$tokens = $this->tokenStorage->getUserTokens($id, $clientType);
|
||
|
||
if (empty($tokens)) {
|
||
return [];
|
||
}
|
||
|
||
$sessions = [];
|
||
foreach ($tokens as $jti => $info) {
|
||
// 获取完整的 token 数据
|
||
$tokenData = $this->tokenStorage->get($jti);
|
||
if ($tokenData !== null) {
|
||
$sessions[] = [
|
||
'jti' => $jti,
|
||
'client_type' => $info['client_type'] ?? $tokenData['client_type'] ?? '',
|
||
'created_at' => $info['created_at'] ?? $tokenData['created_at'] ?? 0,
|
||
'extra' => $tokenData['extra'] ?? [],
|
||
];
|
||
}
|
||
}
|
||
|
||
return $sessions;
|
||
}
|
||
|
||
/**
|
||
* 获取所有在线会话列表
|
||
*
|
||
* @param string|null $clientType
|
||
*
|
||
* @return array
|
||
*/
|
||
public function getAllSessions(?string $clientType = null): array
|
||
{
|
||
$tokens = $this->tokenStorage->getAllTokens($clientType);
|
||
|
||
return array_values($tokens);
|
||
}
|
||
|
||
/**
|
||
* 刷新当前 Token 的最后活跃时间
|
||
*
|
||
* @return bool
|
||
*/
|
||
public function touchCurrentToken(): bool
|
||
{
|
||
$payload = $this->getPayloadFromRequest();
|
||
$jti = $payload['jti'] ?? '';
|
||
if (empty($jti)) {
|
||
return false;
|
||
}
|
||
|
||
return $this->tokenStorage->touch($jti);
|
||
}
|
||
|
||
/**
|
||
* 通过 JTI 踢出指定会话
|
||
*
|
||
* @param string $jti
|
||
*
|
||
* @return bool
|
||
*/
|
||
public function kickoutByJti(string $jti): bool
|
||
{
|
||
// 获取 token 信息
|
||
$tokenData = $this->tokenStorage->get($jti);
|
||
|
||
if ($tokenData === null) {
|
||
return $this->tokenStorage->markOffline($jti);
|
||
}
|
||
|
||
// 将 access_token 加入黑名单
|
||
$blacklistTtl = $this->config['ttl']['refresh'] ?? 604800;
|
||
$this->blacklistStorage->add($jti, $blacklistTtl);
|
||
|
||
// 将 refresh_token 也加入黑名单
|
||
$refreshJti = $tokenData['refresh_jti'] ?? '';
|
||
if (!empty($refreshJti)) {
|
||
$this->blacklistStorage->add($refreshJti, $blacklistTtl);
|
||
}
|
||
|
||
// 标记会话离线,保留监控列表记录
|
||
return $this->tokenStorage->markOffline($jti);
|
||
}
|
||
|
||
/**
|
||
* 获取登录设备数
|
||
*
|
||
* @param string $id
|
||
* @param string|null $clientType
|
||
*
|
||
* @return int
|
||
*/
|
||
public function deviceCount(string $id, ?string $clientType = null): int
|
||
{
|
||
return $this->tokenStorage->countByUser($id, $clientType);
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否在黑名单
|
||
*
|
||
* @param string $id
|
||
*
|
||
* @return bool
|
||
*/
|
||
public function isUserBlacklisted(string $id): bool
|
||
{
|
||
return $this->blacklistStorage->hasUser($id);
|
||
}
|
||
|
||
/**
|
||
* 将用户加入黑名单
|
||
*
|
||
* @param string $id
|
||
* @param int|null $ttl 有效期(秒)
|
||
*
|
||
* @return bool
|
||
*/
|
||
public function blacklistUser(string $id, ?int $ttl = null): bool
|
||
{
|
||
$ttl = $ttl ?? ($this->config['ttl']['refresh'] ?? 604800);
|
||
|
||
// 退出用户所有设备
|
||
$this->logoutAll($id);
|
||
|
||
// 加入黑名单
|
||
return $this->blacklistStorage->addUser($id, $ttl);
|
||
}
|
||
|
||
/**
|
||
* 撤销 Token
|
||
*
|
||
* @param string $token
|
||
*
|
||
* @return bool
|
||
*/
|
||
public function revoke(string $token): bool
|
||
{
|
||
try {
|
||
$payload = $this->parseToken($token);
|
||
$jti = $payload['jti'] ?? '';
|
||
|
||
if (!empty($jti)) {
|
||
$this->blacklistStorage->add($jti, $this->config['ttl']['access'] ?? 7200);
|
||
$this->tokenStorage->delete($jti);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
} catch (\Throwable $e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取 Token 状态
|
||
*
|
||
* @param string $token
|
||
*
|
||
* @return TokenStatus
|
||
*/
|
||
public function getTokenStatus(string $token): TokenStatus
|
||
{
|
||
try {
|
||
$payload = $this->parseToken($token);
|
||
$jti = $payload['jti'] ?? '';
|
||
|
||
// 检查是否在黑名单
|
||
if (!empty($jti) && $this->blacklistStorage->has($jti)) {
|
||
return TokenStatus::REVOKED;
|
||
}
|
||
|
||
// 检查是否过期
|
||
$exp = $payload['exp'] ?? 0;
|
||
if ($exp > 0 && $exp < time()) {
|
||
return TokenStatus::EXPIRED;
|
||
}
|
||
|
||
return TokenStatus::ACTIVE;
|
||
} catch (ExpiredException $e) {
|
||
return TokenStatus::EXPIRED;
|
||
} catch (\Throwable $e) {
|
||
return TokenStatus::REVOKED;
|
||
}
|
||
}
|
||
|
||
// ==================== 私有方法 ====================
|
||
|
||
/**
|
||
* 处理登录模式
|
||
*/
|
||
protected function handleLoginMode(string $id, string $clientType, string $loginMode): void
|
||
{
|
||
switch ($loginMode) {
|
||
case LoginMode::SINGLE->value:
|
||
// 单端登录:踢掉所有现有设备
|
||
$this->logoutAll($id);
|
||
break;
|
||
|
||
case LoginMode::CLIENT->value:
|
||
// 客户端模式:踢掉同类型设备的登录
|
||
$this->logoutAll($id, $clientType);
|
||
break;
|
||
|
||
case LoginMode::MULTI->value:
|
||
// 多端登录:检查最大设备数
|
||
$maxDevices = $this->config['security']['max_devices'] ?? 0;
|
||
if ($maxDevices > 0) {
|
||
$count = $this->tokenStorage->countByUser($id);
|
||
if ($count >= $maxDevices) {
|
||
// 删除最早的设备
|
||
$this->tokenStorage->deleteOldest($id);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通过 refresh_jti 反查 access token 会话
|
||
*/
|
||
protected function findAccessTokenByRefreshJti(string $id, string $clientType, string $refreshJti): array
|
||
{
|
||
$tokens = $this->tokenStorage->getUserTokens($id, $clientType);
|
||
foreach ($tokens as $accessJti => $info) {
|
||
if (($info['refresh_jti'] ?? '') !== $refreshJti) {
|
||
continue;
|
||
}
|
||
|
||
$tokenInfo = $this->tokenStorage->get($accessJti);
|
||
if ($tokenInfo !== null) {
|
||
return [$accessJti, $tokenInfo];
|
||
}
|
||
}
|
||
|
||
return ['', null];
|
||
}
|
||
|
||
/**
|
||
* 构建 Token 载荷
|
||
*/
|
||
protected function buildPayload(
|
||
string $id,
|
||
string $client,
|
||
string $jti,
|
||
TokenType $type,
|
||
array $extra = []
|
||
): array
|
||
{
|
||
$now = time();
|
||
$ttl = match ($type) {
|
||
TokenType::ACCESS => $this->config['ttl']['access'] ?? 7200,
|
||
TokenType::REFRESH => $this->config['ttl']['refresh'] ?? 604800,
|
||
};
|
||
|
||
$payload = [
|
||
'iss' => $this->config['issuer'] ?? 'webman',
|
||
'id' => $id,
|
||
'client' => $client,
|
||
'jti' => $jti,
|
||
'type' => $type->value,
|
||
'iat' => $now,
|
||
'exp' => $now + $ttl,
|
||
];
|
||
|
||
if (!empty($extra)) {
|
||
$payload['extra'] = $extra;
|
||
}
|
||
|
||
return $payload;
|
||
}
|
||
|
||
/**
|
||
* 生成 JWT Token
|
||
*/
|
||
protected function makeToken(array $payload): string
|
||
{
|
||
return JWT::encode($payload, $this->secret, $this->algo);
|
||
}
|
||
|
||
/**
|
||
* 解析 Token
|
||
*/
|
||
protected function parseToken(string $token): array
|
||
{
|
||
// 处理带有前缀的 token
|
||
if (str_starts_with($token, 'Bearer ')) {
|
||
$token = substr($token, 7);
|
||
}
|
||
return (array)JWT::decode($token, new Key($this->secret, $this->algo));
|
||
}
|
||
|
||
/**
|
||
* 生成 JTI
|
||
*/
|
||
protected function generateJti(): string
|
||
{
|
||
return bin2hex(random_bytes(16));
|
||
}
|
||
|
||
/**
|
||
* 从请求中获取 Access Token
|
||
*/
|
||
protected function getAccessTokenFromRequest(): ?string
|
||
{
|
||
$auth = request()->header('Authorization', '');
|
||
if (str_starts_with($auth, 'Bearer ')) {
|
||
return substr($auth, 7);
|
||
}
|
||
return request()->input('token');
|
||
}
|
||
|
||
/**
|
||
* 从请求中获取 Refresh Token
|
||
*/
|
||
protected function getRefreshTokenFromRequest(): ?string
|
||
{
|
||
// 先从 Authorization 头中获取
|
||
$tokenName = config('core.jwt.app.token_name', 'Authorization');
|
||
$authorization = request()->header($tokenName);
|
||
if (!empty($authorization)) {
|
||
if (str_starts_with($authorization, 'Bearer ')) {
|
||
$token = substr($authorization, 7);
|
||
if (!empty($token)) {
|
||
return $token;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 再从请求输入中获取
|
||
return request()->input('refresh_token');
|
||
}
|
||
|
||
/**
|
||
* 从请求中获取 Token 载荷
|
||
*/
|
||
public function getPayloadFromRequest(): array
|
||
{
|
||
if ($this->payloadCache !== null) {
|
||
return $this->payloadCache;
|
||
}
|
||
|
||
try {
|
||
$token = $this->getAccessTokenFromRequest();
|
||
if (empty($token)) {
|
||
return [];
|
||
}
|
||
|
||
$this->payloadCache = $this->parseToken($token);
|
||
return $this->payloadCache;
|
||
} catch (\Throwable $e) {
|
||
return [];
|
||
}
|
||
}
|
||
}
|