我有一个React / Redux / React路由器前端,Node / Express后端。我正在使用Passport(包括Facebook,Google和Github在内的各种策略)进行身份验证。
我想要发生什么:
未经身份验证的用户尝试访问受保护的客户端路由
(类似于/posts/:postid
,并重定向到/login
。
(React Router正在处理这部分)
用户点击“使用Facebook登录”按钮(或其他社交身份验证服务)
正在发生的事情:
我发现使用React前端成功处理Passport社交身份验证的唯一方法是在<a>
标记中包含“使用Facebook登录”按钮:
<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)重定向到客户端路由 - 在他们被重定向到{{1之前 - 他们试图访问的受保护路由},或某些默认路由,例如/login
或/
。
但是因为没有办法在Express中做这两件事(我不能/dashboard
和res.send
,我必须选择一个),我一直在处理它什么感觉像一种笨重的方式:
res.redirect
这会在客户端上加载
res.redirect(`${CLIENT_URL}/user/${userId}`)
路由,然后我将userId拉出路由参数,将其保存到Redux,然后再向服务器发出调用以返回令牌以将令牌保存到localStorage的。
这一切都有效,虽然感觉很笨,但我无法弄明白如何在提示登录之前重定向到用户尝试访问的受保护路由。
当用户尝试访问Redux时,我首先尝试将尝试的路由保存到Redux,认为我可以在身份验证后登陆配置文件页面时使用它重定向。但是,由于Passport身份验证流程将用户带到异地以进行3D方身份验证,然后在/user
上重新加载SPA,因此存储将被销毁并且重定向路径将丢失。
我最终解决的问题是将尝试的路由保存到localStorage,在res.redirect
组件安装在前端时检查localStorage中是否有redirectUrl
密钥,并使用{{重定向1}}然后从localStorage清除/user
密钥。这似乎是一个非常肮脏的解决方法,必须有一个更好的方法来做到这一点。有没有人想出如何使这项工作?
答案 0 :(得分:3)
如果其他人正在努力解决这个问题,那么这就是我最终的目标:
<强> 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
走出路线params,立即删除它们
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的开发中使用它,因为值设置为在服务器URL处呈现的页面的localStorage将不存在于客户端URL的本地存储中
答案 1 :(得分:1)
根据您的应用程序架构,我可以给您一些想法,但它们都基于基础:
一旦您有后端处理身份验证,您还需要在后端存储用户的状态(通过会话cookie / JWT)
您可以为您的快递应用创建一个cookie会话商店哪个cookie,您需要正确配置以使用这两个域(后端域和前端域)或使用JWT。
让我们了解更多详情
您可以在名为/api/credentials/check
的快递中实施一个终点,如果用户未经过身份验证,则会返回403
,如果是,则会200
。
在您的反应应用中,您必须调用此终点并检查用户是否已通过身份验证。如果未经过身份验证,您可以重定向到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
的来电,以防我们从200
返回/credentials/check
。
这个有点棘手。并且假设您的反应应用程序是从您的快速应用程序提供的,而不是静态.html
文件。
在这种情况下,您可以添加一个特殊的<script>const state = { authenticated: true }</script>
,如果用户经过身份验证,将由express提供。
通过这样做你可以做到:
const isAuthenticated = window.authenticated;
这不是最好的做法,但它是你的国家水合物和补液的想法。