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

Symfony React应用中Cookie过期时自动刷新JWT的方案选择与实现方法

How to Auto-Refresh JWT Tokens When Cookies Expire in a Symfony + React App

Great question! Since you're storing JWTs in HTTP-only cookies (smart move for security), the approach you choose depends on whether you want the refresh logic handled server-side or client-side. Let's break down both options with concrete implementations tailored to your setup with lexik_jwt_authentication and gesdinet/jwt-refresh-token-bundle.

HTTP-only cookies can't be read by JavaScript, so frontend code can't directly access the JWT's expiration time. A server-side listener solves this by automatically checking token validity on every request and refreshing it when it's close to expiring. Here's how to set it up:

Step 1: Create the Event Listener

This listener will hook into the kernel.request event, check if the current JWT is nearing expiration, and use the refresh token to generate new credentials if needed.

// src/EventListener/JwtRefreshListener.php
namespace App\EventListener;

use Gesdinet\JWTRefreshTokenBundle\Generator\RefreshTokenGeneratorInterface;
use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpFoundation\Cookie;

class JwtRefreshListener
{
    private $tokenExtractor;
    private $jwtEncoder;
    private $refreshTokenManager;
    private $refreshTokenGenerator;
    private $jwtTtl;

    public function __construct(
        TokenExtractorInterface $tokenExtractor,
        JWTEncoderInterface $jwtEncoder,
        RefreshTokenManagerInterface $refreshTokenManager,
        RefreshTokenGeneratorInterface $refreshTokenGenerator,
        int $jwtTtl
    ) {
        $this->tokenExtractor = $tokenExtractor;
        $this->jwtEncoder = $jwtEncoder;
        $this->refreshTokenManager = $refreshTokenManager;
        $this->refreshTokenGenerator = $refreshTokenGenerator;
        $this->jwtTtl = $jwtTtl;
    }

    public function onKernelRequest(RequestEvent $event)
    {
        // Skip sub-requests (like Twig includes)
        if (!$event->isMainRequest()) {
            return;
        }

        $request = $event->getRequest();
        $token = $this->tokenExtractor->extract($request);

        if (!$token) {
            return;
        }

        try {
            $payload = $this->jwtEncoder->decode($token);
            $currentTime = time();
            $expirationTime = $payload['exp'];
            $timeUntilExpiry = $expirationTime - $currentTime;

            // Refresh if token expires in less than 5 minutes (adjust as needed)
            if ($timeUntilExpiry < 300) {
                $refreshTokenString = $request->cookies->get('REFRESH_TOKEN');
                if (!$refreshTokenString) {
                    return;
                }

                $refreshToken = $this->refreshTokenManager->getRefreshToken($refreshTokenString);
                if (!$refreshToken || !$refreshToken->isValid()) {
                    return; // Let Lexik's auth system handle invalid tokens
                }

                // Generate new JWT
                $user = $refreshToken->getUser();
                $newToken = $this->jwtEncoder->encode([
                    'username' => $user->getUserIdentifier(),
                    'exp' => $currentTime + $this->jwtTtl,
                    // Add any other required payload fields here
                ]);

                // Generate new refresh token (extend validity, e.g., 7 days)
                $newRefreshToken = $this->refreshTokenGenerator->createForUserWithTtl($user, $this->jwtTtl * 7);
                
                // Clean up old refresh token
                $this->refreshTokenManager->delete($refreshToken);
                $this->refreshTokenManager->save($newRefreshToken);

                // Update cookies with new tokens
                $response = $event->getResponse();
                if ($response) {
                    // Configure cookie settings to match your production requirements (Secure, SameSite, etc.)
                    $response->headers->setCookie(Cookie::create(
                        'JWT', // Match your JWT cookie name from Lexik config
                        $newToken,
                        $currentTime + $this->jwtTtl,
                        '/',
                        null,
                        true, // Enable Secure in production
                        true, // HTTP-only (critical for security)
                        false,
                        'strict'
                    ));

                    $response->headers->setCookie(Cookie::create(
                        'REFRESH_TOKEN',
                        $newRefreshToken->getRefreshToken(),
                        $currentTime + $this->jwtTtl * 7,
                        '/',
                        null,
                        true,
                        true,
                        false,
                        'strict'
                    ));
                }
            }
        } catch (\Exception $e) {
            // Ignore decoding errors (Lexik's auth system will handle invalid tokens)
            return;
        }
    }
}

Step 2: Register the Listener

Add this to your config/services.yaml to wire up the listener:

services:
    App\EventListener\JwtRefreshListener:
        arguments:
            $jwtTtl: '%lexik_jwt_authentication.token_ttl%'
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }

Option 2: Client-Side Refresh via React Timer

