我有一个服务器,用户可以通过电子邮件注册。我想允许在大多数N
设备中进行连接,如电脑、手机和平板电脑。我想阻止用户与许多其他用户共享凭据,因此当用户登录时,我想注销除N
最近会话之外的所有会话。
我使用NodeJS、MongoDB和Passport,并使用自定义的一次性密码(otp
)身份验证策略:
用户模型文件包括:
const mongoose = require('mongoose');
const UserSchema = new Schema({
// ...
});
UserSchema.methods.validateOtp = async function(otp) {
// ...
};
用户的路由文件包括:
const express = require('express');
const router = express.Router();
const passport = require('passport');
router.post(
"/login",
passport.authenticate("user-otp", {
successRedirect: "/dashboard",
failureRedirect: "back",
})
);
passport.use('user-otp', new CustomStrategy(
async function(req, done) {
user = await User.findOne({req.body.email});
let check = await user.validateOtp(req.body.otp);
// more logic...
}
));
我发现NodeJS注销了所有用户会话,但在数据库中找不到sessions
集合,尽管我有两个活动会话。
如何让用户退出除N
最近的会话之外的所有会话?
更新
在回答之后,我意识到我遗漏了与会话相关的代码。主脚本文件包括:
const cookieParser = require('cookie-parser');
const passport = require('passport');
const session = require('cookie-session');
app.use(cookieParser("something secret"));
app.use(
session({
// cookie expiration: 90 days
maxAge: 90 * 24 * 60 * 60 * 1000,
secret: config.secret,
signed: true,
resave: true,
httpOnly: true, // Don't let browser javascript access cookies.
secure: true, // Only use cookies over https.
})
);
app.use(passport.initialize());
app.use(passport.session());
app.use('/', require('./routes/users'));
模块cookie-session
将数据存储在客户端上,我认为它不能处理注销除最后一个N
会话之外的所有会话,因为服务器上没有数据库。
您确定当前确实有一个持久会话存储吗?如果你不是有意在帖子中遗漏任何中间件,那么我怀疑你没有。
大多数使用express进行开发的首选是express-session
,它需要添加为自己的中间件。在其默认配置中,express会话只会将所有会话存储在内存中。内存存储在重新启动时是不持久的,并且除了存储会话信息之外,不容易出于任何目的进行交互。(如用户查询会话以删除它们)
我怀疑您想要使用connect-mongodb-session
作为express-session
的会话存储机制。这将把您的会话存储在mongodb中的"sessions"集合中。这里有一些样板可以帮你。
请原谅这里可能存在的任何小错误,我在这里编写了所有这些代码,但没有运行任何代码,所以可能有一些小问题需要纠正。
const express = require('express');
const passport = require('passport');
const session = require('express-session');
const MongoDBStore = require('connect-mongodb-session')(session);
const app = express();
const router = express.Router();
// Initialize mongodb session storage
const store = new MongoDBStore({
uri: 'mongodb://localhost:27017/myDatabaseName',
// The 'expires' option specifies how long after the last time this session was used should the session be deleted.
// Effectively this logs out inactive users without really notifying the user. The next time they attempt to
// perform an authenticated action they will get an error. This is currently set to 1 hour (in milliseconds).
// What you ultimately want to set this to will be dependent on what your application actually does.
// Banks might use a 15 minute session, while something like social media might be a full month.
expires: 1000 * 60 * 60,
});
// Initialize and insert session middleware into the app using mongodb session storage
app.use(session({
secret: 'This is a secret that you should securely generate yourself',
cookie: {
// Specifies how long the user's browser should keep their cookie, probably should match session expires
maxAge: 1000 * 60 * 60
},
store: store,
// Boilerplate options, see:
// * https://www.npmjs.com/package/express-session#resave
// * https://www.npmjs.com/package/express-session#saveuninitialized
resave: true,
saveUninitialized: true
}));
// Probably should include any body parser middleware here
app.use(passport.initialize());
app.use(passport.session());
// Should init passport stuff here like your otp strategy
// Routes go here
因此,在cookie和会话正常工作后,下一部分是让路由受到身份验证的实际保护。我们这样做是为了确保一切正常。
// Middleware to reject users who are not logged in
var isAuthenticated = function(req, res, next) {
if (req.user) {
return next();
}
// Do whatever you want to happen when the user is not logged in, could redirect them to login
// Here's an example of just rejecting them outright
return res.status(401).json({
error: 'Unauthorized'
});
}
// Middleware added to this route makes it protected
router.get('/mySecretRoute', isAuthenticated, (req, res) => {
return res.send('You can only see this if you are logged in!');
});
在这一步中,你应该检查如果你没有登录,你是否无法到达秘密路由(应该会出错),如果你已经登录,你可以到达它(请参阅秘密消息)。注销和往常一样:注销路径中的req.logout()
。假设一切都好,现在让我们攻击实际问题,注销除最近的4个会话之外的所有会话。
现在,为了简单起见,我假设您正在对每个用户强制执行otp。正因为如此,我们可以利用您之前声明的passport-otp中间件。如果你不是,那么你可能需要对passport做更多的自定义逻辑。
// Connect to the database to access the `sessions` collection.
// No need to share the connection from the main script `app.js`,
// since you can have multiple connections open to mongodb.
const mongoose = require('mongoose');
const connectRetry = function() {
mongoose.connect('mongodb://localhost:27017/myDatabaseName', {
useUnifiedTopology: true,
useNewUrlParser: true,
useCreateIndex: true,
poolSize: 500,
}, (err) => {
if (err) {
console.error("Mongoose connection error:", err);
setTimeout(connectRetry, 5000);
}
});
}
connectRetry();
passport.use('user-otp', new CustomStrategy(
async function(req, done) {
user = await User.findOne({ req.body.email });
let check = await user.validateOtp(req.body.otp);
// Assuming your logic has decided this user can login
// Query for the sessions using raw mongodb since there's no mongoose model
// This will query for all sessions which have 'session.passport.user' set to the same userid as our current login
// It will ignore the current session id
// It will sort the results by most recently used
// It will skip the first 3 sessions it finds (since this session + 3 existing = 4 total valid sessions)
// It will return only the ids of the found session objects
let existingSessions = await mongoose.connection.db.collection('sessions').find({
'session.passport.user': user._id.toString(),
_id: {
$ne: req.session._id
}
}).sort({ expires: 1}).skip(3).project({ _id: 1 }).toArray();
// Note: .toArray() is necessary to convert the native Mongoose Cursor to an array.
if (existingSessions.length) {
// Anything we found is a session which should be destroyed
await mongoose.connection.db.collection('sessions').deleteMany({
_id: {
$in: existingSessions.map(({ _id }) => _id)
}
});
}
// Done with revoking old sessions, can do more logic or return done
}
));
现在,如果你从不同的设备登录4次,或者每次都清除cookie后,你应该能够在mongo控制台中查询并查看所有4个会话。如果您第五次登录,您应该会看到仍然只有4个会话,并且最旧的会话已被删除。
我要再次指出的是,我实际上并没有尝试执行我在这里写的任何代码,所以我可能错过了一些小东西或包含了拼写错误。请花一点时间,试着自己解决任何问题,但如果有些问题仍然不起作用,请告诉我。
留给您的任务:
- 如果不将
session.passport.user
的索引添加到sessions
集合中,您的mongo查询性能将低于标准。您应该为该字段添加一个索引,例如在Mongo-shell上运行db.sessions.createIndex({"session.passport.user": 1})
(请参阅文档)。(注意:尽管passport
是session
字段的子文档,但您可以像访问Javascript对象一样访问它:session.passport
。) - 如果重置了密码,您可能还应该注销其他会话已执行
- 调用
req.logout()
时,您应该从集合中删除会话 - 为了对用户友好,您可以向已撤销的会话添加一条消息,以便在用户尝试从以前的设备访问内容时显示。过期会话也是如此。您可以删除这些会话以保持集合较小
express-session
模块在用户的浏览器中存储cookie,即使没有登录。为了遵守欧洲的GDPR,您应该添加关于cookie的通知- 实现从
cookie-session
(存储在客户端中)到express-session
的更改将注销所有以前的用户。为了对用户友好,你应该提前警告他们,并确保你一次完成所有更改,而不是多次尝试,让他们因为不得不多次登录而感到愤怒