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

Posted

技术标签:

【中文标题】如何在使用 Passport 进行社交身份验证后重定向到正确的客户端路由(react、react-router、express、passport)【英文标题】:How to redirect to correct client route after social auth with Passport (react, react-router, express, passport) 【发布时间】:2018-09-22 03:16:06 【问题描述】:

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

我想要发生的事情:

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

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

    身份验证后,用户会自动重定向回他们在步骤 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 + proxy)

所以用户点击链接,该链接点击 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 方身份验证,然后在 res.redirect 上重新加载 SPA,因此存储被破坏并且重定向路径丢失。

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

【问题讨论】:

【参考方案1】:

如果其他人为此苦苦挣扎,这就是我最终要解决的问题:

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

首先定义一个<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);
          
        
      
    );
  

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

【讨论】:

【参考方案2】:

根据你的应用架构,我可以给你一些想法,但它们都是基于基础的:

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

您可以为您的 express 应用创建一个 cookie-session 存储,您需要正确配置哪个 cookie 以使用两个域(后端域和前端域)或为此使用 JWT。

让我们了解更多细节

使用 React 检查身份验证状态

您可以在 express 中实现一个名为 /api/credentials/check 的端点,如果用户未通过身份验证,它将返回 403,如果是,则返回 200

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

我使用类似的东西:

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 的调用,以防我们从/credentials/check 返回200

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

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

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

这样做你可以做到:

const isAuthenticated = window.authenticated;

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

参考资料:

    Hydration / rehydration in Redux Hydrate / rehydrate idea Example of React / Passport authentication Example of cookie / Passport authentication

【讨论】:

我已经将 JWT 存储在服务器上并使用 passport-jwt 策略来检查身份验证状态。我试图弄清楚如何处理 unauthenticated 用户试图访问受保护的路由,被发送到/login,单击将他们发送到服务器 URL 的社交身份验证,然后 Express res.redirect s 在回调时返回给客户端。您链接的示例使用本地护照。使用本地策略,我可以使用标准的 API 调用,它工作正常。使用社交身份验证(直接链接到http://localhost:8080/auth/facebook)我试图弄清楚如何在回调后处理客户端重定向。 您必须让您的应用程序拦截来自 Facebook 的回调(根据护照指南),然后重定向到您的客户端应用程序 /some-page。然后,当您的客户端应用程序加载时,请检查请求是否通过 /credentials/check 或您可以拥有的任何其他端点进行身份验证。如果它通过了身份验证,则加载特定的路由。

以上是关于如何在使用 Passport 进行社交身份验证后重定向到正确的客户端路由(react、react-router、express、passport)的主要内容,如果未能解决你的问题,请参考以下文章

使用 Passport 进行社交身份验证登录后如何重定向到个人资料页面? (角度,快递,护照,jwt)

GraphQL 中的社交身份验证

Nodejs和PassportJs:如果身份验证失败,则不会调用passport.authenticate后重定向中间件

使用 Passport 和 OAuth2 + 社交网络的 NodeJS REST 身份验证

如何在.NET 中进行基本身份验证后重定向到 Pentaho 的主页?

如何在通过 Redux 操作进行身份验证后重定向用户?