Express 實戰(八):利用 MongoDB 進行數據持久化

Cover
Cover

毫無疑問,幾乎全部的應用都會涉及到數據存儲。可是 Express 框架自己只能經過程序變量來保存數據,它並不提供數據持久化功能。而僅僅經過內存來保存數據是沒法應對真實場景的。由於內存自己並不適用於大規模的數據儲存並且服務中止後這些數據也會消失。雖然咱們還能夠經過文件的形式保存數據,可是文件中的數據對於查詢操做明顯不友好。全部,接下來咱們將學習如何在 Express 中經過 MongoDB 數據庫的形式來對數據進行持久化存儲。css

本文包含的主要內容有:html

  • MongoDB 是如何工做的。
  • 如何使用 Mongoose 。
  • 如何安全的建立用戶帳戶。
  • 如何使用用戶密碼進行受權操做。

爲何是 MongoDB ?

對於 Web 應用來講,一般數據庫的選擇能夠劃分爲兩大類:關係型和非關係型。其中前者優勢類型於電子表格,它的數據是結構化而且伴隨着嚴格規定。典型的關係型數據庫包括:MySQL、 SQL Server 以及 PostgreSQL。然後者一般也被稱爲 NoSQL 數據庫,它的結構相對更加靈活,而這一點與 JS 很是相似。node

可是爲何 Node 開發者會特別中意 NoSQL 中的 Mongo 數據庫,還造成了流行的 MEAN 技術棧呢?web

第一個緣由是:Mongo 是 NoSQL 類型數據裏最流行的一個。這也讓網上關於 Mogon 的資料很是豐富,全部你在實際使用過程當中可能會遇到的坑大概率都能找到答案。並且做爲一個成熟的項目,Mongo 也已經被大公司承認和應用。sql

另外一個緣由則是 Mongo 自身很是可靠、有特點。它使用高性能的 C++ 進行底層實現,也讓它贏得了大量的用戶信賴。mongodb

雖然 Mongo 不是用 JavaScript 實現的,可是原生的 shell 卻使用的是 JavaScript 語言。這意味着可使用 JavaScript 在控制檯操做 Mongo 。另外,對於 Node 開發者來講它也減小了學習新語言的成本。shell

固然,Mongo 並非全部 Express 應用的正確選擇,關係數據庫依然佔據着很是重要的地位。順便提一下,NoSQL 中的 CouchDB 功能也很是強大。數據庫

注意:雖然本文只會介紹 Mongo 以及 Mongoose 類庫的使用。可是若是你和我同樣對 SQL 很是熟悉而且但願在 Express 使用關係數據庫的話,你能夠去查看 Sequelize。它爲不少關係型數據庫提供了良好的支持。express

Mongo 是如何工做的

在正式使用 Mongo 前,咱們先來看看 Mongo 是如何工做的。npm

對於大多數應用來講都會在服務器中使用 Mongo 這樣的數據庫來進行持久化工做。雖然,你能夠在一個應用中建立多個數據庫,可是絕大多數都只會使用一個。

若是你想正常訪問這些數據庫的話,首先你須要運行一個 Mongo 服務。客戶端經過給服務端發送指令來實現對數據庫的各類操做。而鏈接客戶端與服務端的程序一般都被稱爲數據庫驅動。對於 Mongo 數據庫來講它在 Node 環境下的數據庫驅動程序是 Mongoose。

每一個數據庫都會有一個或多個相似於數組同樣的數據集合。例如,一個簡單的博客應用,可能就會有文章集合、用戶集合。可是這些數據集合的功能遠比數組來的強大。例如,你能夠查詢集合中 18 歲以上的用戶。

而每個集合裏面存儲了 JSON 形式的文檔,雖然在技術上並無採用 JSON。每個文檔都對應一條記錄,而每一條記錄都包含若干個字段屬性。另外,同一集合裏的文檔記錄並不必定擁有同樣的字段屬性。這也是 NoSQL 與 關係型數據庫最大的區別之一。