If you prefer frontend control, you can have your React app trigger a refresh request before the token expires. Since you can't read the HTTP-only cookie, you'll need to return the token's expiration time in your login response.

Step 1: Update the Login Endpoint

Modify your login controller to return the token's expiration timestamp:

// src/Controller/AuthController.php
public function login(AuthenticationUtils $utils, JWTEncoderInterface $jwtEncoder): Response
{
    // ... existing login logic ...
    $token = $jwtEncoder->encode([
        'username' => $user->getUserIdentifier(),
        'exp' => time() + $this->getParameter('lexik_jwt_authentication.token_ttl')
    ]);
    $expiresAt = time() + $this->getParameter('lexik_jwt_authentication.token_ttl');

    return $this->json([
        'success' => true,
        'expiresAt' => $expiresAt * 1000 // Convert to milliseconds for JS
    ]);
}

Step 2: Add Refresh Logic to React

Store the expiration time and set a timer to trigger a refresh before it expires:

// Example React auth logic
const handleLogin = async (email, password) => {
    try {
        const response = await fetch('/api/login', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ email, password })
        });

        if (response.ok) {
            const data = await response.json();
            // Store expiration time in localStorage (or memory)
            localStorage.setItem('jwtExpiresAt', data.expiresAt);
            // Schedule refresh 5 minutes before expiration
            scheduleTokenRefresh(data.expiresAt);
        }
    } catch (err) {
        console.error('Login failed:', err);
    }
};

const scheduleTokenRefresh = (expiresAt) => {
    const timeUntilRefresh = expiresAt - Date.now() - 5 * 60 * 1000; // 5 minutes before expiry
    if (timeUntilRefresh > 0) {
        setTimeout(refreshToken, timeUntilRefresh);
    }
};

const refreshToken = async () => {
    try {
        const response = await fetch('/api/refresh-token', { method: 'POST' });
        if (response.ok) {
            const data = await response.json();
            localStorage.setItem('jwtExpiresAt', data.expiresAt);
            // Reschedule next refresh
            scheduleTokenRefresh(data.expiresAt);
        } else {
            // Refresh failed, redirect to login
            localStorage.removeItem('jwtExpiresAt');
            window.location.href = '/login';
        }
    } catch (err) {
        console.error('Token refresh failed:', err);
        localStorage.removeItem('jwtExpiresAt');
        window.location.href = '/login';
    }
};

Step 3: Create the Refresh Endpoint

Add a controller action to handle refresh requests:

// src/Controller/AuthController.php
use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface;

#[Route('/api/refresh-token', name: 'api_refresh_token', methods: ['POST'])]
public function refreshToken(
    RefreshTokenManagerInterface $refreshTokenManager,
    JWTEncoderInterface $jwtEncoder
): Response {
    $refreshTokenString = $this->getRequest()->cookies->get('REFRESH_TOKEN');
    if (!$refreshTokenString) {
        return $this->json(['error' => 'Refresh token missing'], Response::HTTP_UNAUTHORIZED);
    }

    $refreshToken = $refreshTokenManager->getRefreshToken($refreshTokenString);
    if (!$refreshToken || !$refreshToken->isValid()) {
        return $this->json(['error' => 'Invalid refresh token'], Response::HTTP_UNAUTHORIZED);
    }

    $user = $refreshToken->getUser();
    $tokenTtl = $this->getParameter('lexik_jwt_authentication.token_ttl');
    $newToken = $jwtEncoder->encode([
        'username' => $user->getUserIdentifier(),
        'exp' => time() + $tokenTtl
    ]);

    // Generate new refresh token
    $newRefreshToken = $this->get('gesdinet.jwtrefreshtoken.generator')->createForUserWithTtl($user, $tokenTtl * 7);
    $refreshTokenManager->delete($refreshToken);
    $refreshTokenManager->save($newRefreshToken);

    // Update cookies
    $response = $this->json(['expiresAt' => (time() + $tokenTtl) * 1000]);
    $response->headers->setCookie(Cookie::create(
        'JWT',
        $newToken,
        time() + $tokenTtl,
        '/',
        null,
        true,
        true,
        false,
        'strict'
    ));
    $response->headers->setCookie(Cookie::create(
        'REFRESH_TOKEN',
        $newRefreshToken->getRefreshToken(),
        time() + $tokenTtl *7,
        '/',
        null,
        true,
        true,
        false,
        'strict'
    ));

    return $response;
}

Which Option Should You Choose?

  • Server-side listener: More reliable because it works regardless of frontend state (e.g., user switches tabs, browser suspends timers). It also keeps token logic centralized and avoids exposing expiration times to the frontend.
  • Client-side timer: Better if you need granular control over refresh timing (e.g., refresh only when the app is active). Just be aware that browser timers can be throttled in background tabs.

内容的提问来源于stack exchange,提问作者winterbbq

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 22:22:47