使用passport.js在node.js中进行身份验证后重定向到先前的页面

116

我正在尝试使用node.js,express和passport.js建立登录机制。登录本身运行良好,也在redis中很好地存储了会话,但我在重定向用户到进行身份验证之前所在的位置时遇到了一些问题。

例如,用户访问链接http://localhost:3000/hidden,然后被重定向到http://localhost:3000/login,但我希望他再次被重定向回http://localhost:3000/hidden

这样做的目的是,如果用户随机访问页面,需要先登录,他将被重定向到登录页面提供凭据,然后再被重定向回他之前尝试访问的网站。

这是我的登录提交代码:

app.post('/login', function (req, res, next) {
    passport.authenticate('local', function (err, user, info) {
        if (err) {
            return next(err)
        } else if (!user) { 
            console.log('message: ' + info.message);
            return res.redirect('/login') 
        } else {
            req.logIn(user, function (err) {
                if (err) {
                    return next(err);
                }
                return next(); // <-? Is this line right?
            });
        }
    })(req, res, next);
});

这里是我的 ensureAuthenticated 方法

function ensureAuthenticated (req, res, next) {
  if (req.isAuthenticated()) { 
      return next();
  }
  res.redirect('/login');
}

该钩子连接到/hidden页面

app.get('/hidden', ensureAuthenticated, function(req, res){
    res.render('hidden', { title: 'hidden page' });
});

登录页面的HTML输出非常简单。

<form method="post" action="/login">

  <div id="username">
    <label>Username:</label>
    <input type="text" value="bob" name="username">
  </div>

  <div id="password">
    <label>Password:</label>
    <input type="password" value="secret" name="password">
  </div>

  <div id="info"></div>
    <div id="submit">
    <input type="submit" value="submit">
  </div>

</form>

嗨,我也在做同样的事情,唯一的区别是当用户成功登录后,我将重定向到welcome.html页面,并显示消息“欢迎UserName”,其中UserName将是他/她的LoginName。你能告诉我如何将文本框的值传递到下一页吗? - Dhrumil Shah
2
不知道太多的话,我猜只需简单地传递它。根据我的例子,可能是这样的: res.render('hidden', {title: '隐藏页面', username: '提利昂·兰尼斯特'}); - Alx
7个回答

126
在你的ensureAuthenticated方法中,像这样将返回的url保存在session中:
...
req.session.returnTo = req.originalUrl; 
res.redirect('/login');
...

接下来你可以将你的passport.authenticate路由更新为类似以下内容:

app.get('/auth/google/return', passport.authenticate('google'), function(req, res) {
    res.redirect(req.session.returnTo || '/');
    delete req.session.returnTo;
}); 

7
在passport.authenticate路由中的重定向之后,我需要添加以下代码来避免在后续的注销/登录后返回到returnTo地址: req.session.returnTo = null; 有关passport默认注销的其他信息,请参见此问题。通过查看源代码,似乎只清除session.user而不是整个会话。 - Rob Andren
@linuxdan 为什么它不安全? - phillipwei
@phillipwei - 如果你在查询字符串中传递身份验证相关数据,那么你就是在向黑客透露你的身份验证系统的实现细节。 - linuxdan
@linuxdan 但这里的值只是用户最初导航到的URL,我认为不需要保护它,对吧?或者我漏掉了什么明显的东西吗? - phillipwei
6
建议使用 req.originalUrl 替代 req.path,因为它包括了 baseUrl 和 path 的组合。详见文档 - matthaeus
显示剩余6条评论

89

我不知道护照的事,但是这是我的做法:

我有一个中间件,使用app.get('/account', auth.restrict, routes.account),在会话中设置redirectTo,然后重定向到/login。

auth.restrict = function(req, res, next){
    if (!req.session.userid) {
        req.session.redirectTo = '/account';
        res.redirect('/login');
    } else {
        next();
    }
};

然后在routes.login.post中,我执行以下操作:

