Symfony JWT过期刷新:React前端如何更新Authorization头令牌?
JWT刷新后前端适配方案(React)
核心结论
必须前端处理后端返回的新JWT——后端修改当前请求的Authorization头仅对本次请求的后端内部流程生效,无法同步到前端的请求上下文。前端需要主动获取新JWT并更新本地存储的令牌,才能让后续请求使用新令牌。
后端逻辑调整建议
先修改后端代码,放弃修改当前请求头的思路,改为在JWT过期且refresh token验证通过时:
- 生成新JWT和刷新后的refresh token(推荐滚动刷新,避免旧refresh token被滥用)
- 将新JWT放在响应体中返回,同时更新HTTPOnly的refresh token Cookie
- 根据业务场景,要么继续处理当前请求,要么返回专门的刷新响应
调整后的关键代码示例:
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
相关产品推荐
相关产品推荐