實際上文檔在技術上採用的是簡稱爲 BSON 的 Binary JSON。在實際寫代碼過程當中,咱們並不會直接操做 BSON 。多數狀況下會將其轉化爲 JavaScript 對象。另外,BSON 的編碼和解碼方式與 JSON 也有不一樣。BSON 支持的類型也更多,例如,它支持日期、時間戳。下圖展現了應用中數據庫使用結構:

08_01
08_01

最後還有一點很是重要:Mongo 會給每一個文檔記錄添加一個 _id 屬性,用於標示該記錄的惟一性。若是兩個同類型的文檔記錄的 id 屬性一致的話,那麼就能夠推斷它們是同一記錄。

SQL 使用者須要注意的問題

若是你有關係型數據庫的知識背景的話,其實你會發現 Mongo 不少概念是和 SQL 意義對應的。

首先, Mongo 中的文檔概念其實就至關於 SQL 中的一行記錄。在應用的用戶系統中,每個用戶在 Mongo 中是一個文檔而在 SQL 中則對應一條記錄。可是與 SQL 不一樣的是,在數據庫層 Mongo 並無強制的 schema,因此一條沒有用戶名和郵件地址的用戶記錄在 Mongo 中是合法的。

其次,Mongo 中的集合對應 SQL 中的表,它們都是用來存儲同一類型的記錄。

一樣,Mongo 中的數據庫也和 SQL 數據庫概念很是類似。一般一個應用只會有一個數據庫,而數據庫內部則能夠包含多個集合或者數據表。

更多的術語對應表能夠去查看官方的這篇文檔

Mongo 環境搭建

在使用以前,首要的任務固然就是機器上安裝 Mongo 數據庫並拉起服務了。若是你的機器是 macOS 系統而且不喜歡命令行模式的話,你能夠經過安裝 Mongo.app 應用完成環境搭建。若是你熟悉命令行交互的話能夠經過 Homebrew 命令 brew install mongodb 進行安裝。

Ubuntu 系統能夠參照文檔,同時 Debian 則能夠參照文檔 進行 Mongo 安裝。

另外,在本書中咱們會假設你安裝是使用的 Mongo 數據庫的默認配置。也就是說你沒有對 Mongo 的服務端口號進行修改而是使用了默認的 27017 。

使用 Mongoose 操做 Mongo 數據庫

安裝 Mongo 後接下來問題就是如何在 Node 環境中操做數據庫。這裏最佳的方式就是使用官方的 Mongoose類庫。其官方文檔描述爲:

Mongoose 提供了一個直觀並基於 schema 的方案來應對程序的數據建模、類型轉換、數據驗證等常見數據庫問題。

換句話說,除了充當 Node 和 Mongo 之間的橋樑以外,Mongoose 還提供了更多的功能。下面,咱們經過構建一個帶用戶系統的簡單網站來熟悉 Mongoose 的特性。

準備工做

爲了更好的學習本文的內容,下面咱們會開發一個簡單的社交應用。該應用將會實現用戶註冊、我的信息編輯、他人信息的瀏覽等功能。這裏咱們將它稱爲 Learn About Me 或者簡稱爲 LAM 。應用中主要包含如下頁面:

  • 主頁,用於列出全部的用戶而且能夠點擊查看用戶詳情。
  • 我的信息頁,用於展現用戶姓名等信息。
  • 用戶註冊頁。
  • 用戶登陸頁。

和以前同樣,首先咱們須要新建工程目錄並編輯 package.json 文件中的信息:

{
    "name": "learn-about-me",
    "private": true,
    "scripts": {
        "start": "node app"
    },
    "dependencies": {
        "bcrypt-nodejs": "0.0.3",
        "body-parser": "^1.6.5",
        "connect-flash": "^0.1.1",
        "cookie-parser": "^1.3.2",
        "ejs": "^1.0.0",
        "express": "^4.0.0",
        "express-session": "^1.7.6",
        "mongoose": "^3.8.15",
        "passport": "^0.2.0",
        "passport-local": "^1.0.0"
    }
}複製代碼

