在Node.js中使用Passport进行认证

2,659 阅读5分钟

对于开发者来说,从头开始实施应用程序认证是一个非常令人头痛的问题。而且,如果实现得不正确,认证过程会导致系统出现漏洞。

在这篇文章中,我们将使用Passport库MongoDB在Node.js应用程序中实现认证。

什么是Passport.js?

Passport是用于Node.js应用程序的一个流行的模块化认证中间件。有了它,认证可以轻松集成到任何基于Node和Express的应用程序中。Passport库提供了500多种认证机制,包括OAuth、JWT以及基于用户名和密码的简单认证。

使用Passport也可以很容易地将一种以上的认证类型集成到应用程序中。我们将在本文中使用mongoose-local 策略来实现认证。

创建Node应用程序的文件夹结构

首先,让我们为我们的文件创建特定的文件夹,像这样。

Node.js App Displaying Various Folders

在这里,routes 文件夹包含所有路由的文件。views 文件夹包含将被显示的ejs 文件,而layout 文件夹包含ejs 布局代码。

除此之外,我们有一个.env 文件来存储密钥,一个index.js 文件作为应用程序的起点,还有一个userDetails.js 文件用于Mongoose模式。

用Passport、passport-local-mongoose 和MongoDB构建一个认证系统是非常简单的,但是在继续构建应用程序之前,我们需要一个MongoDB集群。

你可以使用你的自我托管版本的MongoDB,或者你可以使用MongoDB Atlas。无论哪种情况,都要先创建一个MongoDB数据库,并在.env 文件中存储SRV URI。

初始化Node并安装软件包

一旦我们完成了数据库的创建,让我们用npm初始化这个文件夹。创建一个新的文件夹,并用npm init -y 来初始化它。

接下来,安装依赖项。下面是它们的列表。

  • express :我们将为我们的网络应用程序使用Express框架
  • mongoose :Node.js的MongoDB驱动将被用来连接MongoDB
  • ejs :我们的模板引擎
  • express-ejs-layouts :这将被用于布局。
  • dotenv :这个包将环境变量从一个名为.env 的文件中加载到 。process.env
  • connect-ensure-login :这将保护需要认证的页面。
  • passportpassport-local-mongoose: 用于实现认证
  • express-session :创建和管理会话

安装这个包时要注意。

npm i express mongoose ejs express-ejs-layouts dotenv connect-ensure-login passport passport-local-mongoose express-session

我们将使用nodemon 开发依赖性。使用npm i -D nodemon 安装 dev 依赖关系,然后用这两行修改package.json 文件中的scripts 部分。

"scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  }

创建视图和布局

因为我们要使用ejs 作为模板引擎,所以我们要使用express-ejs-layouts 包来建立我们的默认布局。

尽管安装这个插件是可选的,但在处理一个大项目时,它很方便。首先,在根目录下创建一个名为views 的文件夹,然后在views 目录内创建一个名为layout 的文件夹。

layout 目录内创建一个叫main.ejs 的文件。我在这个应用程序中使用BootstrapCSS来设计网页,所以我不需要写任何CSS。我不打算在这里解释HTML文件,因为它们非常简单明了,可以很容易理解。下面是main.ejs 文件的代码。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
        crossorigin="anonymous"></script>
    <title>
        <%- title %>
    </title>
</head>

<body>
    <%- body %>
</body>

</html>

在标题部分,我们已经使用CDN导入了Bootstrap的CSS和JavaScript。标题标签内的文本和正文将为每个视图而改变。

正因为如此,我们使用了<%- title %><%- body %> 字样。我们将从我们的routes 文件中传递title ,而body 将渲染HTML主体。

这就是需要添加到main.ejs 文件中的所有内容。让我告诉你其他三个页面的代码。

1.index.ejs

<div class="px-4 py-5 my-5 text-center">
    <img class="d-block mx-auto mb-4" src="https://uilogos.co/img/logomark/u-mark.png" alt="" width="auto" height="150">
    <h1 class="display-5 fw-bold">Your Login Page</h1>
    <div class="col-lg-6 mx-auto">
        <p class="lead mb-4">Lorem ipsum dolor sit amet consectetur, adipisicing elit. Quod, quidem. Distinctio,
            natus, recusandae nostrum beatae provident aut quasi sequi eos nemo et quia dolor ipsum reprehenderit
            molestiae id facere sunt.</p>
        <div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
            <a type="button" class="btn btn-primary btn-lg px-4 gap-3 me-2" href="/login">Log In</a>
        </div>
    </div>
