成人无码视频,亚洲精品久久久久av无码,午夜精品久久久久久毛片,亚洲 中文字幕 日韩 无码

資訊專欄INFORMATION COLUMN

Express 實戰(zhàn)(八):利用 MongoDB 進(jìn)行數(shù)據(jù)持久化

yanbingyun1990 / 1869人閱讀

摘要:在使用過程中我們可以通過增加哈希次數(shù)來提高數(shù)據(jù)的安全性。當(dāng)然,對密碼的哈希操作應(yīng)該在保存數(shù)據(jù)之前。

毫無疑問,幾乎所有的應(yīng)用都會涉及到數(shù)據(jù)存儲。但是 Express 框架本身只能通過程序變量來保存數(shù)據(jù),它并不提供數(shù)據(jù)持久化功能。而僅僅通過內(nèi)存來保存數(shù)據(jù)是無法應(yīng)對真實場景的。因為內(nèi)存本身并不適用于大規(guī)模的數(shù)據(jù)儲存而且服務(wù)停止后這些數(shù)據(jù)也會消失。雖然我們還可以通過文件的形式保存數(shù)據(jù),但是文件中的數(shù)據(jù)對于查詢操作明顯不友好。所有,接下來我們將學(xué)習(xí)如何在 Express 中通過 MongoDB 數(shù)據(jù)庫的形式來對數(shù)據(jù)進(jìn)行持久化存儲。

本文包含的主要內(nèi)容有:

MongoDB 是如何工作的。

如何使用 Mongoose 。

如何安全的創(chuàng)建用戶賬戶。

如何使用用戶密碼進(jìn)行授權(quán)操作。

為什么是 MongoDB ?

對于 Web 應(yīng)用來說,通常數(shù)據(jù)庫的選擇可以劃分為兩大類:關(guān)系型和非關(guān)系型。其中前者優(yōu)點類型于電子表格,它的數(shù)據(jù)是結(jié)構(gòu)化并且伴隨著嚴(yán)格規(guī)定。典型的關(guān)系型數(shù)據(jù)庫包括:MySQL、 SQL Server 以及 PostgreSQL。而后者通常也被稱為 NoSQL 數(shù)據(jù)庫,它的結(jié)構(gòu)相對更加靈活,而這一點與 JS 非常類似。

但是為什么 Node 開發(fā)者會特別中意 NoSQL 中的 Mongo 數(shù)據(jù)庫,還形成了流行的 MEAN 技術(shù)棧呢?

第一個原因是:Mongo 是 NoSQL 類型數(shù)據(jù)里最流行的一個。這也讓網(wǎng)上關(guān)于 Mogon 的資料非常豐富,所有你在實際使用過程中可能會遇到的坑大幾率都能找到答案。而且作為一個成熟的項目,Mongo 也已經(jīng)被大公司認(rèn)可和應(yīng)用。

另一個原因則是 Mongo 自身非常可靠、有特色。它使用高性能的 C++ 進(jìn)行底層實現(xiàn),也讓它贏得了大量的用戶信賴。

雖然 Mongo 不是用 JavaScript 實現(xiàn)的,但是原生的 shell 卻使用的是 JavaScript 語言。這意味著可以使用 JavaScript 在控制臺操作 Mongo 。另外,對于 Node 開發(fā)者來說它也減少了學(xué)習(xí)新語言的成本。

當(dāng)然,Mongo 并不是所有 Express 應(yīng)用的正確選擇,關(guān)系數(shù)據(jù)庫依然占據(jù)著非常重要的地位。順便提一下,NoSQL 中的 CouchDB 功能也非常強(qiáng)大。

注意:雖然本文只會介紹 Mongo 以及 Mongoose 類庫的使用。但是如果你和我一樣對 SQL 非常熟悉并且希望在 Express 使用關(guān)系數(shù)據(jù)庫的話,你可以去查看 Sequelize。它為很多關(guān)系型數(shù)據(jù)庫提供了良好的支持。

Mongo 是如何工作的

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