接下來,運行 npm install 安裝這些依賴項。在後面的內容中將會一一對這些依賴項的做用進行介紹。

須要注意的是,這裏咱們引入了一個純 JS 實現的加密模塊 bcrypt-nodejs 。其實 npm 中還有一個使用 C 語言實現的加密模塊 bcrypt 。雖然 bcrypt 性能更好,可是由於須要編譯 C 代碼全部安裝起來沒 bcrypt-nodejs 簡單。不過,這兩個類庫功能一致能夠進行自由切換。

建立 user 模型

前面說過 Mongo 是以 BSON 形式進行數據存儲的。例如,Hello World 的 BSON 表現形式爲:

\x16\x00\x00\x00\x02hello\x00\x06\x00\x00\x00world\x00\x00複製代碼

雖然計算機徹底可以理解 BSON 格式,可是很明顯 BSON 對人類來講並非一種易於閱讀的格式。所以,開發者發明了更易於理解的數據庫模型概念。數據庫模型以一種近似人類語言的方式對數據庫對象作出了定義。一個模型表明了一個數據庫記錄,一般也表明了編程語言中的對象。例如,這裏它就表明一個 JavaScript 對象。

除了表示數據庫的一條記錄以外,模型一般還伴隨數據驗證、數據拓展等方法。下面經過具體示例來見識下 Mongoose 中的這些特性。

在示例中,咱們將建立一個用戶模型,該模型帶有如下屬性:

  • 用戶名,該屬性沒法缺省且要求惟一。
  • 密碼,一樣沒法缺省。
  • 建立時間。
  • 用戶暱稱,用於信息展現且可選。
  • 我的簡介,非必須屬性。

在 Mongoose 中咱們使用 schema 來定義用戶模型。除了包含上面的屬性以外,以後還會在其中添加一些類型方法。在項目的根目錄建立 models 文件夾,而後在其中建立一個名爲 user.js 的文件並複製下面代碼:

var mongoose = require("mongoose");
var userSchema = mongoose.Schema({
    username: { type: String, require: true, unique: true },
    password: { type: String, require: true },
    createdAt: {type: Date, default: Date.now },
    displayName: String,
    bio: String
});複製代碼

從上面的代碼中,咱們能看到屬性字段的定義很是簡單。同時咱們還對字段的數據類型、惟一性、缺省、默認值做出了約定。

當模型定義好以後,接下來就是在模型中定義方法了。首先,咱們添加一個返回用戶名稱的簡單方法。若是用戶定義了暱稱則返回暱稱不然直接返回用戶名。代碼以下:

...

userSchema.methods.name = function() {
    return this.displayName || this.username;
}複製代碼

一樣,爲了確保數據庫中用戶信息安全,密碼字段必須以密文形式存儲。這樣即便出現數據庫泄露或者入侵行爲也能載必定程度上確保用戶信息的安全。這裏咱們將會使用對 Bcrypt 程序對用戶密碼進行單向哈希散列,而後在數據庫中存儲加密後的結果。

首先,咱們須要在 user.js 文件頭部引入 Bcrypt 類庫。在使用過程當中咱們能夠經過增長哈希次數來提升數據的安全性。固然,哈希操做是很是操做,因此咱們應該選取一個相對適中的數值。例如,下面的代碼中咱們將哈希次數設定爲了 10 。

var bcrypt = require("bcrypt-nodejs");
var SALT_FACTOR = 10;複製代碼

固然,對密碼的哈希操做應該在保存數據以前。因此這部分代碼應該在數據保存以前的回調函數中完成,代碼以下:

...