var redirectTo = req.session.redirectTo || '/';
delete req.session.redirectTo;
// is authenticated ?
res.redirect(redirectTo);

4
没问题。确实如此。他们登录后,我总是重定向到 /account 页面,但你可以用 req.session.redirect_to = req.path 来替代它。 - chovy
1
嗯...这不是我正在寻找的。我调用ensureAuthenticated来确定用户是否已经进行了身份验证,如果没有,就会重定向到/login。从这个视图中,我需要一种方法返回/account。在/login中调用req.path只会给我简单的/login。使用referer也不起作用,而且不确定浏览器是否正确设置了referer... - Alx
6
在您的身份验证中间件中,您需要设置 req.session.redirect_to = req.path。然后在 login.post 路由中读取并删除它。 - chovy
1
这并不是一个很好的解决方案,因为passport在请求不可用的时候接受successRedirect作为选项。 - linuxdan
或者您可以使用 req.flash('redirect')。 - OMGPOP
显示剩余2条评论

17

7

我的做事方式:

const isAuthenticated = (req, res, next) => {
  if (req.isAuthenticated()) {
    return next()
  }
  res.redirect( `/login?origin=${req.originalUrl}` )
};

GET /login 控制器:

if( req.query.origin )
  req.session.returnTo = req.query.origin
else
  req.session.returnTo = req.header('Referer')

res.render('account/login')

POST /login控制器:

  let returnTo = '/'
  if (req.session.returnTo) {
    returnTo = req.session.returnTo
    delete req.session.returnTo
  }

  res.redirect(returnTo);

POST /logout控制器(不确定是否完全正确,欢迎评论):

req.logout();
res.redirect(req.header('Referer') || '/');
if (req.session.returnTo) {
  delete req.session.returnTo
}

清除returnTo中间件(在任何路由上清除会话中的returnTo,除了认证路由 - 对我来说它们是/login和/auth/:provider):

String.prototype.startsWith = function(needle)
{
  return(this.indexOf(needle) == 0)
}

app.use(function(req, res, next) {
  if ( !(req.path == '/login' || req.path.startsWith('/auth/')) && req.session.returnTo) {
    delete req.session.returnTo
  }
  next()
})

这种方法有两个特点

  • 您可以使用isAuthenticated中间件来保护一些路由;
  • 在任何页面上,您只需单击登录URL,然后在登录后返回到该页面即可;

这里有很多好的回复,我能够使用你的示例并使其与我的项目一起工作。谢谢! - user752746

6

这似乎是@jaredhanson答案的重复。 - mikemaccana
值得注意的是,需要配置express-session以保存“未初始化”的cookie:{ saveUninitialized: true }。这意味着匿名用户将为他们创建会话,并对您的会话存储库施加更大的压力。好处是,这些会话将存储returnTo位置,并在匿名用户成功验证后使用。 - Anton Drukh

1
@chovy和@linuxdan的答案存在一个bug,如果用户在登录重定向后转到另一个页面并通过该页面登录,则不会清除session.returnTo。因此,请将以下代码添加到他们的实现中:
// clear session.returnTo if user goes to another page after redirect to login
app.use(function(req, res, next) {
    if (req.path != '/login' && req.session.returnTo) {
        delete req.session.returnTo
    }
    next()
})

如果您从登录页面进行了一些ajax请求,您也可以将它们排除在外。
另一种方法是在ensureAuthenticated中使用flash
req.flash('redirectTo', req.path)
res.redirect('/login')

然后在GET登录

res.render('login', { redirectTo: req.flash('redirectTo') })

在登录表单中添加隐藏字段(以jade为例)。
if (redirectTo != '')
    input(type="hidden" name="redirectTo" value="#{redirectTo}")

在POST登录中
res.redirect(req.body.redirectTo || '/')

请注意,redirectTo 在第一次使用 GET 登录后将被清除。

0

你能详细说明一下吗?我看到successRedirect被用在返回路由上,但是预期的目标地址(即上面的returnTo)会丢失,对吗? - Tom Söderlund

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接