對于大多數(shù)應(yīng)用來說都會在服務(wù)器中使用 Mongo 這樣的數(shù)據(jù)庫來進(jìn)行持久化工作。雖然,你可以在一個應(yīng)用中創(chuàng)建多個數(shù)據(jù)庫,但是絕大多數(shù)都只會使用一個。

如果你想正常訪問這些數(shù)據(jù)庫的話,首先你需要運行一個 Mongo 服務(wù)??蛻舳送ㄟ^給服務(wù)端發(fā)送指令來實現(xiàn)對數(shù)據(jù)庫的各種操作。而連接客戶端與服務(wù)端的程序通常都被稱為數(shù)據(jù)庫驅(qū)動。對于 Mongo 數(shù)據(jù)庫來說它在 Node 環(huán)境下的數(shù)據(jù)庫驅(qū)動程序是 Mongoose。

每個數(shù)據(jù)庫都會有一個或多個類似于數(shù)組一樣的數(shù)據(jù)集合。例如,一個簡單的博客應(yīng)用,可能就會有文章集合、用戶集合。但是這些數(shù)據(jù)集合的功能遠(yuǎn)比數(shù)組來的強(qiáng)大。例如,你可以查詢集合中 18 歲以上的用戶。

而每一個集合里面存儲了 JSON 形式的文檔,雖然在技術(shù)上并沒有采用 JSON。每一個文檔都對應(yīng)一條記錄,而每一條記錄都包含若干個字段屬性。另外,同一集合里的文檔記錄并不一定擁有一樣的字段屬性。這也是 NoSQL 與 關(guān)系型數(shù)據(jù)庫最大的區(qū)別之一。

實際上文檔在技術(shù)上采用的是簡稱為 BSON 的 Binary JSON。在實際寫代碼過程中,我們并不會直接操作 BSON 。多數(shù)情況下會將其轉(zhuǎn)化為 JavaScript 對象。另外,BSON 的編碼和解碼方式與 JSON 也有不同。BSON 支持的類型也更多,例如,它支持日期、時間戳。下圖展示了應(yīng)用中數(shù)據(jù)庫使用結(jié)構(gòu):

最后還有一點非常重要:Mongo 會給每個文檔記錄添加一個 _id 屬性,用于標(biāo)示該記錄的唯一性。如果兩個同類型的文檔記錄的 id 屬性一致的話,那么就可以推斷它們是同一記錄。

SQL 使用者需要注意的問題

如果你有關(guān)系型數(shù)據(jù)庫的知識背景的話,其實你會發(fā)現(xiàn) Mongo 很多概念是和 SQL 意義對應(yīng)的。

首先, Mongo 中的文檔概念其實就相當(dāng)于 SQL 中的一行記錄。在應(yīng)用的用戶系統(tǒng)中,每一個用戶在 Mongo 中是一個文檔而在 SQL 中則對應(yīng)一條記錄。但是與 SQL 不同的是,在數(shù)據(jù)庫層 Mongo 并沒有強(qiáng)制的 schema,所以一條沒有用戶名和郵件地址的用戶記錄在 Mongo 中是合法的。

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

同樣,Mongo 中的數(shù)據(jù)庫也和 SQL 數(shù)據(jù)庫概念非常相似。通常一個應(yīng)用只會有一個數(shù)據(jù)庫,而數(shù)據(jù)庫內(nèi)部則可以包含多個集合或者數(shù)據(jù)表。

更多的術(shù)語對應(yīng)表可以去查看官方的這篇文檔。

Mongo 環(huán)境搭建

在使用之前,首要的任務(wù)當(dāng)然就是機(jī)器上安裝 Mongo 數(shù)據(jù)庫并拉起服務(wù)了。如果你的機(jī)器是 macOS 系統(tǒng)并且不喜歡命令行模式的話,你可以通過安裝 Mongo.app 應(yīng)用完成環(huán)境搭建。如果你熟悉命令行交互的話可以通過 Homebrew 命令 brew install mongodb 進(jìn)行安裝。

