You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 00:37:07