如何在使用 Passport(react、react-router、express、passport)进行社交身份验证后重定向到正确的客户端路由

IT技术 node.js reactjs express react-router passport.js
2021-05-17 09:11:54

我有一个 React/Redux/React 路由器前端,Node/Express 后端。我正在使用 Passport(各种策略,包括 Facebook、Google 和 Github)进行身份验证。

我想要发生的事情:

  1. 未经身份验证的用户尝试访问受保护的客户端路由(类似于/posts/:postid,并被重定向到/login。(react-router正在处理这部分)

  2. 用户单击“使用 Facebook 登录”按钮(或其他社交身份验证服务)

  3. 身份验证后,用户会自动重定向回他们在步骤 1 中尝试访问的路由。

正在发生的事情:

我发现使用 React 前端成功处理 Passport 社交身份验证的唯一方法是将“使用 Facebook 登录”按钮包装在<a>标签中:

<a href="http://localhost:8080/auth/facebook">Facebook Login</a>

如果我尝试将其作为 API 调用而不是链接来执行,我总是会收到一条错误消息(此处更详细地解释了此问题:使用 Passport + Facebook + Express + create-react-app + React-Router + 进行身份验证代理)

所以用户点击链接,命中 Express API,成功通过 Passport 进行身份验证,然后 Passport 重定向到回调路由 ( http://localhost:8080/auth/facebook/callback)。

在回调函数中,我需要 (1) 将用户对象和令牌返回给客户端,以及 (2) 重定向到客户端路由——要么是他们在重定向到之前尝试访问的受保护路由/login,要么是一些默认路由,如//dashboard

但是由于在 Express 中没有办法同时完成这两件事(我不能res.sendres.redirect,我必须选择一个),所以我一直在以一种笨拙的方式处理它: res.redirect(`${CLIENT_URL}/user/${userId}`)

这会在/user客户端加载路由,然后我从路由参数中提取 userId,将其保存到 Redux,然后向服务器发出另一个调用以返回令牌以将令牌保存到 localStorage。

这一切正常,虽然感觉很笨重,但我不知道如何在提示登录之前重定向到用户尝试访问的受保护路由。

当用户尝试访问它时,我首先尝试将尝试的路由保存到 Redux,我想一旦他们在身份验证后登陆个人资料页面,我就可以使用它来重定向。但由于 Passport 身份验证流程将用户带到异地进行 3d 方身份验证,然后在 上重新加载 SPA res.redirect,因此存储被破坏并且重定向路径丢失。

我最终解决的是将尝试的路由保存到 localStorage,redirectUrl/user组件安装在前端检查 localStorage 中是否有一个,重定向this.props.history.push(redirectUrl)然后redirectUrl从 localStorage 中清除键。这似乎是一个非常肮脏的解决方法,必须有更好的方法来做到这一点。有没有其他人弄清楚如何使这项工作?

2个回答

万一其他人为此而苦苦挣扎,这就是我最终的选择:

1. 当用户尝试访问受保护的路由时,/login使用 React-Router重定向到

首先定义一个<PrivateRoute>组件:

// App.jsx

const PrivateRoute = ({ component: Component, loggedIn, ...rest }) => {
  return (
    <Route
      {...rest}
      render={props =>
        loggedIn === true ? (
          <Component {...rest} {...props} />
        ) : (
          <Redirect
            to={{ pathname: "/login", state: { from: props.location } }}
          />
        )
      }
    />
  );
};

然后将loggedIn属性传递给路由:

// App.jsx

<PrivateRoute
  loggedIn={this.props.appState.loggedIn}
  path="/poll/:id"
  component={ViewPoll}
/>

2. 在/login组件中,将之前的路由保存到 localStorage,以便我可以在身份验证后重定向回那里:

// Login.jsx

  componentDidMount() {
   const { from } = this.props.location.state || { from: { pathname: "/" } };
   const pathname = from.pathname;
   window.localStorage.setItem("redirectUrl", pathname);
}

3.在SocialAuth回调中,重定向到客户端的个人资料页面,添加userId和token作为路由参数

// auth.ctrl.js

exports.socialAuthCallback = (req, res) => {
  if (req.user.err) {
    res.status(401).json({
        success: false,
        message: `social auth failed: ${req.user.err}`,
        error: req.user.err
    })
  } else {
    if (req.user) {
      const user = req.user._doc;
      const userInfo = helpers.setUserInfo(user);
      const token = helpers.generateToken(userInfo);
      return res.redirect(`${CLIENT_URL}/user/${userObj._doc._id}/${token}`);
    } else {
      return res.redirect('/login');
    }
  }
};

4.在Profile客户端组件中,从路由参数中取出userId和token,立即使用删除 window.location.replaceState,保存到localStorage。然后检查localStorage 中的redirectUrl。如果存在,重定向然后清除值

// Profile.jsx

  componentWillMount() {
    let userId, token, authCallback;
    if (this.props.match.params.id) {
      userId = this.props.match.params.id;
      token = this.props.match.params.token;
      authCallback = true;

      // if logged in for first time through social auth,
      // need to save userId & token to local storage
      window.localStorage.setItem("userId", JSON.stringify(userId));
      window.localStorage.setItem("authToken", JSON.stringify(token));
      this.props.actions.setLoggedIn();
      this.props.actions.setSpinner("hide");

      // remove id & token from route params after saving to local storage
      window.history.replaceState(null, null, `${window.location.origin}/user`);
    } else {
      console.log("user id not in route params");

      // if userId is not in route params
      // look in redux store or local storage
      userId =
        this.props.profile.user._id ||
        JSON.parse(window.localStorage.getItem("userId"));
      if (window.localStorage.getItem("authToken")) {
        token = window.localStorage.getItem("authToken");
      } else {
        token = this.props.appState.authToken;
      }
    }

    // retrieve user profile & save to app state
    this.props.api.getProfile(token, userId).then(result => {
      if (result.type === "GET_PROFILE_SUCCESS") {
        this.props.actions.setLoggedIn();
        if (authCallback) {
          // if landing on profile page after social auth callback,
          // check for redirect url in local storage
          const redirect = window.localStorage.getItem("redirectUrl");
          if (redirect) {
            // redirect to originally requested page and then clear value
            // from local storage
            this.props.history.push(redirect);
            window.localStorage.setItem("redirectUrl", null);
          }
        }
      }
    });
  }

这篇博文有助于解决问题。链接帖子中的 #4(推荐)解决方案要简单得多,并且可能在生产中运行良好,但我无法让它在服务器和客户端具有不同基本 URL 的开发中工作,因为一个值设置为 localStorage在服务器 URL 处呈现的页面将不存在于客户端 URL 的本地存储中

根据您的应用程序架构,我可以给您一些想法,但它们都基于基本原理:

后端处理身份验证后,您还需要在后端存储用户的状态(通过会话 cookie / JWT)

您可以为您的 Express 应用程序创建一个 cookie 会话存储,您需要正确配置以使用两个域(后端域和前端域)或为此使用 JWT。

让我们了解更多细节

使用 React 检查身份验证状态

您可以在 express 中实现一个端点调用/api/credentials/check403如果用户未通过身份验证并且200如果是该端点将返回

在您的 React 应用程序中,您必须调用此端点并检查用户是否已通过身份验证。如果未通过身份验证,您可以重定向到/loginReact 前端。

我使用类似的东西:

class AuthRoute extends React.Component {
    render() {

        const isAuthenticated = this.props.user;
        const props = assign( {}, this.props );

        if ( isAuthenticated ) {
             return <Route {...props} />;
        } else {
             return <Redirect to="/login"/>;
        }

    }
}

然后在你的路由器中

<AuthRoute exact path="/users" component={Users} />
<Route exact path="/login" component={Login} />

在我的根组件中,我添加

componentDidMount() {
    store.dispatch( CredentialsActions.check() );
}

CredentialsActions.check只是一个调用填充props.user的情况下,我们返回200/credentials/check

使用 express 来渲染你的 React 应用程序并在 React 应用程序中脱水用户状态

这个有点棘手。并且它假定您的 react 应用程序是从您的 express 应用程序提供的,而不是作为静态.html文件提供的。

在这种情况下,您可以添加一个特殊的<script>const state = { authenticated: true }</script>,如果用户通过身份验证,它将由 express 提供服务。

通过这样做,您可以:

const isAuthenticated = window.authenticated;

这不是最佳实践,但它是您的状态的水合物和再水化的想法。

参考 :

  1. Redux 中的水化/再水化
  2. 补水/补水的想法
  3. React / Passport 身份验证示例
  4. cookie / Passport 身份验证示例