Ubuntu 系統(tǒng)可以參照文檔,同時 Debian 則可以參照文檔 進(jìn)行 Mongo 安裝。

另外,在本書中我們會假設(shè)你安裝是使用的 Mongo 數(shù)據(jù)庫的默認(rèn)配置。也就是說你沒有對 Mongo 的服務(wù)端口號進(jìn)行修改而是使用了默認(rèn)的 27017 。

使用 Mongoose 操作 Mongo 數(shù)據(jù)庫

安裝 Mongo 后接下來問題就是如何在 Node 環(huán)境中操作數(shù)據(jù)庫。這里最佳的方式就是使用官方的 [Mongoose] [6]類庫。其官方文檔描述為:

Mongoose 提供了一個直觀并基于 schema 的方案來應(yīng)對程序的數(shù)據(jù)建模、類型轉(zhuǎn)換、數(shù)據(jù)驗證等常見數(shù)據(jù)庫問題。

換句話說,除了充當(dāng) Node 和 Mongo 之間的橋梁之外,Mongoose 還提供了更多的功能。下面,我們通過構(gòu)建一個帶用戶系統(tǒng)的簡單網(wǎng)站來熟悉 Mongoose 的特性。

準(zhǔn)備工作

為了更好的學(xué)習(xí)本文的內(nèi)容,下面我們會開發(fā)一個簡單的社交應(yīng)用。該應(yīng)用將會實現(xiàn)用戶注冊、個人信息編輯、他人信息的瀏覽等功能。這里我們將它稱為 Learn About Me 或者簡稱為 LAM 。應(yīng)用中主要包含以下頁面:

主頁,用于列出所有的用戶并且可以點擊查看用戶詳情。

個人信息頁,用于展示用戶姓名等信息。

用戶注冊頁。

用戶登錄頁。

和之前一樣,首先我們需要新建工程目錄并編輯 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 安裝這些依賴項。在后面的內(nèi)容中將會一一對這些依賴項的作用進(jìn)行介紹。

需要注意的是,這里我們引入了一個純 JS 實現(xiàn)的加密模塊 bcrypt-nodejs 。其實 npm 中還有一個使用 C 語言實現(xiàn)的加密模塊 bcrypt 。雖然 bcrypt 性能更好,但是因為需要編譯 C 代碼所有安裝起來沒 bcrypt-nodejs 簡單。不過,這兩個類庫功能一致可以進(jìn)行自由切換。

創(chuàng)建 user 模型

前面說過 Mongo 是以 BSON 形式進(jìn)行數(shù)據(jù)存儲的。例如,Hello World 的 BSON 表現(xiàn)形式為:

x16x00x00x00x02hellox00x06x00x00x00worldx00x00

雖然計算機(jī)完全能夠理解 BSON 格式,但是很明顯 BSON 對人類來說并不是一種易于閱讀的格式。因此,開發(fā)者發(fā)明了更易于理解的數(shù)據(jù)庫模型概念。數(shù)據(jù)庫模型以一種近似人類語言的方式對數(shù)據(jù)庫對象做出了定義。一個模型代表了一個數(shù)據(jù)庫記錄,通常也代表了編程語言中的對象。例如,這里它就代表一個 JavaScript 對象。

除了表示數(shù)據(jù)庫的一條記錄之外,模型通常還伴隨數(shù)據(jù)驗證、數(shù)據(jù)拓展等方法。下面通過具體示例來見識下 Mongoose 中的這些特性。

在示例中,我們將創(chuàng)建一個用戶模型,該模型帶有以下屬性:

用戶名,該屬性無法缺省且要求唯一。

密碼,同樣無法缺省。

創(chuàng)建時間。

用戶昵稱,用于信息展示且可選。

個人簡介,非必須屬性。

在 Mongoose 中我們使用 schema 來定義用戶模型。除了包含上面的屬性之外,之后還會在其中添加一些類型方法。在項目的根目錄創(chuàng)建 models 文件夾,然后在其中創(chuàng)建一個名為 user.js 的文件并復(fù)制下面代碼:

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
});