var noop = function() {};
// 保存操做以前的回調函數
userSchema.pre("save", function(done) {
    var user = this;
    if (!user.isModified("password")) {
        return done();
    }

    bcrypt.genSalt(SALT_FACTOR, function(err, salt) {
        if (err) { 
            return done(err); 
        }

        bcrypt.hash(user.password, salt, noop, 
            function(err, hashedPassword) {
                if (err) {
                    return done(err); 
                }
                user.password = hashedPassword;
                done();
            }
        );
    });
});複製代碼

該回調函數會在每次進行數據庫保存以前被調用,因此它能確保你的密碼會以密文形式獲得保存。

處理須要對密碼進行加密處理以外,另外一個常見需求就是用戶受權驗證了。例如,在用戶登陸操做時的密碼驗證操做。

...

userSchema.methods.checkPassword = function(guess, done) {
    bcrypt.compare(guess, this.password, function(err, isMatch) {
        done(err, isMatch);
    });
}複製代碼

出於安全緣由,這裏咱們使用的是 bcrypt.compare 函數而不是簡單的相等判斷 === 。

完成模型定義和通用方法實現後,接下來咱們就須要將其暴露出來供其餘代碼使用了。不過暴露模型的操做很是簡單隻需兩行代碼:

...

var User = mongoose.model("User", userSchema);
module.exports = User;複製代碼

models/user.js 文件中完整的代碼以下:

// 代碼清單 8.8 models/user.js編寫完成以後
var bcrypt = require("bcrypt-nodejs");
var SALT_FACTOR = 10;
var mongoose = require("mongoose");
var userSchema = mongose.Schema({
    username: { type: String, require: true, unique: true },
    password: { type: String, require: true },
    createdAt: {type: Date, default: Date.now },
    displayName: String,
    bio: String
});
userSchema.methods.name = function() {
    return this.displayName || this.username;
}

var noop = function() {};

userSchema.pre("save", function(done) {
    var user = this;
    if (!user.isModified("password")) {
        return done();
    }

    bcrypt.genSalt(SALT_FACTOR, function(err, salt) {
        if (err) { return done(err); }
        bcrypt.hash(user.password, salt, noop, 
            function(err, hashedPassword) {
                if (err) { return done(err); }
                user.password = hashedPassword;
                done();
            }
        );
    });
});
userSchema.methods.checkPassword = function(guess, done) {
    bcrypt.compare(guess, this.password, function(err, isMatch) {
        done(err, isMatch);
    });
}
var User = mongoose.model("User", userSchema);
module.exports = User;複製代碼

模型使用

模型定義好以後,接下來就是在主頁、編輯頁面、註冊等頁面進行使用了。相比於以前的模型定義,使用過程相對來講要更簡單。

首先,在項目根目錄建立主入口文件 app.js 並複製下面的代碼:

var express = require("express");
var mongoose = require("mongoose");
var path = require("path");
var bodyParser = require("body-parser");
var cookieParser = require("cookie-parser");
var session = require("express-session");
var flash = require("connect-flash");

var routes = require("./routes");
var app = express();

// 鏈接到你MongoDB服務器的test數據庫
mongoose.connect("mongodb://localhost:27017/test");
app.set("port", process.env.PORT || 3000);
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");

app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(session({
    secret: "TKRv0IJs=HYqrvagQ#&!F!%V]Ww/4KiVs$s,<<MX",
    resave: true,
    saveUninitialized: true
}));
app.use(flash());
app.use(routes);

app.listen(app.get("port"), function() {
    console.log("Server started on port " + app.get("port"));
});複製代碼

接下來,咱們須要實現上面使用到的路由中間件。在根目錄新建 routes.js 並複製代碼:

var express = require("express");
var User = require("./models/user");
var router = express.Router();
router.use(function(req, res, next) {
    res.locals.currentUser = req.user;
    res.locals.errors = req.flash("error");
    res.locals.infos = req.flash("info");
    next();
});
router.get("/", function(req, res, next) {
    User.find()
        .sort({ createdAt: "descending" })
        .exec(function(err, users) {
            if (err) { return next(err); }
            res.render("index", { users: users });
        });
});
module.exports = router;複製代碼

