如何改造Auth0 Hook以适配SSR环境(基于React Router)
适配SSR环境改造Auth0 Hook方案
问题背景
基于Auth0官方@auth0/auth0-spa-js示例代码实现的Auth0重定向Hook,在客户端运行正常,但因直接使用window.history和window.location,无法在服务端渲染(SSR)环境下工作。现有react-router生成的history上下文,需借助useLocation、useHistory等钩子改写代码,让useAuth0适配SSR环境。
核心改造步骤
1. 改造Auth0Provider,接入react-router的history
修改Auth0Provider,支持传入history实例或从Router上下文自动获取,同时替换所有直接访问window的逻辑:
import { useHistory, useLocation } from 'react-router-dom'; // 用react-router的history替换window.history const DEFAULT_REDIRECT_CALLBACK = (history) => () => { history.replace({ pathname: history.location.pathname }); }; export const Auth0Context = createContext(); export const useAuth0 = () => useContext(Auth0Context); export const Auth0Provider = ({ children, onRedirectCallback, history: propHistory, ...initOptions }) => { // 优先使用传入的history,否则从Router上下文获取 const history = propHistory || useHistory(); const location = useLocation(); const [isAuthenticated, setIsAuthenticated] = useState(); const [user, setUser] = useState(); const [auth0Client, setAuth0] = useState(); const [loading, setLoading] = useState(true); const [popupOpen, setPopupOpen] = useState(false); // 绑定history到默认回调 const redirectCallback = onRedirectCallback || DEFAULT_REDIRECT_CALLBACK(history); useEffect(() => { const initAuth0 = async () => { // SSR环境下跳过Auth0客户端初始化 if (typeof window === 'undefined') { setLoading(false); return; } const auth0FromHook = await createAuth0Client({ ...initOptions, redirect_uri: initOptions.redirect_uri || window.location.origin }); setAuth0(auth0FromHook); // 用react-router的location替代window.location if (location.search.includes('code=') && location.search.includes('state=')) { const { appState } = await auth0FromHook.handleRedirectCallback(); redirectCallback(appState); } const isAuthenticated = await auth0FromHook.isAuthenticated(); setIsAuthenticated(isAuthenticated); if (isAuthenticated) { const user = await auth0FromHook.getUser(); setUser(user); // SSR环境下禁用localStorage,改用cookie存储 if (typeof window !== 'undefined') { localStorage.setItem('user', user.email); const token = await auth0FromHook.getTokenSilently(); localStorage.setItem('token', token); } } setLoading(false); }; initAuth0(); }, [history, location, redirectCallback, initOptions]); const loginWithPopup = async (params = {}) => { setPopupOpen(true); try { await auth0Client.loginWithPopup(params); } catch (error) { console.error(error); } finally { setPopupOpen(false); } const user = await auth0Client.getUser(); setUser(user); setIsAuthenticated(true); }; const handleRedirectCallback = async () => { setLoading(true); await auth0Client.handleRedirectCallback(); const user = await auth0Client.getUser(); setLoading(false); setIsAuthenticated(true); setUser(user); }; return ( <Auth0Context.Provider value={{ isAuthenticated, user, loading, popupOpen, loginWithPopup, handleRedirectCallback, getIdTokenClaims: (...p) => auth0Client?.getIdTokenClaims(...p), loginWithRedirect: (...p) => auth0Client?.loginWithRedirect(...p), getTokenSilently: (...p) => auth0Client?.getTokenSilently(...p), getTokenWithPopup: (...p) => auth0Client?.getTokenWithPopup(...p), logout: (...p) => auth0Client?.logout({ ...p, returnTo: p.returnTo || `${history.location.origin}${history.location.pathname}` }) }} > {children} </Auth0Context.Provider> ); };
2. 调整App组件的Redirect逻辑
替换依赖window.location的部分,改用useLocation:
import { useLocation, Redirect, Switch, Route } from 'react-router-dom'; const App = () => { const { loading } = useAuth0(); const location = useLocation(); if (loading) { return <div>Loading...</div>; } return ( <Switch> <Redirect from='/:url*(/+)' to={location.pathname.slice(0, -1) || '/'} /> <Route exact path='/' component={Deals} /> <PrivateRoute exact path='/draft' component={Draft} /> <PrivateRoute exact path='/cabinet' component={Cabinet} /> <PrivateRoute exact path='/profile' component={Profile} /> <PrivateRoute path='/deal/:id' exact component={(props) => <Draft {...props} />} /> </Switch> ); };
3. 修改应用入口的Auth0Provider配置
传入history实例,同时替换onRedirectCallback中的window.location:
const { store, history } = configureStore(); const MOUNT_NODE = document.getElementById('root'); const AUTH0_SCOPE = 'openid profile email'; const onRedirectCallback = (appState) => { history.push( appState && appState.targetUrl ? `${appState.targetUrl}${appState.qs || ''}` : history.location.pathname ); }; const Application = ( <Suspense fallback={<Loading />}> <CookiesProvider> <Provider store={store}> <ConnectedRouter history={history}> <Auth0Provider domain={process.env.REACT_APP_AUTH0_DOMAIN} audience={process.env.REACT_APP_AUTH0_AUDIENCE} scope={AUTH0_SCOPE} client_id={process.env.REACT_APP_AUTH0_CLIENT_ID} redirect_uri={typeof window !== 'undefined' ? window.location.origin : ''} onRedirectCallback={onRedirectCallback} history={history} > <Frontload noServerRender={true}> <App /> </Frontload> </Auth0Provider> </ConnectedRouter> </Provider> </CookiesProvider> </Suspense> );
关键注意事项
- SSR环境隔离:所有依赖
window、localStorage的逻辑必须加typeof window !== 'undefined'判断,避免服务端报错。 - 存储方案替换:SSR环境下
localStorage不可用,建议改用cookie存储用户凭证和信息,确保服务端能读取。 - Router上下文依赖:若未手动传入history,需保证
Auth0Provider放在Router组件内部,才能通过useHistory获取实例。 - Redirect URI处理:初始化Auth0客户端时,
redirect_uri需区分SSR和浏览器环境,服务端可传入空字符串或从请求头获取域名。
内容的提问来源于stack exchange,提问作者Igor Shmukler
相关产品推荐
相关产品推荐

