成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

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

yanbingyun1990 / 1831人閱讀

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

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

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

MongoDB 是如何工作的。

如何使用 Mongoose 。

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

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

為什么是 MongoDB ?

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

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

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

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

雖然 Mongo 不是用 JavaScript 實(shí)現(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)大。

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

Mongo 是如何工作的

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

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

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

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

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

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

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

SQL 使用者需要注意的問題

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

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

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

同樣,Mongo 中的數(shù)據(jù)庫也和 SQL 數(shù)據(jù)庫概念非常相似。通常一個(gè)應(yīng)用只會(huì)有一個(gè)數(shù)據(jù)庫,而數(shù)據(jù)庫內(nèi)部則可以包含多個(gè)集合或者數(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)可以參照文檔,同時(shí) Debian 則可以參照文檔 進(jìn)行 Mongo 安裝。

另外,在本書中我們會(huì)假設(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 提供了一個(gè)直觀并基于 schema 的方案來應(yīng)對程序的數(shù)據(jù)建模、類型轉(zhuǎn)換、數(shù)據(jù)驗(yàn)證等常見數(shù)據(jù)庫問題。

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

準(zhǔn)備工作

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

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

個(gè)人信息頁,用于展示用戶姓名等信息。

用戶注冊頁。

用戶登錄頁。

和之前一樣,首先我們需要新建工程目錄并編輯 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"
    }
}

接下來,運(yùn)行 npm install 安裝這些依賴項(xiàng)。在后面的內(nèi)容中將會(huì)一一對這些依賴項(xiàng)的作用進(jìn)行介紹。

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

創(chuàng)建 user 模型

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

x16x00x00x00x02hellox00x06x00x00x00worldx00x00

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

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

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

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

密碼,同樣無法缺省。

創(chuàng)建時(shí)間。

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

個(gè)人簡介,非必須屬性。

在 Mongoose 中我們使用 schema 來定義用戶模型。除了包含上面的屬性之外,之后還會(huì)在其中添加一些類型方法。在項(xiàng)目的根目錄創(chuàng)建 models 文件夾,然后在其中創(chuàng)建一個(gè)名為 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í)我們還對字段的數(shù)據(jù)類型、唯一性、缺省、默認(rèn)值作出了約定。

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

...

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

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

首先,我們需要在 user.js 文件頭部引入 Bcrypt 類庫。在使用過程中我們可以通過增加哈希次數(shù)來提高數(shù)據(jù)的安全性。當(dāng)然,哈希操作是非常操作,所以我們應(yīng)該選取一個(gè)相對適中的數(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ù)會(huì)在每次進(jìn)行數(shù)據(jù)庫保存之前被調(diào)用,所以它能確保你的密碼會(huì)以密文形式得到保存。

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

...

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

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

完成模型定義和通用方法實(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)行使用了。相比于之前的模型定義,使用過程相對來說要更簡單。

首先,在項(xiàng)目根目錄創(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,<

接下來,我們需要實(shí)現(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 異步獲取用戶列表并將其傳遞給了主頁視圖模版。

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




    
    Learn About Me
    


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

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

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

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

<% include _header %>

Welcome to Learn About Me!

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

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

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

接下來,我們就來實(shí)現(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只返回一個(gè)用戶。你想在這匹配一個(gè)用戶名
    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)行登錄操作并實(shí)現(xiàn)重定向
}, passport.authenticate("login", {
    successRedirect: "/",
    failureRedirect: "/signup",
    failureFlash: true
}));

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

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

Sign up

<% include _footer %>

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

而注冊頁的 UI 大致如下:

在實(shí)現(xiàn)登錄功能之前,我們先把個(gè)人信息展示功能先補(bǔ)充完整。在 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)入用戶詳情頁話,那么你就會(huì)出現(xiàn)類似下圖的界面:

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

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

為了減少很多不必要的工作量,這里我們會(huì)使用到第三方的 Passport 模塊。該模版是特地為請求進(jìn)行驗(yàn)證而設(shè)計(jì)處理的 Node 中間件。通過該中間件只需一小段代碼就能實(shí)現(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)境時(shí),你需要在工程中引入一些其他中間件。它們分別為:

body-parser

cookie-parser

express-session

connect-flash

passport.initialize

passport.session

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

最后,我們需要在 app.js 中引入 Passport 模塊并在后面調(diào)用其中的兩個(gè)中間件函數(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 沒有被修改也依然會(huì)被刷新。

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

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

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

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

下面就是 setuppassport.js 中的代碼實(shí)現(xiàn)了。因?yàn)?User 對象都有一個(gè) 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)證前,還有一個(gè)小工作需要完成:設(shè)置認(rèn)證策略。雖然 Passport 附帶了 Facebook 、Google 的身份認(rèn)證策略,但是這里我們需要的將其設(shè)置為 local strategy 。因?yàn)轵?yàn)證部分的規(guī)則和代碼是由我們自己來實(shí)現(xiàn)的。

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

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

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

查詢該用戶。

用戶不存在則提示無法通過驗(yàn)證。

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

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

// setuppassport.js 驗(yàn)證代碼
...
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." });
            }
        });
    });
}));
...

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

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

登錄

登出

登錄完成后的個(gè)人信息編輯

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

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

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

<% include _header %>

Log in

<% include _footer %>

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

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

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

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

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

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

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

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

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

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

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

接下來,我們會(huì)在編輯中間件中調(diào)用該函數(shù)。因?yàn)槲覀冃枰_保在開始編輯之前,當(dāng)前用戶擁有編輯權(quán)限。

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

接下來我們需要實(shí)現(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中)
...
// 通常,這會(huì)是一個(gè)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)行一遍驗(yà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://systransis.cn/yun/84845.html

相關(guān)文章

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

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

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

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

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

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

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

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

    zhaochunqi 評論0 收藏0

發(fā)表評論

0條評論

最新活動(dòng)
閱讀需要支付1元查看
<