這兩段代碼中,首先,咱們使用 Mongoose 進行了數據庫鏈接。而後,在路由中間件中經過 User.find 異步獲取用戶列表並將其傳遞給了主頁視圖模版。

接下來,咱們就輪到主頁視圖的實現了。首先在根目錄建立 views 文件夾,而後在文件夾中添加第一個模版文件 _header.ejs

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Learn About Me</title>
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
</head>
<body>
    <div class="navbar navbar-default navbar-static-top" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <a class="navbar-brand" href="/">Learn About Me</a>
            </div>
            <!-- 
                若是用戶已經登錄了則對導航條進行相應的改變。
                一開始你的代碼中並不存在currentUser,因此總會顯示一個狀態
             -->
            <ul class="nav navbar-nav navbar-right">
                <% if (currentUser) { %>
                    <li>
                        <a href="/edit">
                            Hello, <%= currentUser.name() %>
                        </a>
                    </li>
                    <li><a href="/logout">Log out</a></li>
                 <% } else { %>
                    <li><a href="/login">Log in</a></li>
                    <li><a href="/signup">Sign up</a></li>  
                 <% } %>   
            </ul>
        </div>
    </div>
    <div class="container">
        <% errors.forEach(function(error) { %>
            <div class="alert alert-danger" role="alert">
                <%= error %>
            </div>
        <% }) %>
        <% infos.forEach(function(info) { %>
            <div class="alert alert-info" role="alert">
                <%= info %>
            </div>
        <% }) %>複製代碼

你可能注意到了這些文件的名字是如下劃線開始的。這是一個社區約定,全部組件模版都會如下劃線進行區分。

接下來,添加第二個通用組件模版 _footer.js

</div>
</body>
</html>複製代碼

最後,咱們添加主頁視圖模版文件。該視圖模版會接受中間件中傳入的 users 變量並完成渲染:

<% include _header %>
<h1>Welcome to Learn About Me!</h1>
<% users.forEach(function(user) { %>
    <div class="panel panel-default">
        <div class="panel-heading">
            <a href="/users/<%= user.username %>">
                <%= user.name() %>
            </a>
        </div>
        <% if (user.bio) { %>
            <div class="panel-body"><%= user.bio %></div>
        <% } %>
    </div>
<% }) %>
<% include _footer %>複製代碼

確保代碼無誤後,接下來啓動 Mongo 數據庫服務並使用 npm start 拉起工程。而後,經過瀏覽器訪問 localhost:3000 就能類型下圖的主頁界面:

08_02
08_02

固然,由於此時數據庫中並無任何記錄全部這裏並無出現任何用戶信息。

接下來,咱們就來實現用戶用戶註冊和登陸功能。不過在此以前,咱們須要在 app.js 中引入 body-parser 模塊並用於後面請求參數的解析。

var bodyParser = require("body-parser");
...

app.use(bodyParser.urlencoded({ extended: false }));
…複製代碼

爲了提升安全性,這裏咱們將 body-parser 模塊的 extended 設置爲 false 。接下來,咱們在 routes.js 添加 sign-up 功能的中間件處理函數:

var passport = require("passport");
...
router.get("/signup", function(req, res) {
    res.render("signup");
});
router.post("/signup", function(req, res, next) {
    // 參數解析
    var username = req.body.username;
    var password = req.body.password;

    // 調用findOne只返回一個用戶。你想在這匹配一個用戶名
    User.findOne({ username: username }, function(err, user) {
        if (err) { return next(err); }
        // 判斷用戶是否存在
        if (user) {
            req.flash("error", "User already exists");
            return res.redirect("/signup");
        }
        // 新建用戶
        var newUser = new User({
            username: username,
            password: password
        });
        // 插入記錄
        newUser.save(next);
    });
    // 進行登陸操做並實現重定向
}, passport.authenticate("login", {
    successRedirect: "/",
    failureRedirect: "/signup",
    failureFlash: true
}));複製代碼