從上面的代碼中,我們能看到屬性字段的定義非常簡單。同時我們還對字段的數(shù)據(jù)類型、唯一性、缺省、默認(rèn)值作出了約定。

當(dāng)模型定義好之后,接下來就是在模型中定義方法了。首先,我們添加一個返回用戶名稱的簡單方法。如果用戶定義了昵稱則返回昵稱否則直接返回用戶名。代碼如下:

...

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

同樣,為了確保數(shù)據(jù)庫中用戶信息安全,密碼字段必須以密文形式存儲。這樣即使出現(xiàn)數(shù)據(jù)庫泄露或者入侵行為也能載一定程度上確保用戶信息的安全。這里我們將會使用對 Bcrypt 程序?qū)τ脩裘艽a進(jìn)行單向哈希散列,然后在數(shù)據(jù)庫中存儲加密后的結(jié)果。

首先,我們需要在 user.js 文件頭部引入 Bcrypt 類庫。在使用過程中我們可以通過增加哈希次數(shù)來提高數(shù)據(jù)的安全性。當(dāng)然,哈希操作是非常操作,所以我們應(yīng)該選取一個相對適中的數(shù)值。例如,下面的代碼中我們將哈希次數(shù)設(shè)定為了 10 。

var bcrypt = require("bcrypt-nodejs");
var SALT_FACTOR = 10;

當(dāng)然,對密碼的哈希操作應(yīng)該在保存數(shù)據(jù)之前。所以這部分代碼應(yīng)該在數(shù)據(jù)保存之前的回調(diào)函數(shù)中完成,代碼如下:

...

var noop = function() {};
// 保存操作之前的回調(diào)函數(shù)
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();
            }
        );
    });
});

該回調(diào)函數(shù)會在每次進(jìn)行數(shù)據(jù)庫保存之前被調(diào)用,所以它能確保你的密碼會以密文形式得到保存。

處理需要對密碼進(jìn)行加密處理之外,另一個常見需求就是用戶授權(quán)驗證了。例如,在用戶登錄操作時的密碼驗證操作。

...

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

出于安全原因,這里我們使用的是 bcrypt.compare 函數(shù)而不是簡單的相等判斷 === 。

完成模型定義和通用方法實現(xiàn)后,接下來我們就需要將其暴露出來供其他代碼使用了。不過暴露模型的操作非常簡單只需兩行代碼:

...

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;
模型使用

模型定義好之后,接下來就是在主頁、編輯頁面、注冊等頁面進(jìn)行使用了。相比于之前的模型定義,使用過程相對來說要更簡單。

首先,在項目根目錄創(chuàng)建主入口文件 app.js 并復(fù)制下面的代碼:

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服務(wù)器的test數(shù)據(jù)庫
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,<

接下來,我們需要實現(xiàn)上面使用到的路由中間件。在根目錄新建 routes.js 并復(fù)制代碼:

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 進(jìn)行了數(shù)據(jù)庫連接。然后,在路由中間件中通過 User.find 異步獲取用戶列表并將其傳遞給了主頁視圖模版。

接下來,我們就輪到主頁視圖的實現(xiàn)了。首先在根目錄創(chuàng)建 views 文件夾,然后在文件夾中添加第一個模版文件 _header.ejs




    
    Learn About Me
    


    
    
<% errors.forEach(function(error) { %> <% }) %> <% infos.forEach(function(info) { %> <% }) %>

你可能注意到了這些文件的名字是以下劃線開始的。這是一個社區(qū)約定,所有組件模版都會以下劃線進(jìn)行區(qū)分。

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

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

<% include _header %>

Welcome to Learn About Me!

<% users.forEach(function(user) { %>
<% if (user.bio) { %>
<%= user.bio %>
<% } %>
<% }) %> <% include _footer %>

確保代碼無誤后,接下來啟動 Mongo 數(shù)據(jù)庫服務(wù)并使用 npm start 拉起工程。然后,通過瀏覽器訪問 localhost:3000 就能類型下圖的主頁界面:

當(dāng)然,因為此時數(shù)據(jù)庫中并沒有任何記錄所有這里并沒有出現(xiàn)任何用戶信息。

接下來,我們就來實現(xiàn)用戶用戶注冊和登錄功能。不過在此之前,我們需要在 app.js 中引入 body-parser 模塊并用于后面請求參數(shù)的解析。

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

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

為了提高安全性,這里我們將 body-parser 模塊的 extended 設(shè)置為 false 。接下來,我們在 routes.js 添加 sign-up 功能的中間件處理函數(shù):

var passport = require("passport");
...
router.get("/signup", function(req, res) {
    res.render("signup");
});
router.post("/signup", function(req, res, next) {
    // 參數(shù)解析
    var username = req.body.username;
    var password = req.body.password;
    
    // 調(diào)用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);
    });
    // 進(jìn)行登錄操作并實現(xiàn)重定向
}, passport.authenticate("login", {
    successRedirect: "/",
    failureRedirect: "/signup",
    failureFlash: true
}));

路由中間件定義完成后,下面我們就來實現(xiàn)視圖模版 signup.ejs 文件。

// 拷貝代碼到 views/signup.ejs
<% include _header %>

Sign up

<% include _footer %>

如果你成功創(chuàng)建用戶并再次訪問主頁的話,你就能看見一組用戶列表:

而注冊頁的 UI 大致如下:

在實現(xiàn)登錄功能之前,我們先把個人信息展示功能先補充完整。在 routes.js 添加如下中間件函數(shù):

...
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 %>

<% if ((currentUser) && (currentUser.id === user.id)) { %>
    Edit your profile
<% } %>

<%= user.name() %>

Joined on <%= user.createdAt %>

<% if (user.bio) { %>

<%= user.bio %>

<% } %> <% include _footer %>

如果現(xiàn)在你通過首頁進(jìn)入用戶詳情頁話,那么你就會出現(xiàn)類似下圖的界面:

通過 Passport 來進(jìn)行用戶身份驗證

除了上面這些基本功能之外,User 模型做重要的功能其實是登錄以及權(quán)限認(rèn)證。而這也是 User 模型與其他模型最大的區(qū)別。所以接下來的任務(wù)就是實現(xiàn)登錄頁并進(jìn)行密碼和權(quán)限認(rèn)證。

為了減少很多不必要的工作量,這里我們會使用到第三方的 Passport 模塊。該模版是特地為請求進(jìn)行驗證而設(shè)計處理的 Node 中間件。通過該中間件只需一小段代碼就能實現(xiàn)復(fù)雜的身份認(rèn)證操作。不過 Passport 并沒有指定如何進(jìn)行用戶身份認(rèn)證,它只是提供了一些模塊化函數(shù)。

設(shè)置 Passport

Passport 的設(shè)置過程主要有三件事:

設(shè)置 Passport 中間件。

設(shè)置 Passport 對 User 模型的序列化和反序列化的操作。

告訴 Passport 如何對 User 進(jìn)行認(rèn)證。

首先,在初始化 Passport 環(huán)境時,你需要在工程中引入一些其他中間件。它們分別為:

body-parser

cookie-parser

express-session

connect-flash

passport.initialize

passport.session

其中前面 4 個中間件已經(jīng)引入過了。它們的作用分別為: body-parser 用于參數(shù)解析;cookie-parser 處理從瀏覽器中獲取的cookies;express-session 用于處理用戶 session;而 connect-flash 則用戶展示錯誤信息。

