如何在使用护照进行社交身份验证后重定向到正确的客户端路由(反应,反应路由器,快递,护照)



我有一个React/Redux/React Router前端,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 方身份验证,然后在res.redirect上重新加载 SPA,因此存储被破坏并且重定向路径丢失。

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

如果其他人正在为此苦苦挣扎,这就是我最终选择的:

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. 在社交身份验证回调中,重定向到客户端上的配置文件页面,添加 userId 和令牌作为路由参数

// 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组件中,拉取用户 ID 和令牌 从路由参数中取出,立即使用window.location.replaceState,并将它们保存到本地存储。然后检查 localStorage 中的重定向 URL。如果存在,请重定向,然后清除该值

// 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 的开发中工作,因为在服务器 URL 呈现的页面设置为 localStorage 的值将不存在于客户端 URL 的本地存储中

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

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

您可以为您的快速应用程序创建一个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

使用表达式渲染你的 React 应用并冻结 react 应用内的用户状态

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

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

通过这样做,您可以执行以下操作:

const isAuthenticated = window.authenticated;

这不是最佳做法,但这是您的状态的水合和补水的想法。

引用:

  1. Redux 中的水合作用/再水合作用
  2. 合物/补水的想法
  3. 反应/护照认证示例
  4. 饼干/护照认证示例

最新更新