路由中間件定義完成後,下面咱們就來實現視圖模版 signup.ejs 文件。

// 拷貝代碼到 views/signup.ejs
<% include _header %>
<h1>Sign up</h1>
<form action="/signup" method="post">
    <input name="username" type="text" class="form-control" placeholder="Username" required autofocus>
    <input name="password" type="password" class="form-control" placeholder="Password" required>
    <input type="submit" value="Sign up" class="btn btn-primary btn-block">
</form>
<% include _footer %>複製代碼

若是你成功建立用戶並再次訪問主頁的話,你就能看見一組用戶列表:

08_03
08_03

而註冊頁的 UI 大體以下:

08_04
08_04

在實現登陸功能以前,咱們先把我的信息展現功能先補充完整。在 routes.js 添加以下中間件函數:

...
router.get("/users/:username", function(req, res, next) {
    User.findOne({ username: req.params.username }, function(err, user) {
        if (err) { return next(err); }
        if (!user) { return next(404); }
        res.render("profile", { user: user });
    });
});
...複製代碼

接下來編寫視圖模版文件 profile.ejs

// 保存到 views 文件夾中
<% include _header %>
<!-- 
    參考變量currentUser來判斷你的登錄狀態。不過如今它總會是false狀態
 -->
<% if ((currentUser) && (currentUser.id === user.id)) { %>
    <a href="/edit" class="pull-right">Edit your profile</a>
<% } %>
<h1><%= user.name() %></h1>
<h2>Joined on <%= user.createdAt %></h2>
<% if (user.bio) { %>
    <p><%= user.bio %></p>
<% } %>
<% include _footer %>複製代碼

若是如今你經過首頁進入用戶詳情頁話,那麼你就會出現相似下圖的界面:

08_05
08_05

經過 Passport 來進行用戶身份驗證

除了上面這些基本功能以外,User 模型作重要的功能實際上是登陸以及權限認證。而這也是 User 模型與其餘模型最大的區別。因此接下來的任務就是實現登陸頁並進行密碼和權限認證。

爲了減小不少沒必要要的工做量,這裏咱們會使用到第三方的 Passport 模塊。該模版是特意爲請求進行驗證而設計處理的 Node 中間件。經過該中間件只需一小段代碼就能實現複雜的身份認證操做。不過 Passport 並無指定如何進行用戶身份認證,它只是提供了一些模塊化函數。

設置 Passport

Passport 的設置過程主要有三件事:

  • 設置 Passport 中間件。
  • 設置 Passport 對 User 模型的序列化和反序列化的操做。
  • 告訴 Passport 如何對 User 進行認證。

首先,在初始化 Passport 環境時,你須要在工程中引入一些其餘中間件。它們分別爲:

  1. body-parser
  2. cookie-parser
  3. express-session
  4. connect-flash
  5. passport.initialize
  6. passport.session

其中前面 4 箇中間件已經引入過了。它們的做用分別爲: body-parser 用於參數解析;cookie-parser 處理從瀏覽器中獲取的cookies;express-session 用於處理用戶 session;而 connect-flash 則用戶展現錯誤信息。

最後,咱們須要在 app.js 中引入 Passport 模塊並在後面調用其中的兩個中間件函數。

var bodyParser = require("body-parser");
var cookieParser = require("cookie-parser");
var flash = require("connect-flash");
var passport = require("passport");
var session = require("express-session");
...
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(session({
    // 須要一串隨機字母序列,字符串不必定須要跟此處同樣
    secret: "TKRv0IJs=HYqrvagQ#&!F!%V]Ww/4KiVs$s,<<MX",
    resave: true,
    saveUninitialized: true
}));
app.use(flash());
app.use(passport.initialize());
app.use(passport.session());
...複製代碼

