跨域中的Node.js身份验证

10

我正在开发一个使用 MEAN 技术栈的应用,项目中使用了 Angular 4。在认证方面,我使用了 Passport.js 的Local-strategy,并使用了Express-session来保持持久会话。到目前为止,一切都很顺利。

问题

在同一域中,session可以正常工作,并且我能够对用户进行身份验证。但是在跨域中,我无法保持会话,每个新请求都会生成一个新的会话 ID。

然后我尝试了Passport-jwt,但它的问题是我无法控制用户会话。我的意思是,如果用户处于非活动状态,我不能从服务器注销用户,甚至在服务器重新启动时token也不会失效。

因此简而言之,我正在寻找一个Node.js(Express.js)身份验证解决方案,可以管理跨域身份验证。

我已经阅读了一些博客文章和类似于这样的 SO 问题,但它们都没有帮助。

谢谢。

编辑

我应该编写自己的代码来实现此功能吗?如果是这样,我有一个计划。

我的基本计划是:

  1. 用户将在登录请求中发送凭据。
  2. 我将在数据库中检查凭据。如果凭据有效,我将生成一个随机令牌并将其保存到用户表中,并将同一令牌提供给用户以获得成功响应。
  3. 现在,每个请求用户都会发送令牌,我将在数据库中检查每个请求的令牌。如果令牌有效,则允许用户访问 API,否则将生成具有401状态代码的错误。
  4. 我使用 Mongoose (MongoDB),所以从性能角度考虑,每个请求中检查令牌都是可以接受的。

我认为这也是一个好主意。我只想要一些建议,无论我是否朝着正确的方向思考。

我将得到以下内容:

  1. 应用程序中已登录用户的数量(活动会话)。
  2. 如果用户处于空闲状态,我可以注销用户。
  3. 我可以管理同一用户的多个登录会话(通过在数据库中进行记录)。
  4. 我可以允许最终用户清除所有其他登录会话(就像 Facebook 和 Gmail 一样)。
  5. 与授权相关的任何自定义。

编辑 2

我在这里分享我的app.js代码。

var express = require('express');
var helmet = require('helmet');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var dotenv = require('dotenv');
var env = dotenv.load();
var mongoose = require('mongoose');
var passport = require('passport');
var flash    = require('connect-flash');
var session      = require('express-session');
var cors = require('cors');

var databaseUrl = require('./config/database.js')[process.env.NODE_ENV || 'development'];
// configuration 
mongoose.connect(databaseUrl); // connect to our database

var app = express();

// app.use(helmet());

// required for passport


app.use(function(req, res, next) {
  res.header('Access-Control-Allow-Credentials', true);
  res.header('Access-Control-Allow-Origin', req.headers.origin);
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
  res.header('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept');
  if ('OPTIONS' == req.method) {
       res.send(200);
   } else {
       next();
   }
});


app.use(cookieParser());

app.use(session({
    secret: 'ilovescotchscotchyscotchscotch', // session secret
    resave: true,
    saveUninitialized: true,
    name: 'Session-Id',
    cookie: {
      secure: false,
      httpOnly: false
    }
}));


require('./config/passport')(passport); // pass passport for configuration

var index = require('./routes/index');
var users = require('./routes/user.route');
var seeders = require('./routes/seeder.route');
var branches = require('./routes/branch.route');
var companies = require('./routes/company.route');
var dashboard = require('./routes/dashboard.route');
var navigation = require('./routes/navigation.route');
var roles = require('./routes/role.route');
var services = require('./routes/services.route');

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use(passport.initialize());
app.use(passport.session()); // persistent login sessions
app.use(flash()); // use connect-flash for flash messages stored in session

require('./routes/auth.route')(app, passport);
app.use('/', index);
app.use('/users', users);
app.use('/seed', seeders);
app.use('/branches', branches);
app.use('/companies', companies);
app.use('/dashboard', dashboard);
app.use('/navigation', navigation);
app.use('/roles', roles);
app.use('/services', services);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  res.status(404).send({ status: 'NOT_FOUND', message: 'This resource is not available.'});
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  let errorObj = { 
    status: 'INTERNAL_SERVER_ERROR',
    message: 'Something went wrong.',
    error: err.message
  };
  res.status(err.status || 500).send(errorObj);
});

module.exports = app;

编辑3

