摘要:在使用過程中我們可以通過增加哈希次數(shù)來提高數(shù)據(jù)的安全性。當然,對密碼的哈希操作應該在保存數(shù)據(jù)之前。
毫無疑問,幾乎所有的應用都會涉及到數(shù)據(jù)存儲。但是 Express 框架本身只能通過程序變量來保存數(shù)據(jù),它并不提供數(shù)據(jù)持久化功能。而僅僅通過內(nèi)存來保存數(shù)據(jù)是無法應對真實場景的。因為內(nèi)存本身并不適用于大規(guī)模的數(shù)據(jù)儲存而且服務停止后這些數(shù)據(jù)也會消失。雖然我們還可以通過文件的形式保存數(shù)據(jù),但是文件中的數(shù)據(jù)對于查詢操作明顯不友好。所有,接下來我們將學習如何在 Express 中通過 MongoDB 數(shù)據(jù)庫的形式來對數(shù)據(jù)進行持久化存儲。
本文包含的主要內(nèi)容有:
MongoDB 是如何工作的。
如何使用 Mongoose 。
如何安全的創(chuàng)建用戶賬戶。
如何使用用戶密碼進行授權操作。
為什么是 MongoDB ?對于 Web 應用來說,通常數(shù)據(jù)庫的選擇可以劃分為兩大類:關系型和非關系型。其中前者優(yōu)點類型于電子表格,它的數(shù)據(jù)是結構化并且伴隨著嚴格規(guī)定。典型的關系型數(shù)據(jù)庫包括:MySQL、 SQL Server 以及 PostgreSQL。而后者通常也被稱為 NoSQL 數(shù)據(jù)庫,它的結構相對更加靈活,而這一點與 JS 非常類似。
但是為什么 Node 開發(fā)者會特別中意 NoSQL 中的 Mongo 數(shù)據(jù)庫,還形成了流行的 MEAN 技術棧呢?
第一個原因是:Mongo 是 NoSQL 類型數(shù)據(jù)里最流行的一個。這也讓網(wǎng)上關于 Mogon 的資料非常豐富,所有你在實際使用過程中可能會遇到的坑大幾率都能找到答案。而且作為一個成熟的項目,Mongo 也已經(jīng)被大公司認可和應用。
另一個原因則是 Mongo 自身非常可靠、有特色。它使用高性能的 C++ 進行底層實現(xiàn),也讓它贏得了大量的用戶信賴。
雖然 Mongo 不是用 JavaScript 實現(xiàn)的,但是原生的 shell 卻使用的是 JavaScript 語言。這意味著可以使用 JavaScript 在控制臺操作 Mongo 。另外,對于 Node 開發(fā)者來說它也減少了學習新語言的成本。
當然,Mongo 并不是所有 Express 應用的正確選擇,關系數(shù)據(jù)庫依然占據(jù)著非常重要的地位。順便提一下,NoSQL 中的 CouchDB 功能也非常強大。
Mongo 是如何工作的注意:雖然本文只會介紹 Mongo 以及 Mongoose 類庫的使用。但是如果你和我一樣對 SQL 非常熟悉并且希望在 Express 使用關系數(shù)據(jù)庫的話,你可以去查看 Sequelize。它為很多關系型數(shù)據(jù)庫提供了良好的支持。
在正式使用 Mongo 前,我們先來看看 Mongo 是如何工作的。
對于大多數(shù)應用來說都會在服務器中使用 Mongo 這樣的數(shù)據(jù)庫來進行持久化工作。雖然,你可以在一個應用中創(chuàng)建多個數(shù)據(jù)庫,但是絕大多數(shù)都只會使用一個。
如果你想正常訪問這些數(shù)據(jù)庫的話,首先你需要運行一個 Mongo 服務。客戶端通過給服務端發(fā)送指令來實現(xiàn)對數(shù)據(jù)庫的各種操作。而連接客戶端與服務端的程序通常都被稱為數(shù)據(jù)庫驅動。對于 Mongo 數(shù)據(jù)庫來說它在 Node 環(huán)境下的數(shù)據(jù)庫驅動程序是 Mongoose。
每個數(shù)據(jù)庫都會有一個或多個類似于數(shù)組一樣的數(shù)據(jù)集合。例如,一個簡單的博客應用,可能就會有文章集合、用戶集合。但是這些數(shù)據(jù)集合的功能遠比數(shù)組來的強大。例如,你可以查詢集合中 18 歲以上的用戶。
而每一個集合里面存儲了 JSON 形式的文檔,雖然在技術上并沒有采用 JSON。每一個文檔都對應一條記錄,而每一條記錄都包含若干個字段屬性。另外,同一集合里的文檔記錄并不一定擁有一樣的字段屬性。這也是 NoSQL 與 關系型數(shù)據(jù)庫最大的區(qū)別之一。
實際上文檔在技術上采用的是簡稱為 BSON 的 Binary JSON。在實際寫代碼過程中,我們并不會直接操作 BSON 。多數(shù)情況下會將其轉化為 JavaScript 對象。另外,BSON 的編碼和解碼方式與 JSON 也有不同。BSON 支持的類型也更多,例如,它支持日期、時間戳。下圖展示了應用中數(shù)據(jù)庫使用結構:
最后還有一點非常重要:Mongo 會給每個文檔記錄添加一個 _id 屬性,用于標示該記錄的唯一性。如果兩個同類型的文檔記錄的 id 屬性一致的話,那么就可以推斷它們是同一記錄。
SQL 使用者需要注意的問題如果你有關系型數(shù)據(jù)庫的知識背景的話,其實你會發(fā)現(xiàn) Mongo 很多概念是和 SQL 意義對應的。
首先, Mongo 中的文檔概念其實就相當于 SQL 中的一行記錄。在應用的用戶系統(tǒng)中,每一個用戶在 Mongo 中是一個文檔而在 SQL 中則對應一條記錄。但是與 SQL 不同的是,在數(shù)據(jù)庫層 Mongo 并沒有強制的 schema,所以一條沒有用戶名和郵件地址的用戶記錄在 Mongo 中是合法的。
其次,Mongo 中的集合對應 SQL 中的表,它們都是用來存儲同一類型的記錄。
同樣,Mongo 中的數(shù)據(jù)庫也和 SQL 數(shù)據(jù)庫概念非常相似。通常一個應用只會有一個數(shù)據(jù)庫,而數(shù)據(jù)庫內(nèi)部則可以包含多個集合或者數(shù)據(jù)表。
更多的術語對應表可以去查看官方的這篇文檔。
Mongo 環(huán)境搭建在使用之前,首要的任務當然就是機器上安裝 Mongo 數(shù)據(jù)庫并拉起服務了。如果你的機器是 macOS 系統(tǒng)并且不喜歡命令行模式的話,你可以通過安裝 Mongo.app 應用完成環(huán)境搭建。如果你熟悉命令行交互的話可以通過 Homebrew 命令 brew install mongodb 進行安裝。
Ubuntu 系統(tǒng)可以參照文檔,同時 Debian 則可以參照文檔 進行 Mongo 安裝。
另外,在本書中我們會假設你安裝是使用的 Mongo 數(shù)據(jù)庫的默認配置。也就是說你沒有對 Mongo 的服務端口號進行修改而是使用了默認的 27017 。
使用 Mongoose 操作 Mongo 數(shù)據(jù)庫安裝 Mongo 后接下來問題就是如何在 Node 環(huán)境中操作數(shù)據(jù)庫。這里最佳的方式就是使用官方的 [Mongoose] [6]類庫。其官方文檔描述為:
Mongoose 提供了一個直觀并基于 schema 的方案來應對程序的數(shù)據(jù)建模、類型轉換、數(shù)據(jù)驗證等常見數(shù)據(jù)庫問題。
換句話說,除了充當 Node 和 Mongo 之間的橋梁之外,Mongoose 還提供了更多的功能。下面,我們通過構建一個帶用戶系統(tǒng)的簡單網(wǎng)站來熟悉 Mongoose 的特性。
準備工作為了更好的學習本文的內(nèi)容,下面我們會開發(fā)一個簡單的社交應用。該應用將會實現(xiàn)用戶注冊、個人信息編輯、他人信息的瀏覽等功能。這里我們將它稱為 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 安裝這些依賴項。在后面的內(nèi)容中將會一一對這些依賴項的作用進行介紹。
需要注意的是,這里我們引入了一個純 JS 實現(xiàn)的加密模塊 bcrypt-nodejs 。其實 npm 中還有一個使用 C 語言實現(xiàn)的加密模塊 bcrypt 。雖然 bcrypt 性能更好,但是因為需要編譯 C 代碼所有安裝起來沒 bcrypt-nodejs 簡單。不過,這兩個類庫功能一致可以進行自由切換。
創(chuàng)建 user 模型前面說過 Mongo 是以 BSON 形式進行數(shù)據(jù)存儲的。例如,Hello World 的 BSON 表現(xiàn)形式為:
x16x00x00x00x02hellox00x06x00x00x00worldx00x00
雖然計算機完全能夠理解 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 的文件并復制下面代碼:
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ù)類型、唯一性、缺省、默認值作出了約定。
當模型定義好之后,接下來就是在模型中定義方法了。首先,我們添加一個返回用戶名稱的簡單方法。如果用戶定義了昵稱則返回昵稱否則直接返回用戶名。代碼如下:
... userSchema.methods.name = function() { return this.displayName || this.username; }
同樣,為了確保數(shù)據(jù)庫中用戶信息安全,密碼字段必須以密文形式存儲。這樣即使出現(xiàn)數(shù)據(jù)庫泄露或者入侵行為也能載一定程度上確保用戶信息的安全。這里我們將會使用對 Bcrypt 程序對用戶密碼進行單向哈希散列,然后在數(shù)據(jù)庫中存儲加密后的結果。
首先,我們需要在 user.js 文件頭部引入 Bcrypt 類庫。在使用過程中我們可以通過增加哈希次數(shù)來提高數(shù)據(jù)的安全性。當然,哈希操作是非常操作,所以我們應該選取一個相對適中的數(shù)值。例如,下面的代碼中我們將哈希次數(shù)設定為了 10 。
var bcrypt = require("bcrypt-nodejs"); var SALT_FACTOR = 10;
當然,對密碼的哈希操作應該在保存數(shù)據(jù)之前。所以這部分代碼應該在數(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ù)會在每次進行數(shù)據(jù)庫保存之前被調(diào)用,所以它能確保你的密碼會以密文形式得到保存。
處理需要對密碼進行加密處理之外,另一個常見需求就是用戶授權驗證了。例如,在用戶登錄操作時的密碼驗證操作。
... 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;模型使用
模型定義好之后,接下來就是在主頁、編輯頁面、注冊等頁面進行使用了。相比于之前的模型定義,使用過程相對來說要更簡單。
首先,在項目根目錄創(chuàng)建主入口文件 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數(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 并復制代碼:
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 進行了數(shù)據(jù)庫連接。然后,在路由中間件中通過 User.find 異步獲取用戶列表并將其傳遞給了主頁視圖模版。
接下來,我們就輪到主頁視圖的實現(xiàn)了。首先在根目錄創(chuàng)建 views 文件夾,然后在文件夾中添加第一個模版文件 _header.ejs :
Learn About Me <% errors.forEach(function(error) { %><%= error %><% }) %> <% infos.forEach(function(info) { %><%= info %><% }) %>你可能注意到了這些文件的名字是以下劃線開始的。這是一個社區(qū)約定,所有組件模版都會以下劃線進行區(qū)分。
接下來,添加第二個通用組件模版 _footer.js:
最后,我們添加主頁視圖模版文件。該視圖模版會接受中間件中傳入的 users 變量并完成渲染:
<% include _header %>Welcome to Learn About Me!
<% users.forEach(function(user) { %><% if (user.bio) { %><% }) %> <% include _footer %><%= user.bio %><% } %>確保代碼無誤后,接下來啟動 Mongo 數(shù)據(jù)庫服務并使用 npm start 拉起工程。然后,通過瀏覽器訪問 localhost:3000 就能類型下圖的主頁界面:
當然,因為此時數(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 設置為 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); }); // 進行登錄操作并實現(xiàn)重定向 }, passport.authenticate("login", { successRedirect: "/", failureRedirect: "/signup", failureFlash: true }));路由中間件定義完成后,下面我們就來實現(xiàn)視圖模版 signup.ejs 文件。
// 拷貝代碼到 views/signup.ejs <% include _header %><% include _footer %>Sign up
如果你成功創(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)在你通過首頁進入用戶詳情頁話,那么你就會出現(xiàn)類似下圖的界面:
通過 Passport 來進行用戶身份驗證除了上面這些基本功能之外,User 模型做重要的功能其實是登錄以及權限認證。而這也是 User 模型與其他模型最大的區(qū)別。所以接下來的任務就是實現(xiàn)登錄頁并進行密碼和權限認證。
為了減少很多不必要的工作量,這里我們會使用到第三方的 Passport 模塊。該模版是特地為請求進行驗證而設計處理的 Node 中間件。通過該中間件只需一小段代碼就能實現(xiàn)復雜的身份認證操作。不過 Passport 并沒有指定如何進行用戶身份認證,它只是提供了一些模塊化函數(shù)。
設置 PassportPassport 的設置過程主要有三件事:
設置 Passport 中間件。
設置 Passport 對 User 模型的序列化和反序列化的操作。
告訴 Passport 如何對 User 進行認證。
首先,在初始化 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({ // 需要一串隨機字母序列,字符串不一定需要跟此處一樣 secret: "TKRv0IJs=HYqrvagQ#&!F!%V]Ww/4KiVs$s,<代碼中,我們使用一串隨機字符串來對客戶端的 session 進行編碼。這樣就能在一定程度上增加 cookies 的安全性。而將 resave 設置為 true 則保證了即使 session 沒有被修改也依然會被刷新。
接下來就是第二步操作:設置 Passport 對 User 模型的序列化和反序列化操作了。這樣 Passport 就能實現(xiàn) session 和 user 對象的互相轉化了。Passport 文檔對這一操作的描述為:
在標準的 web 應用中,只有當客戶端發(fā)送了登錄請求才會需要對用戶進行身份認證。如果認證通過的話,兩者之間就會新建一個 session 并將其保存到 cookie 中進行維護。任何后續(xù)操作都不會再進行認證操作,取而代之的是使用 cookie 中唯一指定的 session 。所以,Passport 需要通過序列化和反序列化實現(xiàn) session 和 user 對象的互相轉化。
為了后期代碼維護方便,這里我們新建一個名為 setuppassport.js 的文件并將序列化和反序列化的代碼放入其中。最后,我們將其引入到 app.js 中:
… var setUpPassport = require("./setuppassport"); … var app = express(); mongoose.connect("mongodb://localhost:27017/test"); setUpPassport(); …下面就是 setuppassport.js 中的代碼實現(xiàn)了。因為 User 對象都有一個 id 屬性作為唯一標識符,所以我們就根據(jù)它來進行 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 。因為驗證部分的規(guī)則和代碼是由我們自己來實現(xiàn)的。
首先,我們在 setuppassport.js 中引入 LocalStrategy
... var LocalStrategy = require("passport-local").Strategy; …接下來,按照下面的步驟使用 LocalStrategy 來進行具體的驗證:
查詢該用戶。
用戶不存在則提示無法通過驗證。
用戶存在則進行密碼比較。如果匹配成功則返回當前用戶否則提示“密碼錯誤”。
下面就是將這些步驟轉化為具體的代碼:
// 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." }); } }); }); })); ...完成策略定義后,接下來就可以在項目的任何地方進行調(diào)用。
最后,我們還需要完成一些視圖和功能:
登錄
登出
登錄完成后的個人信息編輯
首先,我們實現(xiàn)登錄界面視圖。在 routes.js 中添加登錄路由中間件:
... router.get("/login", function(req, res) { res.render("login"); }); ...在登錄視圖 login.ejs 中,我們會接收一個用戶名和一個密碼,然后發(fā)送登錄的 POST 請求:
<% include _header %><% include _footer %>Log in
接下來,我們就需要處理該 POST 請求。其中就會使用到 Passport 的身份認證函數(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ù)我們的指定對不同的驗證結果分別進行重定向。例如,登錄成功會重定向到首頁,而失敗則會重定向到登錄頁。
登出操作相對來說要簡單得多,代碼如下
// 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(); }); ...
登錄和登出玩抽,下面就該輪到個人信息編輯功能了。
首先,我們來實現(xiàn)一個通用的中間件工具函數(shù) ensureAuthenticated 。該中間件函數(shù)會對當前用戶的權限進行檢查,如果檢查不通過則會重定向到登錄頁。
// 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ù)。因為我們需要確保在開始編輯之前,當前用戶擁有編輯權限。
// GET /edit(在router.js中) ... // 確保用戶被身份認證;如果它們沒有被重定向的話則運行你的請求處理 router.get("/edit", ensureAuthenticated, function(req, res) { res.render("edit"); }); ...
接下來我們需要實現(xiàn) edit.ejs 視圖模版文件。該視圖模版的內(nèi)容非常簡單,只包含用戶昵稱和簡介的修改。
// views/edit.ejs <% include _header %><% include _footer %>Edit your profile
最后,我們需要對修改后提交的請求作出處理。在進行數(shù)據(jù)庫更新之前,這里同樣需要進行權限認證。
// 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ù)庫對應記錄的字段進行了更新。最終渲染的編輯視圖如下:
最后,你可以創(chuàng)建一些測試數(shù)據(jù)對示例應用的所有功能進行一遍驗證。
總結本文包含的內(nèi)容有:
Mongo 的工作原理。
Mongoose 的使用。
使用 bcrypt 對特定字段進行加密來提高數(shù)據(jù)安全性。
使用 Passport 進行權限認證。
原文地址
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/84845.html
摘要:沒有耐心閱讀的同學,可以直接前往學習全棧最后一公里。我下面會羅列一些,我自己錄制過的一些項目,或者其他的我覺得可以按照這個路線繼續(xù)深入學習的項目資源。 showImg(https://segmentfault.com/img/bVMlke?w=833&h=410); 本文技術軟文,閱讀需謹慎,長約 7000 字,通讀需 5 分鐘 大家好,我是 Scott,本文通過提供給大家學習的方法,...
摘要:前言要做一個全沾的工程師,對于后端和數(shù)據(jù)庫來說,即使不認識也要見個面的。基本了解的概念就好,主要是安裝上數(shù)據(jù)庫,并進行簡單的增刪操作。 前言:要做一個全沾的工程師,對于后端和數(shù)據(jù)庫來說,即使不認識也要見個面的。本文給的例子很簡單,也貼出來源碼,只要一步步下來,就可以跑起來啦~~~ 思考一個需求:做一個登錄頁面,自己搭建服務和數(shù)據(jù)庫,將用戶輸入的登錄信息保存到數(shù)據(jù)庫如何完成呢:首先選擇...
摘要:中文資料導航官網(wǎng)七牛鏡像深入淺出系列進階必讀中文文檔被誤解的編寫實戰(zhàn)系列熱門模塊排行榜,方便找出你想要的模塊多線程,真正的非阻塞淺析的類利用編寫異步多線程的實例中與的區(qū)別管道拒絕服務漏洞高級編程業(yè)界新聞看如何評價他們的首次嘗鮮程序員如何說服 node.js中文資料導航 Node.js HomePage Node官網(wǎng)七牛鏡像 Infoq深入淺出Node.js系列(進階必讀) Nod...
摘要:一個標準性的事件就是年的橫空出世。引擎快速處理能力和異步編程風格,讓開發(fā)者從多線程中解脫了出來。其次,通過異步編程范式將其高并發(fā)的能力發(fā)揮的淋漓盡致。它也僅僅是一個處理請求并作出響應的函數(shù),并無任何特殊之處。 showImg(https://segmentfault.com/img/remote/1460000010819116); 在正式學習 Express 內(nèi)容之前,我們有必要從大...
閱讀 2802·2021-09-01 10:30
閱讀 1691·2019-08-30 15:52
閱讀 980·2019-08-29 18:40
閱讀 1135·2019-08-28 18:30
閱讀 2406·2019-08-23 17:19
閱讀 1334·2019-08-23 16:25
閱讀 2714·2019-08-23 16:18
閱讀 2990·2019-08-23 13:53