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 []; } } }