</div>

这是我们渲染时的样子。

Login Page Created For Our App

2.login.ejs

<div class="px-4 py-5 my-5 text-center">
    <img class="d-block mx-auto mb-4" src="https://uilogos.co/img/logomark/u-mark.png" alt="" width="auto" height="150">
    <h1 class="display-5 fw-bold">Login Here</h1>
    <div class="col-lg-3 mx-auto">
        <form action="/login" method="POST">
            <div class="mb-2">
                <label for="username" class="form-label">Username</label>
                <input type="text" class="form-control" name="username" placeholder="Username" required>
            </div>
            <div class="mb-2">
                <label for="password" class="form-label">Password</label>
                <input type="password" class="form-control" name="password" placeholder="Password" required>
            </div>
            <button type="submit" class="btn btn-primary mt-2">Submit</button>
        </form>
    </div>
</div>

这里唯一需要注意的是,我们正在使用表单动作中的POST 方法打到login 路线。除此以外,它是一个简单的带有标签的HTML表单。登录页面将看起来像这样。

Username And Password Fill-In Created For App Login

3.secret.ejs

<div class="px-4 py-5 my-5 text-center">
    <img class="d-block mx-auto mb-4" src="https://uilogos.co/img/logomark/u-mark.png" alt="" width="auto" height="150">
    <h1 class="display-5 fw-bold">Welcome to the Secret Page</h1>
    <div class="col-lg-6 mx-auto">
        <p class="lead mb-4">You've Successfully Entered the Secret Page</p>
    </div>
    <div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
        <a href="/logout" type="button" class="btn btn-danger btn-lg px-4 gap-3">Log Out</a>
    </div>
</div>

秘密页面将看起来像这样。

Welcome Page To The Secret Page With Log Out Button

这个页面包含一个名为logout 的按钮,它将注销用户。
然而,这些页面都还不会呈现,因为我们还没有设置好我们的服务器。现在让我们来做这件事。

设置服务器

让我们在index.js 文件中导入软件包。

// Requiring Modules
const express = require('express');
var expressLayouts = require('express-ejs-layouts');
const app = express();

// set up view engine and layout
app.use(expressLayouts);
app.set('layout', './layout/main');
app.set('view engine', 'ejs');

app.use(express.urlencoded({ extended: false }));

const PORT = process.env.PORT || 3000;

// Set up express server
const server = app.listen(PORT, () => {
  console.log(`Listening on port ${PORT}`);
});

在这里,我们导入expressexpress-ejs-layouts 包。之后,我们用const app = express()app.use(expressLayouts) 来初始化expressexpress-ejs-layouts

app.set('layout', './layout/main') 是将main 文件设置为布局,app.set('view engine', 'ejs') 是将ejs 设置为模板引擎。app.use(express.urlencoded({ extended: false })) 作为正文分析器工作。最后,我们正在创建我们的服务器,端口为3000

现在,让我们创建一个名为routes 的新文件夹,并在该文件夹内,创建一个名为router.js 的新文件。

const express = require('express');
const router = express.Router();
const connectEnsureLogin = require('connect-ensure-login');
const passport = require('passport');

// GET Routes
router.get('/', (req, res) => {
  res.render('index', { title: 'Home' });
});

router.get('/login', (req, res) => {
  res.render('login', { title: 'Login' });
});

router.get('/secret', connectEnsureLogin.ensureLoggedIn(), (req, res) =>
  res.render('secret', { title: 'Secret Page' })
);

router.get('/logout', (req, res) => {
  req.logout();
  res.redirect('/');
});

// POST Routes
router.post(
  '/login',
  passport.authenticate('local', {
    failureRedirect: '/login',
    successRedirect: '/secret',
  }),
  (req, res) => {
    console.log(req.user);
  }
);

module.exports = router;

从上面的代码中可以看出,我们有三个GET 路线和一个POST 路线。首先,我们已经添加了必要的包。

secret 路由中的connectEnsureLogin.ensureLoggedIn() 中间件确保禁止用户在没有登录的情况下进入该页面。

POST 路由内,passport.authenticate 中间件通过local 策略来验证用户,如果用户成功登录,它将重定向到secret 路由。否则,它将重定向到login 路由。

