如何使用Flash、Express Sessions和Bootstrap处理认证问题

100 阅读8分钟

使用Flash、Express Sessions和Bootstrap处理认证问题

本文探讨了如何使用Passport认证模块来处理认证,用适当的Bootstrap风格向用户显示Flash消息,并将消息存储在会话中,以便在网页导航中正确显示它们。

我们将在一个应用程序中使用express-sessionconnect-flashpassportbootstrap 等模块构建一个认证系统。

前提条件

要跟上进度,读者应该具备以下条件。

  • 对[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.jsusers.js 的文件。认证请求将转到user.js ,而其他请求将转到index.js 路由。

创建另一个名为views 的文件夹。这个文件夹将包含我们的视图文件,这些文件将被呈现给屏幕上的用户。

routes 目录中,添加以下文件;login.ejs register.ejs layout.ejsdashboard.ejs

当用户导航到项目的索引路线时,系统会将他/她引导到欢迎页面,该页面会渲染welcome.ejs 文件。

在那里,他选择登录或注册一个新账户。一旦用户成功注册,他就可以登录,然后他就被重定向到仪表板页面。

login.ejs ,我们有一个表单,提交user emailpassword 进行认证,如下图所示。

 <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.jsconfig.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)
            })
        }
    })
})

Registration validation

实现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">&times;</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">&times;</span>
      </button>
    </div>
<% } %>

Login page redirect

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 strategymongoose ,以便在数据库中查找用户。

在这种情况下,我们使用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');
});

Log out

总结

本文向你展示了如何使用connect-flash来显示系统中的错误信息,将信息存储在express sessions中,并使用bootstrap来设计错误信息的样式。

我们通过建立一个基于Passport的完整认证系统来实现这些概念。

这个项目应该为积极从事任何node.js项目的认证模块提供一个开端。