加载页面时 Firebase auth.currentUser 为空,当页面加载几毫秒后调用 authstatechange 时用户加载

IT技术 javascript reactjs firebase firebase-authentication
2021-04-05 06:41:14

我正在使用 React 和 Firebase 开发一个小型网络应用程序。为了处理身份验证,我使用了上下文 API,并在上下文中添加了登录用户的详细信息。

AuthProvider.tsx

const AuthProvider: React.FC = props => {
  const [state, setState] = useState<IAuthContext>(authInitialState);
  console.log("Inside AuthProvider");
  useEffect(() => {
    auth.onAuthStateChanged( user => {
      console.log("Auth State changed and user is ----->", user);
      if (user) {
        console.log("User value updated to the context")
        setState({
            ...authInitialState,
          isAuthenticated:!!user,
          permissions:[],
          user:user
        });
      }
      const stateChange = {
        ...authInitialState,
        isAuthenticated: !!user,
        user
      };
      // if (!user) {
        return setState(stateChange);
      // }
    });
  }, []);
  console.log("Rendering AuthProvider", state);
  return (
    <AuthContext.Provider value={state}>{props.children}</AuthContext.Provider>
  );
};
export default AuthProvider;

AuthConsumer.tsx

const withAuthContext = (
  Component: React.ComponentClass<any> | React.FunctionComponent<any>
) => {
  console.log("Rendering AuthConsumer for ");
  return (props: any) => (
    <AuthContext.Consumer>
      {context => <Component {...props} context={context} />}
    </AuthContext.Consumer>
  );
};

export default withAuthContext;

PrivateRoute.tsx

interface PrivateRouteProps extends RouteProps {
    // tslint:disable-next-line:no-any
    component: any;
    context: IAuthContext;
}

const PrivateRoute = (props: PrivateRouteProps) => {
    const { component: Component, context: IAuthContext, ...rest } = props;
    console.log("Private route for ", props);
    return (
        <Route
            {...rest}
            render={(routeProps) =>
                props.context.isAuthenticated ? (
                    <Component {...routeProps} />
                ) : (
                    <Redirect
                        to={{
                            pathname: '/login',
                            state: { from: routeProps.location }
                        }}
                    />
                )
            }
        />
    );
};

export default withAuthContext(PrivateRoute);

应用程序.tsx

return (
        <BrowserRouter>
            <div>
                <Switch>
                    <PublicRoute path="/frame" component={Frame} exact isAuthorized={true}/>
                    <Route path="/login" component={NewLogin} exact isAuthorized={true}/>
                    <PrivateRoute path="/nav" component={NavigationBar} exact/>
                    <PrivateRoute path="/dashboard" component={AnalyticsDashBoard} exact/>
                    <PrivateRoute path="/subscription" component={OrderSuccess} exact/>
                    <PrivateRoute path="/onboarding" component={OnBoarding} exact/>
                </Switch>
            </div>
        </BrowserRouter>
    );

用户已登录,会话持久性设置为本地。问题是,当我尝试 localhost/subscription,这是一个私有路由时,context.isAuthenticated 是假的,因为“onAuthStateChanged”观察者还没有被触发,所以它会进入登录页面,但在几毫秒内,authStateChange 被触发并设置了上下文,但它没有用,因为应用程序已经导航到登录,因为 privateroute 认为用户没有登录。我想了解如何克服这个问题。

2个回答

当页面加载时,Firebase 从本地存储恢复用户的凭据,并与服务器检查它们是否仍然有效。由于这是对服务器的调用,因此可能需要一些时间并且异步发生。这是正常的原因firebase.auth().currentUsernull,直到onAuthStateChanged火灾。

您遇到的问题是有多种原因firebase.auth().currentUser可能是null

  1. firebase.auth().currentUsernull因为客户端刚刚加载并且仍在与服务器检查凭据。
  2. firebase.auth().currentUsernull因为客户端根据服务器检查了凭据并且客户端未登录。
  3. firebase.auth().currentUsernull因为用户从未登录,所以没有要检查的凭据。

您想在情况 2 和 3 中导航,但不想在情况 1 中导航。

典型的解决方案是在第一次onAuthStateChanged触发之前不处理导航此时,您可以确定已针对服务器检查了凭据,或者没有要检查的凭据,在这两种情况下,您都需要导航到登录页面。


另一个加快速度的方法是在用户第一次登录时自己在本地存储中存储一个小令牌,然后在应用加载时读取它。如果令牌存在,您可以区分情况 1 和 3,并使用它来更快地导航到正确的页面。

有关此示例,请参阅有关构建移动 Web 应用程序的I/O 讨论

如果你想等待onAuthStateChanged()触发,你可以使用异步/等待的Promise:

/** Waits for auth state to init on navigation or refresh, otherwise resolves immediately */
private async waitForAuthInit() {
  let unsubscribe: Promise<firebase.default.Unsubscribe>;
  await new Promise<void>((resolve) => {
    unsubscribe = this.auth.onAuthStateChanged((_) => resolve());
  });
  (await unsubscribe!)();
}

在查询用户之前,我等待此功能:

async getCurrentUser(): Promise<firebase.default.User | null> {
    try {
      await this.waitForAuthInit();
      return await this.auth.currentUser;
    } catch (err: any) {
      console.log('Failed to get current user...', err);
      return null;
    }
  }

现在可以安全地在页面加载时查询用户,无需使用回调函数、可观察对象、订阅等,也无需onAuthStateChanged()再次调用

我应该注意到我使用的是 Angular 13 和 @angular/fire 依赖项,它是一个瘦包装库。其他框架中的语法可能有所不同。

这是我发现将 firestore 依赖项包含到单个服务的唯一方法,而不是onAuthStateChanged()到处调用

它对 Angular AuthGuards 特别有用,您可以getCurrentUser()在最初几毫秒内调用而不必担心误报。

如果 firebase 在他们这边实施这样的东西会很好,这样我们打电话时就不会得到漏报auth.currentUser,但这不关我的事......

这取决于你用它做什么,我的用例是检查页面是否应该首先加载。换句话说,如果您在页面加载之前不需要用户,那么在页面加载之前不要等待此方法。
2021-06-02 06:41:14
应用它是因为它看起来像一个方便的方法,但我想知道它是否对页面加载时间有任何有意义的影响。
2021-06-14 06:41:14
为了提高性能,您始终可以像上面建议的 Frank 那样向 localStorage 添加令牌。本质上,您正在尝试预测 firebase 的响应。
2021-06-19 06:41:14