从以前的会话中注销用户
Log user out of previous sessions
我有一个用户通过电子邮件注册的服务器。我想允许最多连接 N
台设备,例如计算机、phone 和平板电脑。我想阻止用户与许多其他用户共享凭据,因此我想在用户登录时注销除 N
最近的所有会话。
我正在使用 NodeJS、MongoDB 和带有自定义一次性密码 (otp
) 身份验证策略的 Passport:
用户模型文件包括:
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 logout all user sessions,但我无法在数据库中找到 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
会话之外的所有注销,因为服务器上没有数据库。
您确定您目前确实拥有持久会话存储吗?如果您没有故意在 post 中遗漏任何中间件,那么我怀疑您没有。
大多数使用 express 的开发的首选是 express-session
,它需要作为自己的中间件添加。在其默认配置中,express-session 只会将所有会话存储在内存中。内存存储不会通过重新启动持久存在,并且除了存储会话信息之外,不容易与其他任何目的进行交互。 (如用户查询会话以删除它们)
我怀疑您想要使用 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。因此,我们可以利用您之前声明的护照 otp 中间件。如果你不是,那么你可能需要对护照做更多的自定义逻辑。
// 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 个会话。如果您第 5 次登录,您应该会看到仍然只有 4 个会话,并且最旧的已被删除。
我要再说一次,我实际上并没有尝试执行我在这里编写的任何代码,所以我可能遗漏了一些小东西或包含错别字。请花点时间尝试自己解决任何问题,但如果仍然无法解决问题,请告诉我。
留给你的任务:
- 如果您不将
session.passport.user
的索引添加到 sessions
集合,您的 mongo 查询性能将低于标准。您应该为该字段添加索引,例如运行 db.sessions.createIndex({"session.passport.user": 1})
在 Mongo shell 上(参见 docs)。 (注意:虽然 passport
是 session
字段的子文档,但您可以像访问 Javascript 对象一样访问它:session.passport
。)
- 如果重设密码,您可能还应该注销其他会话
已执行。
- 您应该在调用
req.logout()
时从集合中删除会话。
- 为了对用户友好,您可以向已撤销的会话添加一条消息,以便在用户尝试从以前的设备访问内容时显示。过期会话也是如此。您可以删除这些会话以保持集合较小。
- 即使没有登录,模块
express-session
也会在用户的浏览器中存储一个 cookie。为了符合欧洲的 GDPR,您应该添加有关 cookie 的通知。
- 实施从
cookie-session
(存储在客户端中)到 express-session
的更改将注销所有以前的用户。为了对用户友好,您应该提前警告他们并确保您一次完成所有更改,而不是尝试多次,而他们因必须多次登录而感到恼火。
我有一个用户通过电子邮件注册的服务器。我想允许最多连接 N
台设备,例如计算机、phone 和平板电脑。我想阻止用户与许多其他用户共享凭据,因此我想在用户登录时注销除 N
最近的所有会话。
我正在使用 NodeJS、MongoDB 和带有自定义一次性密码 (otp
) 身份验证策略的 Passport:
用户模型文件包括:
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 logout all user sessions,但我无法在数据库中找到 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
会话之外的所有注销,因为服务器上没有数据库。
您确定您目前确实拥有持久会话存储吗?如果您没有故意在 post 中遗漏任何中间件,那么我怀疑您没有。
大多数使用 express 的开发的首选是 express-session
,它需要作为自己的中间件添加。在其默认配置中,express-session 只会将所有会话存储在内存中。内存存储不会通过重新启动持久存在,并且除了存储会话信息之外,不容易与其他任何目的进行交互。 (如用户查询会话以删除它们)
我怀疑您想要使用 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。因此,我们可以利用您之前声明的护照 otp 中间件。如果你不是,那么你可能需要对护照做更多的自定义逻辑。
// 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 个会话。如果您第 5 次登录,您应该会看到仍然只有 4 个会话,并且最旧的已被删除。
我要再说一次,我实际上并没有尝试执行我在这里编写的任何代码,所以我可能遗漏了一些小东西或包含错别字。请花点时间尝试自己解决任何问题,但如果仍然无法解决问题,请告诉我。
留给你的任务:
- 如果您不将
session.passport.user
的索引添加到sessions
集合,您的 mongo 查询性能将低于标准。您应该为该字段添加索引,例如运行db.sessions.createIndex({"session.passport.user": 1})
在 Mongo shell 上(参见 docs)。 (注意:虽然passport
是session
字段的子文档,但您可以像访问 Javascript 对象一样访问它:session.passport
。) - 如果重设密码,您可能还应该注销其他会话 已执行。
- 您应该在调用
req.logout()
时从集合中删除会话。 - 为了对用户友好,您可以向已撤销的会话添加一条消息,以便在用户尝试从以前的设备访问内容时显示。过期会话也是如此。您可以删除这些会话以保持集合较小。
- 即使没有登录,模块
express-session
也会在用户的浏览器中存储一个 cookie。为了符合欧洲的 GDPR,您应该添加有关 cookie 的通知。 - 实施从
cookie-session
(存储在客户端中)到express-session
的更改将注销所有以前的用户。为了对用户友好,您应该提前警告他们并确保您一次完成所有更改,而不是尝试多次,而他们因必须多次登录而感到恼火。