使用Flash、Express Sessions和Bootstrap处理认证问题
本文探讨了如何使用Passport认证模块来处理认证,用适当的Bootstrap风格向用户显示Flash消息,并将消息存储在会话中,以便在网页导航中正确显示它们。
我们将在一个应用程序中使用express-session 、connect-flash 、passport 、bootstrap 等模块构建一个认证系统。
前提条件
要跟上进度,读者应该具备以下条件。
- 对[Node.js]有良好的理解。
- 安装了Node.js。
- 一个合适的代码编辑器,最好是[VS Code]。
- MongoDB数据库的基础知识。
安装依赖项
在终端中执行以下命令。
npm install express passport passport-local bcryptjs ejs express-ejs-layouts mongoose connect-flash express-sessions
npm i -D nodemon
设置app,入口点
我们首先将入口点设置为main.js ,并导入依赖项,如下图所示。
const express = require('express')
const app = express()
const passport = require('passport');
const expressLayouts = require('express-ejs-layouts')
const flash = require('connect-flash');
const session = require('express-session');
项目设置
我们将为我们的路由建立一个单独的文件夹,因此在应用程序的根目录下创建一个routes 。
在该文件夹中,创建两个名为index.js 和users.js 的文件。认证请求将转到user.js ,而其他请求将转到index.js 路由。
创建另一个名为views 的文件夹。这个文件夹将包含我们的视图文件,这些文件将被呈现给屏幕上的用户。
在routes 目录中,添加以下文件;login.ejs register.ejs layout.ejs 和dashboard.ejs 。
当用户导航到项目的索引路线时,系统会将他/她引导到欢迎页面,该页面会渲染welcome.ejs 文件。
在那里,他选择登录或注册一个新账户。一旦用户成功注册,他就可以登录,然后他就被重定向到仪表板页面。
在login.ejs ,我们有一个表单,提交user email 和password 进行认证,如下图所示。
<form action="/users/login" method="POST">
<div class="form-group">
<input type="email" id="email" name="email" class="form-control" placeholder="Enter Email" />
</div>
<div class="form-group">
<input type="password" id="password" name="password" class="form-control" placeholder="Enter Password" />
</div>
<button type="submit" class="btn btn-success btn-block">Login</button>
</form>
对于registration 页面,我们将有一个表单,将用户数据提交给数据库,由下面的片段生成。
<form action="/users/register" method="POST">
<div class="form-group">
<input type="name" id="name" name="name" class="form-control" placeholder="Enter Name"/>
</div>
<div class="form-group">
<input type="email" id="email" name="email" class="form-control" placeholder="Enter Email" />
</div>
<div class="form-group">
<input type="password" id="password" name="password" class="form-control" placeholder="Create Password"/>
</div>
<div class="form-group">
<input type="password" id="password2" name="password2" class="form-control" placeholder="Confirm Password" />
</div>
<button type="submit" class="btn btn-success btn-block">
Register
</button>
</form>
连接到数据库
由于该应用程序将使用Mongo Atlas,我们将创建一个连接字符串并将其存储到一个文件中。
首先,在应用程序的根目录下创建一个名为config 的文件夹。在这个文件夹中,添加两个名为connection.js 和config.env 的文件。
config 文件包含环境变量,这些变量在整个应用程序中是统一的。
这个文件存储了我们的connection string 和我们的应用程序将在其中运行的port 。
PORT = 5000
MONGO_URI = 'YOUR CONNECTION STRING'
connection.js 文件将包含connection function 。该函数负责使用配置文件中的数据库URL在应用程序和远程数据库之间建立连接。
我们把这个函数导出到应用程序的入口点,这样数据库就会在服务器初始化时被连接。
const mongoose = require('mongoose')
//connnect the system to the mongo atlas remote db
const connectToRemoteDatabase = async () => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
})
console.log(`Database connection successful`)
} catch (error) {
console.error(error)
process.exit(1)
}
}
module.exports = connectDatabase
创建用户模型
模型定义了数据库记录的模样。对于我们的案例,用户将保留他们的电子邮件、用户名和密码的记录。
这些字段应该出现在用户模型和数据库中。
在应用程序的根文件夹中,创建一个名为models 的新目录。然后在生成的文件夹中创建一个名为user.js 的新文件,并添加下面的片段。
const mongoose = require('mongoose');
//user schema
const UserSchema = new mongoose.Schema({
username: {type: String, required: true },
useremail: {type: String, required: true },
password: {type: String, required: true },
date: {type: Date, default: Date.now }
});
const User = mongoose.model('User', UserSchema);
//export user model
module.exports = User;
用户注册
在这个阶段,我们将通过从下面的表格大纲中收集数据来注册用户。
由于我们要从表单中收集数据,我们需要使用body-parser中间件。
//Body parser
app.use(express.urlencoded({ extended: false}))
接下来,我们设置了一个register-handler ,从前端的请求中收集表单数据。
在第一步,注册路线从请求体中提取表单数据,然后检查表单数据是否输入正确。
//registration handler
router.post('/register', (request, response) =>{
//extract the data from request body
const name= request.body.name;
const email= request.body.email;
const password, = request.password;
const passwordConfirm = request.passwordConfirm;
let errors = []
//validation
if (!name || !email || !password || !password2) {
errors.push({ message: 'All the fields must be filled to proceed' });
}
if (password != password2) {
errors.push({ message: 'The two passwords must match to proceed' });
}
if (password.length < 5) {
errors.push({ message: 'Sorry the password must be at least 5 characters long' });
}
if (errors.length > 0) {
response.render('register', { errors, name, email, password, password2 });
}else{
//Check if the user exists
}
})
表单验证通过后,我们必须确认用户提交的电子邮件是否已经存在于数据库中。
如果电子邮件存在,我们将错误推送到errors 数组中,然后渲染registration 页面并显示errors 。
User.findOne({email: email}).then(user =>{
if(user){
errors.push({messageg: 'Email already in the database'})
res.render('register', { errors, name, email, password, password2 })
}else{
//hash the password and register the user
}
})
如果提供的电子邮件是唯一的,bcryptjs ,对密码进行散列。
保存纯文本密码是一种安全风险,所以我们对密码进行哈希处理,以避免系统被破坏。
散列后,用户实例被保存到数据库,然后系统将用户重定向到登录页面。
const newSystemUser = new User({username, useremail, password});
bcrypt.genSalt(10, (error, saltpass) =>{
bcrypt.hash(password, saltpass, (error, passwordhash) => {
if(error){
throw error;
}else{
newSystemUser.password = hash
newSystemUser.save().then(user =>{
request.flash('success_msg', 'Successfully registered. Login')
response.redirect('/users/login')
}).catch(err =>{
console.log(err)
})
}
})
})

