Express 實戰(八):利用 MongoDB 進行資料持久化

BigNerdCoding發表於2017-09-16

Cover
Cover

毫無疑問,幾乎所有的應用都會涉及到資料儲存。但是 Express 框架本身只能通過程式變數來儲存資料,它並不提供資料持久化功能。而僅僅通過記憶體來儲存資料是無法應對真實場景的。因為記憶體本身並不適用於大規模的資料儲存而且服務停止後這些資料也會消失。雖然我們還可以通過檔案的形式儲存資料,但是檔案中的資料對於查詢操作明顯不友好。所有,接下來我們將學習如何在 Express 中通過 MongoDB 資料庫的形式來對資料進行持久化儲存。

本文包含的主要內容有:

  • MongoDB 是如何工作的。
  • 如何使用 Mongoose 。
  • 如何安全的建立使用者賬戶。
  • 如何使用使用者密碼進行授權操作。

為什麼是 MongoDB ?

對於 Web 應用來說,通常資料庫的選擇可以劃分為兩大類:關係型和非關係型。其中前者優點型別於電子表格,它的資料是結構化並且伴隨著嚴格規定。典型的關係型資料庫包括:MySQL、 SQL Server 以及 PostgreSQL。而後者通常也被稱為 NoSQL 資料庫,它的結構相對更加靈活,而這一點與 JS 非常類似。

但是為什麼 Node 開發者會特別中意 NoSQL 中的 Mongo 資料庫,還形成了流行的 MEAN 技術棧呢?

第一個原因是:Mongo 是 NoSQL 型別資料裡最流行的一個。這也讓網上關於 Mogon 的資料非常豐富,所有你在實際使用過程中可能會遇到的坑大機率都能找到答案。而且作為一個成熟的專案,Mongo 也已經被大公司認可和應用。

另一個原因則是 Mongo 自身非常可靠、有特色。它使用高效能的 C++ 進行底層實現,也讓它贏得了大量的使用者信賴。

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

當然,Mongo 並不是所有 Express 應用的正確選擇,關聯式資料庫依然佔據著非常重要的地位。順便提一下,NoSQL 中的 CouchDB 功能也非常強大。

注意:雖然本文只會介紹 Mongo 以及 Mongoose 類庫的使用。但是如果你和我一樣對 SQL 非常熟悉並且希望在 Express 使用關聯式資料庫的話,你可以去檢視 Sequelize。它為很多關係型資料庫提供了良好的支援。

Mongo 是如何工作的

在正式使用 Mongo 前,我們先來看看 Mongo 是如何工作的。

對於大多數應用來說都會在伺服器中使用 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 進行許可權認證。

原文地址

相關文章