最后,我們需要在 app.js 中引入 Passport 模塊并在后面調(diào)用其中的兩個中間件函數(shù)。

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({
    // 需要一串隨機(jī)字母序列,字符串不一定需要跟此處一樣
    secret: "TKRv0IJs=HYqrvagQ#&!F!%V]Ww/4KiVs$s,<

代碼中,我們使用一串隨機(jī)字符串來對客戶端的 session 進(jìn)行編碼。這樣就能在一定程度上增加 cookies 的安全性。而將 resave 設(shè)置為 true 則保證了即使 session 沒有被修改也依然會被刷新。

接下來就是第二步操作:設(shè)置 Passport 對 User 模型的序列化和反序列化操作了。這樣 Passport 就能實現(xiàn) session 和 user 對象的互相轉(zhuǎn)化了。Passport 文檔對這一操作的描述為:

在標(biāo)準(zhǔn)的 web 應(yīng)用中,只有當(dāng)客戶端發(fā)送了登錄請求才會需要對用戶進(jìn)行身份認(rèn)證。如果認(rèn)證通過的話,兩者之間就會新建一個 session 并將其保存到 cookie 中進(jìn)行維護(hù)。任何后續(xù)操作都不會再進(jìn)行認(rèn)證操作,取而代之的是使用 cookie 中唯一指定的 session 。所以,Passport 需要通過序列化和反序列化實現(xiàn) session 和 user 對象的互相轉(zhuǎn)化。

為了后期代碼維護(hù)方便,這里我們新建一個名為 setuppassport.js 的文件并將序列化和反序列化的代碼放入其中。最后,我們將其引入到 app.js 中:

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

下面就是 setuppassport.js 中的代碼實現(xiàn)了。因為 User 對象都有一個 id 屬性作為唯一標(biāo)識符,所以我們就根據(jù)它來進(jìn)行 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);
        });
    });
}

接下來就是最難的部分了,如何進(jìn)行身份認(rèn)證?

在開始進(jìn)行認(rèn)證前,還有一個小工作需要完成:設(shè)置認(rèn)證策略。雖然 Passport 附帶了 Facebook 、Google 的身份認(rèn)證策略,但是這里我們需要的將其設(shè)置為 local strategy 。因為驗證部分的規(guī)則和代碼是由我們自己來實現(xiàn)的。

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

...
var LocalStrategy = require("passport-local").Strategy;
…

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

查詢該用戶。

用戶不存在則提示無法通過驗證。

用戶存在則進(jìn)行密碼比較。如果匹配成功則返回當(dāng)前用戶否則提示“密碼錯誤”。

下面就是將這些步驟轉(zhuǎn)化為具體的代碼:

// 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." });
            }
        });
    });
}));
...

完成策略定義后,接下來就可以在項目的任何地方進(jìn)行調(diào)用。

最后,我們還需要完成一些視圖和功能:

登錄

登出

登錄完成后的個人信息編輯

首先,我們實現(xiàn)登錄界面視圖。在 routes.js 中添加登錄路由中間件:

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

在登錄視圖 login.ejs 中,我們會接收一個用戶名和一個密碼,然后發(fā)送登錄的 POST 請求:

<% include _header %>

Log in

<% include _footer %>

接下來,我們就需要處理該 POST 請求。其中就會使用到 Passport 的身份認(rèn)證函數(shù)。

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

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

其中 passport.authenticate 函數(shù)會返回一個回調(diào)。該函數(shù)會根據(jù)我們的指定對不同的驗證結(jié)果分別進(jìn)行重定向。例如,登錄成功會重定向到首頁,而失敗則會重定向到登錄頁。

登出操作相對來說要簡單得多,代碼如下

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

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

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

登錄和登出玩抽,下面就該輪到個人信息編輯功能了。

首先,我們來實現(xiàn)一個通用的中間件工具函數(shù) ensureAuthenticated 。該中間件函數(shù)會對當(dāng)前用戶的權(quán)限進(jìn)行檢查,如果檢查不通過則會重定向到登錄頁。

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

接下來,我們會在編輯中間件中調(diào)用該函數(shù)。因為我們需要確保在開始編輯之前,當(dāng)前用戶擁有編輯權(quán)限。

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

接下來我們需要實現(xiàn) edit.ejs 視圖模版文件。該視圖模版的內(nèi)容非常簡單,只包含用戶昵稱和簡介的修改。

