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

Symfony JWT过期刷新:React前端如何更新Authorization头令牌?

JWT刷新后前端适配方案(React)

核心结论

必须前端处理后端返回的新JWT——后端修改当前请求的Authorization头仅对本次请求的后端内部流程生效,无法同步到前端的请求上下文。前端需要主动获取新JWT并更新本地存储的令牌,才能让后续请求使用新令牌。

后端逻辑调整建议

先修改后端代码,放弃修改当前请求头的思路,改为在JWT过期且refresh token验证通过时:

  1. 生成新JWT和刷新后的refresh token(推荐滚动刷新,避免旧refresh token被滥用)
  2. 将新JWT放在响应体中返回,同时更新HTTPOnly的refresh token Cookie
  3. 根据业务场景,要么继续处理当前请求,要么返回专门的刷新响应

调整后的关键代码示例:

public function onKernelRequest(GetResponseEvent $event)
{
    $request = $event->getRequest();
    $response = null;

    if ($request->headers->has('Authorization')) {
        $authorizationHeader = $request->headers->get('Authorization');
        if (strpos($authorizationHeader, 'Bearer ') === 0) {
            try {
                $this->validateJWT($request, $response);
                if ($response) {
                    $event->setResponse($response);
                }
            } catch (AuthenticationException $e) {
                $event->setResponse(new JsonResponse(['error' => $e->getMessage()], Response::HTTP_UNAUTHORIZED));
            }
        }
    }
}

public function validateJWT(Request $request, &$response): void
{
    $credentials = str_replace('Bearer ', '', $request->headers->get('Authorization'));

    $isSignatureValid = $this->JWTAuthService->validateSignature($credentials);
    if (!$isSignatureValid['success']) throw new AuthenticationException($isSignatureValid['message']);

    $isActive = $this->JWTAuthService->isJWTTokenActive($credentials, $request->cookies->get('refresh_token'));
    
    // 处理JWT过期但refresh token有效的场景
    if (!$isActive['success'] && $isActive['error']['code'] === 'JWT_EXPIRED' && $isActive['data']['refresh_token_valid']) {
        $newTokenData = $this->JWTAuthService->generateNewTokenFromRefresh($request->cookies->get('refresh_token'));
        // 更新HTTPOnly refresh token Cookie
        setcookie('refresh_token', $newTokenData['refresh_token'], time() + 3600 * 24 * 7, '/', null, false, true);
        // 将新JWT通过响应体返回给前端
        $response = new JsonResponse([
            'success' => true,
            'new_token' => $newTokenData['token'],
            'user_id' => $newTokenData['user_id']
        ], Response::HTTP_OK);
        return;
    }

    if (!$isActive['success']) throw new AuthenticationException($isActive['error']['message']);

    // JWT正常有效,设置用户属性
    $user = $this->em->getRepository(User::class)->find($isActive['data']['user_id']);
    $request->attributes->set('user', $user->getId());
}

React前端具体实现步骤

1. 本地存储JWT

登录/注册成功后,将后端返回的JWT存储在localStorage或sessionStorage中(不要存在Cookie,因为需要前端主动携带到请求头):

const handleLogin = async (credentials) => {
  const res = await fetch('/api/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(credentials)
  });
  const data = await res.json();
  if (data.success) {
    localStorage.setItem('access_token', data.token);
    // refresh token由后端设置HTTPOnly Cookie,前端无需读取或存储
  }
};

2. 封装带认证的请求拦截器

推荐用Axios封装请求,统一处理令牌携带和刷新逻辑,也可以用原生Fetch实现:

方案一:Axios拦截器(推荐)

import axios from 'axios';

const api = axios.create({
  baseURL: '/api',
  withCredentials: true // 必须开启,让浏览器自动携带HTTPOnly的refresh token Cookie
});

// 请求拦截:自动添加Authorization头
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('access_token');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// 响应拦截:处理令牌刷新
api.interceptors.response.use(
  (response) => {
    // 如果后端在正常响应中携带新令牌,更新本地存储
    if (response.data.new_token) {
      localStorage.setItem('access_token', response.data.new_token);
    }
    return response;
  },
  async (error) => {
    const originalRequest = error.config;
    // 避免重复刷新,标记已重试的请求
    if (error.response.status === 401 && error.response.data.error === 'JWT_EXPIRED' && !originalRequest._retry) {
      originalRequest._retry = true;
      try {
        // 调用后端刷新令牌接口
        const refreshRes = await axios.post('/api/refresh-token', {}, { withCredentials: true });
        if (refreshRes.data.success) {
          const newToken = refreshRes.data.new_token;
          localStorage.setItem('access_token', newToken);
          // 更新原请求的Authorization头并重发
          originalRequest.headers.Authorization = `Bearer ${newToken}`;
          return api(originalRequest);
        }
      } catch (refreshError) {
        // 刷新失败,清除本地令牌并跳转登录页
        localStorage.removeItem('access_token');
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    return Promise.reject(error);
  }
);

export default api;

方案二:原生Fetch封装

async function fetchWithAuth(url, options = {}) {
  const token = localStorage.getItem('access_token');
  const headers = {
    ...options.headers,
    ...(token ? { Authorization: `Bearer ${token}` } : {})
  };
  const config = {
    ...options,
    headers,
    credentials: 'include' // 携带HTTPOnly Cookie
  };

  let response = await fetch(url, config);

  // 检测到JWT过期时刷新令牌
  if (response.status === 401 && (await response.json()).error === 'JWT_EXPIRED') {
    const refreshRes = await fetch('/api/refresh-token', {
      method: 'POST',
      credentials: 'include'
    });
    const refreshData = await refreshRes.json();
    if (refreshData.success) {
      const newToken = refreshData.new_token;
      localStorage.setItem('access_token', newToken);
      // 更新请求头并重发
      config.headers.Authorization = `Bearer ${newToken}`;
      response = await fetch(url, config);
    } else {
      localStorage.removeItem('access_token');
      window.location.href = '/login';
      throw new Error('Refresh token failed');
    }
  }

  return response;
}

3. 页面初始化时验证令牌有效性

在App根组件初始化时,检查本地令牌是否有效,避免无效令牌残留:

import { useEffect } from 'react';
import api from './api';

function App() {
  useEffect(() => {
    const checkTokenValidity = async () => {
      const token = localStorage.getItem('access_token');
      if (token) {
        try {
          await api.get('/api/validate-token');
        } catch (error) {
          localStorage.removeItem('access_token');
          window.location.href = '/login';
        }
      }
    };
    checkTokenValidity();
  }, []);

  return (
    {/* 应用组件内容 */}
  );
}

关键注意事项

  • HTTPOnly Cookie安全:refresh token存在HTTPOnly Cookie中,前端无法读取,可避免XSS攻击,浏览器会自动在请求时携带。
  • 滚动刷新策略:每次刷新JWT时,后端应生成新的refresh token并更新Cookie,降低旧令牌被滥用的风险。
  • 避免循环刷新:在拦截器中标记已重试的请求,防止同一请求多次触发刷新逻辑。
  • 异常处理:当refresh token也过期时,前端需清除本地令牌并引导用户重新登录。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 21:59:54