实现connect-flash模块
目前,我们将错误传递给一个视图,该视图将在注册页面上呈现。
然而,我们希望将这些信息存储在一个会话中,以便在重定向后显示它们。这个操作需要connect-flash 中间件。
const flash = require('connect-flash');
const session = require('express-session');
// Express sessions
app.use(session({ secret: 'yoursecret', resave: true, saveUninitialized: true }));
// Connect flash
app.use(flash());
为了使每个错误以不同的颜色显示,我们在应用程序的入口点为每个错误创建全局变量并设置colors 。
// Global variables
app.use(function(request, response, next) {
response.locals.success_alert_message = request.flash('success_alert_message');
response.locals.error_message = request.flash('error_message');
response.locals.error = request.flash('error');
next();
});
在messages.js 文件中,我们检查一个消息是success 还是error 消息,然后呈现各自的警报。
<% if(success_alert_message != ''){ %>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<%= success_alert_message %>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<% } %>
<% if(error_message != ''){ %>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<%= error_message %>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<% } %>

Passport认证设置
在'config'文件夹中创建一个名为passport.js 的新文件,然后添加下面的片段。
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcryptjs');
// Loading the mongoose model from the models folder
const User = require('../models/User');
首先,我们需要引入local strategy 和mongoose ,以便在数据库中查找用户。
在这种情况下,我们使用bcrypt 来比较用户在注册时输入的密码和登录时输入的密码。
Passport需要检查电子邮件和密码,然后找到一个具有相同电子邮件的用户。如果存在相同电子邮件的用户,那么就将提供的密码与用户的密码进行比较,看是否匹配;如果密码与输入的密码相似,护照就会对用户进行认证。
然而,如果密码不相似,就会向用户显示一个错误,告诉他要输入正确的密码。
function passport() {
passport.use(
new LocalStrategy({ usernameField: 'useremail' }, (useremail, userpassword, done) => {
// find user with supplied email
User.findOne({
useremail: useremail
}).then(user => {
//the fetched user is not found
if (!user) {
return done(null, false, { message: 'The user email entered is not with our records' });
}
//use bcrypt to compare the passwords and validate
bcrypt.compare(password, user.password, (error, isMatch) => {
if (err) throw error;
if (isMatch) {
return done(null, user);
} else {
return done(null, false, { message: 'Password entered is incorrect.' });
}
});
});
})
);
passport.serializeUser( (user, done) => {
done(null, user.id);
});
passport.deserializeUser( (id, done) => {
User.findById(id, (error, user) => {
done(error, user);
});
});
};
module.exports = passport
建立登录模块处理程序。
登录处理程序使用Passport中间件来验证用户。一个通过验证的用户被重定向到home 路径,以查看他的账户细节。
然而,如果用户没有通过验证,系统会将他重定向到登录页面,以纠正他们的细节并再次尝试。
//handling sign in route
router.post('/login', (request, response, next) => {
passport.authenticate('local', {
successRedirect: '/home',
failureRedirect: '/users/login',
failureFlash: true
})(request, response, next);
});
确保路由安全
我们保护一个路由,使其不能被未认证的用户访问。在我们的案例中,唯一可以被保护的路由是home 。
用户需要经过认证才能访问这个页面上的资源。因此,首先,我们需要在config 文件夹中创建一个名为autheticate.js 的新文件,以保护该路由,然后添加下面的代码段。
module.exports = {
ensureUserIsAuthenticated: function(request, response, next) {
if (request.isAuthenticated()) {
return next();
}
request.flash('error_message', 'Please log in to access the requested page');
response.redirect('/users/login');
},
forwardAuthenticatedUser: function(request, response, next) {
if (!request.isAuthenticated()) {
return next();
}
response.redirect('/home');
}
};
在routes 文件夹中的index.js 文件中,添加下面的代码,以导入认证并保护home 路由。
router.get('/home', ensureUserIsAuthenticated, (request, response) => {
response.render('dashboard')
})
设置注销处理程序
注销处理程序负责签出用户并销毁登录用户的会话。
当用户登录时,他们的会话被存储在一个cookie中,注销处理程序将销毁该cookie。
//logout handler
router.get('/logout', (request, response) => {
request.logout();
request.flash('success_alert_message', 'You are succesfully logged out');
response.redirect('/users/login');
});

总结
本文向你展示了如何使用connect-flash来显示系统中的错误信息,将信息存储在express sessions中,并使用bootstrap来设计错误信息的样式。
我们通过建立一个基于Passport的完整认证系统来实现这些概念。
这个项目应该为积极从事任何node.js项目的认证模块提供一个开端。