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

如何改造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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 04:32:34