对于那些不理解我的问题的人,我用简单的话来解释一下:

  1. 我的Express服务器运行在3000端口。
  2. 为了使用来自服务器的API,用户必须先登录。
  3. 当一个用户从localhost:3000登录时,服务器使用Passport-local检查凭据并在响应头中返回一个令牌。
  4. 现在,在登录后,当一个用户访问localhost:3000的任何API时,一个预定义的Header会随着passport-session一起被发送,然后passport会使用req.isAuthenticated()验证用户会话,并且所有东西都按预期工作。
  5. 当一个用户从localhost:4000登录并且服务器在响应头中发送一个令牌(与localhost:3000相同)时。
  6. 当成功登录后,用户从localhost:4000访问任何API时,passport js函数req.isAuthenticated()会返回false
  7. 这是因为在跨域时,cookie不会传递到服务器,我们需要在客户端设置withCredentials头为true
  8. 我已将withCredentials头设置为true,但仍然在服务器上,req.isAuthenticated()返回false

可能最简单的解决方案是使用cookies,它们是“跨域localStorage”...有点类似... https://github.com/zendesk/cross-storage 至于另一个问题,我认为已经有现成的解决方案了,但我从未寻找过,如果你找到了,请分享一下,谢谢。 - Akxe
@Akxe_,我的问题不在客户端。问题出在Express-session上,如果我们处于跨域状态下,服务器会在每个请求上生成新的会话。 - Arpit Kumar
你能分享一些代码吗?“为跨域的每个新请求生成一个新的会话ID”对我来说似乎不正确。 - kkkkkkk
@Khang,我已经分享了我的代码,请看一下。 - Arpit Kumar
请在您设置Passport的代码中包含。从您的第三次编辑中,我看到您假设localhost:3000localhost:4000是不同的域。它们不是,域名是localhost,它们是相同的。我猜您设置了错误的Passport。 - kkkkkkk
显示剩余4条评论
5个回答

2
一个解决CORS/cookie/same-domain问题的可能方法是创建一个代理服务器,将 localhost:3000/api 的所有请求镜像到 localhost:4000,然后使用 localhost:3000/api 来访问API,而不是使用 localhost:4000
在生产环境中部署的最好方式是在您的Web服务器(如nginx/apache)上执行。
您还可以通过 expressrequest 模块在node中进行操作,或者使用一些现成的中间件,比如这个: https://github.com/villadora/express-http-proxy 使用这个中间件的解决方案非常简单:
var proxy = require('express-http-proxy');
var app = require('express')();

app.use('/api', proxy('localhost:4000'));

0

你已经看过这里了吗:

在这种情况下,可以根据一些考虑发送响应。

如果所讨论的资源旨在被广泛访问(就像通过GET访问的任何HTTP资源一样),那么发送Access-Control-Allow-Origin:*头将足以,[...]

您可以尝试这个(允许任何公共IP):

app.use(function(req, res, next) {
 res.header('Access-Control-Allow-Credentials', true);
 res.header('Access-Control-Allow-Origin', '*');  // add this line  
 // res.header('Access-Control-Allow-Origin', req.headers.origin);
 res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
 

第二个服务器重新创建一个新会话是正常的,因为假设您使用了Express-session,并根据文档

会话数据不保存在cookie中,只保存会话ID。会话数据存储在服务器端

这意味着您需要找到一种方法来同步服务器会话数据... 假设您找到了一种方法,在尝试连接时,两个服务器将检索相同的用户会话数据,第二个服务器将不必创建新会话...


是的,我知道。 - Arpit Kumar
我们不能使用“*”,因为我们接受跨域cookie,所以需要使用req.headers.origin - Arpit Kumar
你可以再确认一下 req.headers.origin 是否是预期的IP地址吗?这就像跨域不起作用一样,因为你限制了只有一个IP(另一个域的IP)才能访问,但如果你希望客户端可以在两个域上请求,则需要允许所有客户端公共IP,所以使用 "*"。你尝试将 "Access-Control-Allow-Origin" 添加到 "Access-Control-Allow-Headers" 列表中了吗? - A. STEFANI
@A STEFANI,我已经更新了问题,请查看我的“EDIT 3”,您将会明白我的意思。 - Arpit Kumar
@ArpitMeena,我刚刚更新了我的答案,请看一下。 - A. STEFANI

0

0
你可能已经尝试使用 passport-jwt。它将根据JWT协议在登录时生成令牌。你的要求是注销时黑名单生成的令牌。为了实现这一点,你可以在MongoDB中创建一个名为“BlacklistToken”的集合,其中包含userid和token字段。当用户注销时,可以将token和userid插入到集合中。然后编写一个中间件来检查该令牌是否已被列入黑名单。如果是,则重定向到登录页面。

0
如果我正确理解问题,您希望用户的会话在服务器上是无状态的。这样,每当用户登录时,会话可以在应用程序的任何实例中重复使用,即使您扩展应用程序,或者仅重新启动应用程序也是如此。
为了实现这一点,您需要使用数据库解决方案配置express-session。您可以使用mongo和这个包https://github.com/jdesboeufs/connect-mongo来完成这个任务。
然而,最好的做法是使用更为强大的东西来处理这种情况,比如使用redis和这个包https://github.com/tj/connect-redis

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