摘要:在使用過程中我們可以通過增加哈希次數(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)大。
Mongo 是如何工作的注意:雖然本文只會(huì)介紹 Mongo 以及 Mongoose 類庫的使用。但是如果你和我一樣對 SQL 非常熟悉并且希望在 Express 使用關(guān)系數(shù)據(jù)庫的話,你可以去查看 Sequelize。它為很多關(guān)系型數(shù)據(jù)庫提供了良好的支持。
在正式使用 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) { %><%= error %><% }) %> <% infos.forEach(function(info) { %><%= 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) { %><% }) %> <% include _footer %><%= user.bio %><% } %>確保代碼無誤后,接下來啟動(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 %><% include _footer %>Sign up
如果你成功創(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è)置 PassportPassport 的設(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 %><% include _footer %>Log in
接下來,我們就需要處理該 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 %><% include _footer %>Edit your profile
最后,我們需要對修改后提交的請求作出處理。在進(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
摘要:沒有耐心閱讀的同學(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í)的方法,...
摘要:前言要做一個(gè)全沾的工程師,對于后端和數(shù)據(jù)庫來說,即使不認(rèn)識也要見個(gè)面的?;玖私獾母拍罹秃?,主要是安裝上數(shù)據(jù)庫,并進(jìn)行簡單的增刪操作。 前言:要做一個(gè)全沾的工程師,對于后端和數(shù)據(jù)庫來說,即使不認(rèn)識也要見個(gè)面的。本文給的例子很簡單,也貼出來源碼,只要一步步下來,就可以跑起來啦~~~ 思考一個(gè)需求:做一個(gè)登錄頁面,自己搭建服務(wù)和數(shù)據(jù)庫,將用戶輸入的登錄信息保存到數(shù)據(jù)庫如何完成呢:首先選擇...
摘要:中文資料導(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...
摘要:一個(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)容之前,我們有必要從大...
閱讀 2863·2021-09-01 10:30
閱讀 1725·2019-08-30 15:52
閱讀 1031·2019-08-29 18:40
閱讀 1189·2019-08-28 18:30
閱讀 2442·2019-08-23 17:19
閱讀 1383·2019-08-23 16:25
閱讀 2782·2019-08-23 16:18
閱讀 3045·2019-08-23 13:53