//  views/edit.ejs
<% include _header %>

Edit your profile

">
<% include _footer %>

最后,我們需要對修改后提交的請求作出處理。在進(jìn)行數(shù)據(jù)庫更新之前,這里同樣需要進(jìn)行權(quán)限認(rèn)證。

// 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");
    });
});
...

該代碼僅僅只是對數(shù)據(jù)庫對應(yīng)記錄的字段進(jìn)行了更新。最終渲染的編輯視圖如下:

最后,你可以創(chuàng)建一些測試數(shù)據(jù)對示例應(yīng)用的所有功能進(jìn)行一遍驗證。

總結(jié)

本文包含的內(nèi)容有:

Mongo 的工作原理。

Mongoose 的使用。

使用 bcrypt 對特定字段進(jìn)行加密來提高數(shù)據(jù)安全性。

使用 Passport 進(jìn)行權(quán)限認(rèn)證。

原文地址

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/84845.html

相關(guān)文章

  • 全棧最后一公里 - Node.js 項目的線上服務(wù)器部署與發(fā)布

    摘要:沒有耐心閱讀的同學(xué),可以直接前往學(xué)習(xí)全棧最后一公里。我下面會羅列一些,我自己錄制過的一些項目,或者其他的我覺得可以按照這個路線繼續(xù)深入學(xué)習(xí)的項目資源。 showImg(https://segmentfault.com/img/bVMlke?w=833&h=410); 本文技術(shù)軟文,閱讀需謹(jǐn)慎,長約 7000 字,通讀需 5 分鐘 大家好,我是 Scott,本文通過提供給大家學(xué)習(xí)的方法,...

    Nosee 評論0 收藏0
  • 實戰(zhàn)】用 express+MongoDB 搭建一個完整的前端項目

    摘要:前言要做一個全沾的工程師,對于后端和數(shù)據(jù)庫來說,即使不認(rèn)識也要見個面的?;玖私獾母拍罹秃茫饕前惭b上數(shù)據(jù)庫,并進(jìn)行簡單的增刪操作。 前言:要做一個全沾的工程師,對于后端和數(shù)據(jù)庫來說,即使不認(rèn)識也要見個面的。本文給的例子很簡單,也貼出來源碼,只要一步步下來,就可以跑起來啦~~~ 思考一個需求:做一個登錄頁面,自己搭建服務(wù)和數(shù)據(jù)庫,將用戶輸入的登錄信息保存到數(shù)據(jù)庫如何完成呢:首先選擇...

    Steve_Wang_ 評論0 收藏0
  • node.js中文資料導(dǎo)航

    摘要:中文資料導(dǎo)航官網(wǎng)七牛鏡像深入淺出系列進(jìn)階必讀中文文檔被誤解的編寫實戰(zhàn)系列熱門模塊排行榜,方便找出你想要的模塊多線程,真正的非阻塞淺析的類利用編寫異步多線程的實例中與的區(qū)別管道拒絕服務(wù)漏洞高級編程業(yè)界新聞看如何評價他們的首次嘗鮮程序員如何說服 node.js中文資料導(dǎo)航 Node.js HomePage Node官網(wǎng)七牛鏡像 Infoq深入淺出Node.js系列(進(jìn)階必讀) Nod...

    geekidentity 評論0 收藏0
  • Express 實戰(zhàn)(一):概覽

    摘要:一個標(biāo)準(zhǔn)性的事件就是年的橫空出世。引擎快速處理能力和異步編程風(fēng)格,讓開發(fā)者從多線程中解脫了出來。其次,通過異步編程范式將其高并發(fā)的能力發(fā)揮的淋漓盡致。它也僅僅是一個處理請求并作出響應(yīng)的函數(shù),并無任何特殊之處。 showImg(https://segmentfault.com/img/remote/1460000010819116); 在正式學(xué)習(xí) Express 內(nèi)容之前,我們有必要從大...

    zhaochunqi 評論0 收藏0

發(fā)表評論

0條評論

閱讀需要支付1元查看
<