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

Sar*_*rah 8 node.js express reactjs passport.js react-router

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

我想要发生的事情:

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

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

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

正在发生的事情:

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

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

如果我尝试将其作为 API 调用而不是链接来执行,我总是会收到一条错误消息(这里更详细地解释了这个问题:Authentication with 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 时将尝试的路由保存到 Redux,我想一旦他们在身份验证后登陆个人资料页面,我就可以使用它来重定向。但是,由于 Passport 身份验证流程将用户带到异地进行 3d 方身份验证,然后在 上重新加载 SPA res.redirect,因此存储被破坏并且重定向路径丢失。

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

Sar*_*rah 9

In case anybody else is struggling with this, this is what I ended up going with:

1. When user tries to access protected route, redirect to /login with React-Router.

First define a <PrivateRoute> component:

// 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 } }}
          />
        )
      }
    />
  );
};
Run Code Online (Sandbox Code Playgroud)

Then pass the loggedIn property to the route:

// App.jsx

<PrivateRoute
  loggedIn={this.props.appState.loggedIn}
  path="/poll/:id"
  component={ViewPoll}
/>
Run Code Online (Sandbox Code Playgroud)

2. In /login component, save previous route to localStorage so I can later redirect back there after authentication:

// Login.jsx

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

3. In SocialAuth callback, redirect to profile page on client, adding userId and token as route params

// 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');
    }
  }
};
Run Code Online (Sandbox Code Playgroud)

4. In the Profile component on the client, pull the userId and token out of the route params, immediately remove them using window.location.replaceState, and save them to localStorage. Then check for a redirectUrl in localStorage. If it exists, redirect and then clear the value

// 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);
          }
        }
      }
    });
  }
Run Code Online (Sandbox Code Playgroud)

This blog post was helpful in figuring things out. The #4 (recommended) solution in the linked post is much simpler and would probably work fine in production, but I couldn't get it to work in development where the server and client have different base URLs, because a value set to localStorage by a page rendered at the server URL will not exist in local Storage for the client URL