如何用LexikJWTAuthenticationBundle生成短生命周期JWT令牌(Symfony5)
生成短生命周期一次性JWT令牌(Symfony 5 + LexikJWTAuthenticationBundle)
核心思路
利用LexikJWT的JWTManagerInterface->create()方法支持自定义payload的特性,手动设置exp(过期时间)字段生成短生命周期令牌;额外添加jti(令牌唯一标识)并配合缓存验证,实现真正的一次性访问。
步骤1:编写短令牌生成服务
创建专门服务,负责生成带自定义过期时间和唯一标识的一次性令牌:
// src/Service/OneTimeJwtGenerator.php namespace App\Service; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTManagerInterface; use Symfony\Component\Security\Core\User\UserInterface; class OneTimeJwtGenerator { private $jwtManager; public function __construct(JWTManagerInterface $jwtManager) { $this->jwtManager = $jwtManager; } public function generate(UserInterface $user): string { // 设置2秒后过期 $expiresAt = new \DateTimeImmutable('+2 seconds'); // 自定义payload:覆盖exp,添加唯一jti $customPayload = [ 'exp' => $expiresAt->getTimestamp(), 'jti' => bin2hex(random_bytes(16)) // 生成随机唯一标识 ]; // 传入用户和自定义payload生成令牌 return $this->jwtManager->create($user, $customPayload); } }
步骤2:添加一次性令牌验证逻辑
为确保令牌只能使用一次,在令牌验证通过后检查并作废jti标识,这里用Redis缓存存储有效jti,过期时间与令牌一致:
2.1 创建事件监听器
// src/EventListener/OneTimeTokenValidator.php namespace App\EventListener; use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent; use Symfony\Component\Cache\Adapter\RedisAdapter; class OneTimeTokenValidator { private $redisCache; public function __construct(RedisAdapter $redisCache) { $this->redisCache = $redisCache; } public function onJWTDecoded(JWTDecodedEvent $event) { $payload = $event->getPayload(); // 只处理带有jti的一次性令牌 if (!isset($payload['jti'])) { return; } $jti = $payload['jti']; $cacheItem = $this->redisCache->getItem($jti); // 缓存不存在=令牌已使用/过期,标记为无效 if (!$cacheItem->isHit()) { $event->markAsInvalid(); return; } // 令牌验证通过后,删除缓存标记为已使用 $this->redisCache->deleteItem($jti); } }
2.2 配置监听器和缓存
在config/services.yaml中注册监听器并配置Redis缓存:
services: # 注册事件监听器 App\EventListener\OneTimeTokenValidator: tags: - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_valid, method: onJWTDecoded } arguments: $redisCache: '@Symfony\Component\Cache\Adapter\RedisAdapter' # 配置Redis缓存 Symfony\Component\Cache\Adapter\RedisAdapter: arguments: - '%env(REDIS_URL)%'
步骤3:在控制器中生成并使用令牌
在生成文件访问URL的控制器中调用服务生成令牌,拼接到URL参数:
// src/Controller/FileController.php namespace App\Controller; use App\Service\OneTimeJwtGenerator; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\User\UserInterface; class FileController { /** * @Route("/generate-file-url", name="generate_file_url") */ public function generateUrl(OneTimeJwtGenerator $generator, UserInterface $user): Response { $oneTimeToken = $generator->generate($user); // 拼接文件下载URL,携带令牌参数 $downloadUrl = $this->generateUrl('download_file', [ 'token' => $oneTimeToken, 'filename' => 'sample.pdf' ]); return new Response($downloadUrl); } /** * @Route("/download/{filename}", name="download_file") */ public function download(string $filename, \Symfony\Component\HttpFoundation\Request $request, \Symfony\Component\Security\Core\Security $security): Response { $token = $request->query->get('token'); if (!$token) { return new Response('缺少访问令牌', Response::HTTP_UNAUTHORIZED); } try { // 手动验证JWT令牌 $security->authenticate(new \Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\JWTUserToken($token)); // 验证通过,返回文件(示例逻辑) $filePath = \sprintf('%s/%s', $_SERVER['DOCUMENT_ROOT'] . '/private-files', $filename); if (!file_exists($filePath)) { return new Response('文件不存在', Response::HTTP_NOT_FOUND); } return new \Symfony\Component\HttpFoundation\BinaryFileResponse($filePath); } catch (\Exception $e) { return new Response('令牌无效或已过期', Response::HTTP_UNAUTHORIZED); } } }
关键说明
- 自定义
exp字段会覆盖LexikJWT的全局token_ttl配置,不影响登录令牌的生命周期。 - 添加
jti并配合缓存验证,确保令牌只能被使用一次,即使在2秒有效期内刷新页面也会失效。 - 若不需要严格的一次性,仅短生命周期即可,可以省略
jti和缓存验证部分。
内容的提问来源于stack exchange,提问作者NickHatBoecker
相关产品推荐
相关产品推荐