我们还通过title 变量传递页面的标题。req.logout() 是一个将用户注销的护照方法。最后,我们正在导出路由器。

用MongoDB设置用户模式

在根目录下创建一个名为userDetails.js 的新文件。

const mongoose = require('mongoose');
const passportLocalMongoose = require('passport-local-mongoose');
require('dotenv').config();

// Connecting Mongoose
mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

// Setting up the schema
const User = new mongoose.Schema({
  username: String,
  password: String,
});

// Setting up the passport plugin
User.plugin(passportLocalMongoose);

module.exports = mongoose.model('User', User);

我们需要mongoose ,以便与MongoDB连接,而passport-local-mongoose 使得将用户名和密码认证与MongoDB整合起来变得非常容易。

我们设置了dotenv 包,以使用下一行的环境变量。

然后,我们使用mongoose连接到数据库。User 变量是持有mongoose模式的。User.plugin(passportLocalMongoose) 方法为每个用户生成并存储数据库中的哈希值、盐和用户名。最后,我们导出模式。

我们几乎已经完成了。我们只需要设置我们的index.js 文件。

在Node应用程序中初始化Passport

让我们导入Passportexpress-session 模块,router.js ,以及userDetails.js 文件。然后,使用express-session 包设置会话。

const passport = require('passport');
const session = require('express-session');
const UserDetails = require('./userDetails');
const routes = require('./routes/router');
require('dotenv').config();

// Set up session
app.use(
  session({
    secret: process.env.SECRET,
    resave: false,
    saveUninitialized: true,
  })
);

secret 被存储在.env 文件中,它签署了会话ID cookie。如果resave 标志被设置为true ,会话数据将被强行存储。我们不希望这样,因为当saveUninitialized 设置为true 时,会强行保存未初始化的会话。你可以在这里详细阅读关于该包的信息。

现在,通过添加以下几行来设置Passport。

// Set up Passport
app.use(passport.initialize());
app.use(passport.session());

我们首先初始化Passport和会话认证中间件。一旦完成,我们必须设置本地认证。

passport.use(UserDetails.createStrategy());
passport.serializeUser(UserDetails.serializeUser());
passport.deserializeUser(UserDetails.deserializeUser());

上面的代码为我们的Node应用程序添加了本地认证。首先,我们通过调用createStrategy 方法在UserDetails 模型上设置本地策略。

然后,serializeUser 方法在认证时对传递的用户实例进行序列化,并在每一个后续请求中调用deserializeUser 实例,以解除对用户的序列化。

现在在你的index 文件中添加这段代码,并只运行一次index.js

UserDetails.register({username:'nemo', active: false}, '123');

以上一行将注册一个用户名为nemo ,密码为123 的用户。如果你现在检查你的MongoDB数据库,你会看到这个用户。

最后的index.js 文件将看起来像这样。

// Requiring Modules
const express = require('express');
var expressLayouts = require('express-ejs-layouts');
const app = express();
const passport = require('passport');
const session = require('express-session');
const UserDetails = require('./userDetails');
const routes = require('./routes/router');
require('dotenv').config();

// Set up view engine and layout
app.use(expressLayouts);
app.set('layout', './layout/main');
app.set('view engine', 'ejs');

// Set up session
app.use(
  session({
    secret: process.env.SECRET,
    resave: false,
    saveUninitialized: true,
  })
);

app.use(express.urlencoded({ extended: false }));

// Set up Passport
app.use(passport.initialize());
app.use(passport.session());

passport.use(UserDetails.createStrategy());
passport.serializeUser(UserDetails.serializeUser());
passport.deserializeUser(UserDetails.deserializeUser());

app.use(routes);

// Set up Express server
const server = app.listen(3000, () => {
  console.log(`Listening on port ${server.address().port}`);
});

UserDetails.register({ username: 'nemo', active: false }, '123');

这就完成了认证。请看下面的GIF图,看看它的运行情况。

Logging In And Logging Out App

结论

认证是许多网络应用程序的一个重要组成部分。本文介绍了如何使用Passport库和MongoDB将认证集成到Node.js应用程序中。

你也可以查看Passport文档,了解在你的应用程序中实施的更多认证策略。我希望你喜欢阅读。完整的代码可以在这个GitHub repo中找到

The postUsing Passport for authentication in Node.jsappeared first onLogRocket Blog.