代碼中,咱們使用一串隨機字符串來對客戶端的 session 進行編碼。這樣就能在必定程度上增長 cookies 的安全性。而將 resave 設置爲 true 則保證了即便 session 沒有被修改也依然會被刷新。

接下來就是第二步操做:設置 Passport 對 User 模型的序列化和反序列化操做了。這樣 Passport 就能實現 session 和 user 對象的互相轉化了。Passport 文檔對這一操做的描述爲:

在標準的 web 應用中,只有當客戶端發送了登陸請求才會須要對用戶進行身份認證。若是認證經過的話,二者之間就會新建一個 session 並將其保存到 cookie 中進行維護。任何後續操做都不會再進行認證操做,取而代之的是使用 cookie 中惟一指定的 session 。因此,Passport 須要經過序列化和反序列化實現 session 和 user 對象的互相轉化。

爲了後期代碼維護方便,這裏咱們新建一個名爲 setuppassport.js 的文件並將序列化和反序列化的代碼放入其中。最後,咱們將其引入到 app.js 中:

…
var setUpPassport = require("./setuppassport");
…
var app = express();
mongoose.connect("mongodb://localhost:27017/test");
setUpPassport();
…複製代碼

下面就是 setuppassport.js 中的代碼實現了。由於 User 對象都有一個 id 屬性做爲惟一標識符,因此咱們就根據它來進行 User 對象的序列化和反序列化操做:

// setuppassport.js 文件中的代碼
var passport = require("passport");
var User = require("./models/user");
module.exports = function() {
    passport.serializeUser(function(user, done) {
        done(null, user._id);
    });
    passport.deserializeUser(function(id, done) {
        User.findById(id, function(err, user) {
            done(err, user);
        });
    });
}複製代碼

接下來就是最難的部分了,如何進行身份認證?

在開始進行認證前,還有一個小工做須要完成:設置認證策略。雖然 Passport 附帶了 Facebook 、Google 的身份認證策略,可是這裏咱們須要的將其設置爲 local strategy 。由於驗證部分的規則和代碼是由咱們本身來實現的。

首先,咱們在 setuppassport.js 中引入 LocalStrategy

...
var LocalStrategy = require("passport-local").Strategy;
…複製代碼

接下來,按照下面的步驟使用 LocalStrategy 來進行具體的驗證:

  1. 查詢該用戶。
  2. 用戶不存在則提示沒法經過驗證。
  3. 用戶存在則進行密碼比較。若是匹配成功則返回當前用戶不然提示「密碼錯誤」。

下面就是將這些步驟轉化爲具體的代碼:

// setuppassport.js 驗證代碼
...
passport.use("login", new LocalStrategy(function(username, password, done) {
    User.findOne({ username: username }, function(err, user) {
        if(err) { return done(err); }
        if (!user) {
            return done(null, false, { message: "No user has that username!" });
        }

        user.checkPassword(password, function(err, isMatch) {
            if (err) { return done(err); }
            if (isMatch) {
                return done(null, user);
            } else {
                return done(null, false, { message: "Invalid password." });
            }
        });
    });
}));
...複製代碼

完成策略定義後,接下來就能夠在項目的任何地方進行調用。

最後,咱們還須要完成一些視圖和功能:

  1. 登陸
  2. 登出
  3. 登陸完成後的我的信息編輯

首先,咱們實現登陸界面視圖。在 routes.js 中添加登陸路由中間件:

...
router.get("/login", function(req, res) {
    res.render("login");
});
...複製代碼

在登陸視圖 login.ejs 中,咱們會接收一個用戶名和一個密碼,而後發送登陸的 POST 請求:

<% include _header %>
<h1>Log in</h1>
<form action="/login" method="post">
    <input name="username" type="text" class="form-control" placeholder="Username" required autofocus>
    <input name="password" type="password" class="form-control" placeholder="Password" required>
    <input type="submit" value="Log in" class="btn btn-primary btn-block">
