React+Keycloak架构下,iframe内Axios遇401如何请求父组件刷新Token并重试?
解决方案
当然可以实现,核心思路是利用postMessage在iframe子页面和父组件间通信,配合Axios拦截器完成token刷新与请求重发,具体实现步骤如下:
1. 父组件(App.tsx)配置消息监听与token刷新逻辑
父组件需要监听来自可信iframe的token刷新请求,调用Keycloak的刷新方法后将新token返回给子页面:
// App.tsx import { useEffect } from 'react'; import keycloak from './keycloak'; // 你的Keycloak实例 useEffect(() => { const handleMessage = async (event: MessageEvent) => { // 验证消息来源,仅处理可信域名的请求 if (event.origin !== process.env.REACT_APP_IFRAME_DOMAIN) return; if (event.data.type === 'REFRESH_TOKEN') { try { // 调用Keycloak刷新token,5秒内过期则触发刷新 await keycloak.updateToken(5); const newTokens = { accessToken: keycloak.token, refreshToken: keycloak.refreshToken, expiresIn: keycloak.tokenParsed?.exp }; // 向子页面发送新token event.source?.postMessage( { type: 'TOKEN_REFRESHED', payload: newTokens }, event.origin ); } catch (error) { // 刷新失败,通知子页面跳转登录 event.source?.postMessage( { type: 'TOKEN_REFRESH_FAILED' }, event.origin ); } } }; window.addEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage); }, []);
2. 子页面配置Axios拦截器处理401与token刷新
子页面的Axios拦截器在捕获401响应后,向父组件发送刷新请求,等待新token返回后重发原请求,同时添加锁机制避免重复刷新:
// 子页面的Axios配置文件 import axios from 'axios'; let isRefreshing = false; let failedRequestsQueue: { resolve: (token: string) => void; reject: (error: any) => void }[] = []; const api = axios.create({ baseURL: process.env.REACT_APP_API_URL }); // 请求拦截器:自动添加token到请求头 api.interceptors.request.use(config => { const accessToken = localStorage.getItem('accessToken'); // 子页面存储的token if (accessToken) { config.headers.Authorization = `Bearer ${accessToken}`; } return config; }); // 响应拦截器:处理401与token刷新 api.interceptors.response.use( response => response, async error => { const originalRequest = error.config; // 仅处理401且未重试过的请求 if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; if (isRefreshing) { // 已有刷新请求在处理,将当前请求加入队列等待 return new Promise((resolve, reject) => { failedRequestsQueue.push({ resolve, reject }); }).then(token => { originalRequest.headers.Authorization = `Bearer ${token}`; return axios(originalRequest); }).catch(err => Promise.reject(err)); } isRefreshing = true; // 向父组件发送刷新请求 return new Promise((resolve, reject) => { window.parent.postMessage( { type: 'REFRESH_TOKEN' }, process.env.REACT_APP_PARENT_DOMAIN ); // 监听父组件返回的新token const handleTokenResponse = (event: MessageEvent) => { if (event.origin !== process.env.REACT_APP_PARENT_DOMAIN) return; if (event.data.type === 'TOKEN_REFRESHED') { const { accessToken } = event.data.payload; // 更新子页面存储的token localStorage.setItem('accessToken', accessToken); // 更新当前请求的token并重新发起 originalRequest.headers.Authorization = `Bearer ${accessToken}`; resolve(api(originalRequest)); // 处理队列中等待的所有请求 failedRequestsQueue.forEach(promise => promise.resolve(accessToken)); failedRequestsQueue = []; } else if (event.data.type === 'TOKEN_REFRESH_FAILED') { // 刷新失败,抛出错误并清空队列 reject(new Error('Token refresh failed, please login again')); failedRequestsQueue.forEach(promise => promise.reject(error)); failedRequestsQueue = []; } // 移除监听并释放锁 window.removeEventListener('message', handleTokenResponse); isRefreshing = false; }; window.addEventListener('message', handleTokenResponse); }); } return Promise.reject(error); } ); export default api;
3. 关键注意事项
- 域名验证:必须在
postMessage和消息监听中验证event.origin,防止跨域恶意攻击,确保仅处理可信域名的通信。 - 并发请求处理:通过
isRefreshing锁和请求队列,避免多个401请求同时触发父组件的token刷新,提升性能并防止重复操作。 - 刷新失败处理:若Keycloak刷新token失败(比如refresh token也过期),需通知子页面跳转登录,或由父组件统一处理登录逻辑。
- token存储:子页面可将从父组件获取的token存在
localStorage或内存中,请求时自动携带。
内容的提问来源于stack exchange,提问作者yjlee
相关产品推荐
相关产品推荐

