如何在React Router中实现登录后重定向到之前的页面?

11
我想在用户完成登录后重定向到他/她上次访问的页面,我正在使用React-Router。我该如何实现?
5个回答

9

您可以使用查询字符串来实现此操作。

query-string依赖项添加到您的react应用程序中。

每当用户尝试访问受保护的路径时,如果用户尚未登录,则必须将其重定向到登录屏幕。要做到这一点,您将执行以下操作。

history.push('/login');

但我们可以将上一个路径作为查询参数传递,方法如下。可以使用 react-router 中的 useLocation 获取之前的路径。

const prevLocation = useLocation();
history.push(`/login?redirectTo=${prevLocation}`);

现在,在成功登录后,我们可以获取之前路径传递的查询字符串。如果之前的路径为 null,我们可以设置一个默认路径。

const handleSuccessLogin = () => {
      const location = useLocation();
      const { redirectTo } = queryString.parse(location.search);
      history.push(redirectTo == null ? "/apps" : redirectTo);
 };

这在我看来是最好的解决方案。

1
在将重定向URL作为查询参数传递时,要注意无效的URL重定向攻击。更多信息请参见:https://www.virtuesecurity.com/kb/url-redirection-attack-and-defense/。 - Anis LOUNIS aka AnixPasBesoin
useLocation() 的返回值是一个 Location 对象,而不是字符串。 当你执行 history.push(/login?redirectTo=${prevLocation}); 时,你将一个 Location 对象编码为字符串。你正在推送 /login?redirectTo=[object Object] - Marco Castanho
如果/login进一步重定向到另一个URL(例如,在企业/SaaS场景中非常常见的IDP登录页面),则此方法无效。 - Eric Xin Zhang

4

@Burhan的解决方案在/login是一个简单的登录页面时效果很好,没有进一步的重定向。在我的情况下,登录页面只检查localStorage中的凭据,如果凭据不可用或过期,则进一步重定向到IdP登录页面。

我使用react-router-dom@6aws-amplify实现了联合登录/单点登录(Federated Login/SSO),以下代码片段演示了它的工作方式。注意,它们不完整或可运行,只是解释了它的工作方式。

  1. 在应用组件中使用PrivateRoute(有关详细信息,请参见react-router-dom@6文档)检查是否存在登录凭据(auth是通过单独的AuthContext提供的,这超出了本主题的范围):
const PrivateRoute: React.FC<{user: AuthenticatedUser}> = ({ user }) => {
  const location = useLocation();
  return user ? <Outlet /> : <Navigate to="/login" replace state={{ from: location }} />;
};

在 App 组件中应用 PrivateRoute/:
return (
      <Routes>
        <Route path="/" element={<PrivateRoute user={auth.user} />}>
          ... // Other routes
        </Route>
        <Route path="/login" element={<Login />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
);
  1. 在登录组件中,在调用aws-amplify开始联合登录流程之前,将原始URL存储到sessionStorage中:
  const login = async () => {
    sessionStorage.setItem('beforeLogin', location.state?.from?.pathname);
    await Auth.federatedSignIn();
  };
  1. 在联合登录的回调处理程序中,从 sessionStorage 获取位置状态和 URL,并在调用身份验证上下文后使用其中任何一个来设置已认证的用户:
    Auth.currentAuthenticatedUser()
      .then((currentUser) => {
        if (currentUser) {
          auth.setUser(currentUser);
          const beforeLoginUrl = sessionStorage.getItem('beforeLogin');
          sessionStorage.removeItem('beforeLogin');
          navigate(beforeLoginUrl ?? location.state?.from ?? '/default');
        }
      })
      .catch(console.error);

我需要检查两个方面,因为在我的情况下,如果登录组件找到有效的凭据,它根本不会重定向到IdP,因此sessionStorage中没有URL,只有location.state?.from


3

我修改了 @Bonttimo 的第一个解决方案,将整个 location 对象传递给状态,以便您可以保留原始 URL 中的搜索查询。

登录按钮:

import { Link, useLocation } from "react-router-dom";

export const Navigation: FC = () => {
  const location = useLocation();

  return (
    <Link to="/accounts/login" state={{ redirectTo: location }}>
      <Button text="Login" />
    </Link>
  )
}

如果您正在使用重定向到登录页面的受保护路由(基于),则情况相同。请注意保留HTML标签。
import React, { FC, useContext } from "react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { UserContext } from "../contexts/user.context";

type Props = {
  redirectPath?: string;
};

export const ProtectedRoute: FC<Props> = ({ redirectPath = "/accounts/login" }) => {
  const location = useLocation();
  const { currentUser } = useContext(UserContext);

  if (!currentUser) {
    return <Navigate to={redirectPath} replace state={{ redirectTo: location }} />;
  }

  return <Outlet />;
};

然后是登录页面:
type RedirectLocationState = {
  redirectTo: Location;
};

export const Login = () => {
  const navigate = useNavigate();
  const { state: locationState } = useLocation();

  const onLogin = () => {
    if (locationState) {
      // state is any by default
      const { redirectTo } = locationState as RedirectLocationState;
      navigate(`${redirectTo.pathname}${redirectTo.search}`);
    }
  }

  return (...)
}

这使您可以保留原始URL不变。

例如,有人向您发送了一个链接/users/list?country=13,但只有经过身份验证的用户才能查看用户列表页面。此解决方案将重定向您到登录页面,并在登录后将您带回/users/list?country=13


如果实施的话,这个解决方案真的很棒,我喜欢它。 - undefined

1

在React路由器v6中,有两种简单的方法可以实现这一点,一种是在链接状态中传递先前的位置,另一种是使用useNavigate钩子。

第一种解决方案(在链接状态中传递先前的位置)

import { NavLink, useLocation } from "react-router-dom";

export default function App() {
  const location = useLocation();
  return (
      <NavLink to="/login" state={{ prev: location.pathname }}>
        Login
      </NavLink>
  );
}

然后在登录组件中提取状态

import { useNavigate, useLocation } from "react-router-dom";
export default function Login() {
    const navigate = useNavigate();
    const { state } = useLocation();
    
    // Then when the user logs in you redirect them back with
    const onLogin = () => navigate(state);
    return (...)
};

第二种解决方案(仅使用 useNavigate 钩子)

import { useNavigate } from "react-router-dom";
export default function Login() {
    const navigate = useNavigate();

    // Then when the user logs in you redirect them back with the -1
    // means go one page back in the history. you can also pass -2...
    const onLogin = () => navigate(-1);
    return (...)
};

第二种解决方案非常容易出错,因为您假设要返回的页面总是在堆栈中登录页面之前的页面,但这可能并不总是正确的。至于第一种解决方案,在状态中传递整个“location”对象(而不仅仅是“location.pathname”),以防需要保留原始URL中可能存在的搜索查询。 - Marco Castanho

-1
如果你使用的是react-router-dom@6,请按照以下步骤进行操作: 1. 保护你的路由。
  import { Navigate, Outlet,} from "react-router-dom"; 

    const PrivateRouters = () => {     
    let  userAuth = true ;  
   return (
       userAuth ? <Outlet/> : <Navigate to="/login" />
    
      )
        }
    
    export default PrivateRouters;
  • 登录组件使用submit函数和useNavigate()。
  • 第一步导入:

    import { useNavigate } from "react-router-dom";

    第二步变量声明:

    const navigate = useNavigate();

    第三步用户提交函数:

    navigate("/pathname");


    这并没有达到要求。你的解决方案总是重定向到同一个页面。 - Marco Castanho

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接