</form>
<% include _footer %>複製代碼

接下來,咱們就須要處理該 POST 請求。其中就會使用到 Passport 的身份認證函數。

//  routes.js 中登錄功能代碼
var passport = require("passport");
...

router.post("/login", passport.authenticate("login", {
    successRedirect: "/",
    failureRedirect: "/login",
    failureFlash: true 
}));
...複製代碼

其中 passport.authenticate 函數會返回一個回調。該函數會根據咱們的指定對不一樣的驗證結果分別進行重定向。例如,登陸成功會重定向到首頁,而失敗則會重定向到登陸頁。

登出操做相對來講要簡單得多,代碼以下

// routes.js 登出部分
...
router.get("/logout", function(req, res) {
    req.logout();
    res.redirect("/");
});
...複製代碼

Passport 還附加了 req.user 和 connect-flash 信息。再回顧一下前面的這段代碼,相信你能有更深的體會。

...
router.use(function(req, res, next) {
    // 爲你的模板設置幾個有用的變量
    res.locals.currentUser = req.user;
    res.locals.errors = req.flash("error");
    res.locals.infos = req.flash("info");
    next();
});
...複製代碼

登陸和登出玩抽,下面就該輪到我的信息編輯功能了。

首先,咱們來實現一個通用的中間件工具函數 ensureAuthenticated 。該中間件函數會對當前用戶的權限進行檢查,若是檢查不經過則會重定向到登陸頁。

// routes.js 中的 ensureAuthenticated 中間件
...
function ensureAuthenticated(req, res, next) {
    // 一個Passport提供的函數
    if (req.isAuthenticated()) {
        next();
    } else {
        req.flash("info", "You must be logged in to see this page.");
        res.redirect("/login");
    }
}
...複製代碼

接下來,咱們會在編輯中間件中調用該函數。由於咱們須要確保在開始編輯以前,當前用戶擁有編輯權限。

//  GET /edit(在router.js中)
...
// 確保用戶被身份認證;若是它們沒有被重定向的話則運行你的請求處理
router.get("/edit", ensureAuthenticated, function(req, res) {
    res.render("edit");
});
...複製代碼

接下來咱們須要實現 edit.ejs 視圖模版文件。該視圖模版的內容很是簡單,只包含用戶暱稱和簡介的修改。

//  views/edit.ejs
<% include _header %>
<h1>Edit your profile</h1>
<form action="/edit" method="post">
<input name="displayname" type="text" class="form-control" placeholder="Display name" value="<%= currentUser.displayName || "" %>">
<textarea name="bio" class="form-control" placeholder="Tell us about yourself!"> <%= currentUser.bio || "" %></textarea>
<input type="submit" value="Update" class="btn btn-primary btn-block">
</form>
<% include _footer %>複製代碼

最後,咱們須要對修改後提交的請求做出處理。在進行數據庫更新以前,這裏一樣須要進行權限認證。

// POST /edit(在routes.js中)
...
// 一般,這會是一個PUT請求,不過HTML表單僅僅支持GET和POST
router.post("/edit", ensureAuthenticated, function(req, res, next) {
    req.user.displayName = req.body.displayname;
    req.user.bio = req.body.bio;
    req.user.save(function(err) {
        if (err) {
            next(err);
            return;
        }
        req.flash("info", "Profile updated!");
        res.redirect("/edit");
    });
});
...複製代碼

該代碼僅僅只是對數據庫對應記錄的字段進行了更新。最終渲染的編輯視圖以下:

08_06
08_06

最後,你能夠建立一些測試數據對示例應用的全部功能進行一遍驗證。

08_07
08_07

總結

本文包含的內容有:

  • Mongo 的工做原理。
  • Mongoose 的使用。
  • 使用 bcrypt 對特定字段進行加密來提升數據安全性。
  • 使用 Passport 進行權限認證。

原文地址

相關文章
相關標籤/搜索