Node with React: Fullstack Web Development 課程手記(三)——MongoDB

妖僧風月發表於2017-11-07

上篇地址

MongoDB簡介

  • MongoDB是一個基於文件(document)的資料庫。在MongoDB中,資料是以Collection的形式來組織的,也就是一個Collection代表一種資料。一個Collection中的每條記錄(document/record)不必擁有相同的欄位,也就是說我們可以動態地為資料新增、減少或者修改欄位。如下圖所示,不同的User記錄具備可以擁有不同的欄位。
  • 我們使用mongoose來進行資料庫的操作。這其中包括兩個部分:js和資料庫。js部分每個Model Class對應資料庫部分的每個Collection,js部分的每個例項對應資料庫部分的每條記錄(record)。

add mongoDB

  • 使用mongoDB有兩種方式:本地安裝;遠端安裝。本次課採用後者,使用MongoDB後的系統架構如下圖所示。
  • 登陸 mlab.com,建立賬號,登陸,建立一個免費的database,進入其控制皮膚。建立管理員使用者名稱和密碼。done!
  • 在server端引入mongoose,並連線我們剛才建立的資料庫。首先安裝mongoose,npm install --save mongoose。 剛才在建立資料庫成功的頁面有這樣一句話To connect using a driver via the standard MongoDB URI (what's this?):。這句話後面的內容就是我們要訪問這個資料庫的URI。把裡面的<dbuser>dbpassword改為我們剛才建立管理員的使用者名稱和密碼,就可以訪問了。因為這個資訊也屬於敏感資訊,所以把這部分內容寫在./config/keys.js中,在index.js中引入,並使用的程式碼如下所示:
const keys = require('./config/keys');
const mongoose = require("mongoose");
mongoose.connect(keys.mongoURI);複製程式碼
  • 這裡看一下我們所處的狀態和接下來要做的事情。首先我們有了用於儲存資料的MongoDB和用於運算元據的mongoose。接下來我們要對訪問的使用者進行檢查,檢查他們是否在我們的儲存記錄中,如果在就讓他登陸,如果不在點選授權,我們用授權返回的GoogleID為內容建立一條新的記錄,那麼當使用者下次進入網站的時候就不必再次授權了。
  • 接下來建立model。MongoDB自身的collection是可以包含不同結構 的記錄的,但是mongoose卻需要預先定義collection的記錄解構是什麼樣子的。因此這裡需要預先設定Schema。傳入的引數是一個物件,定義collection的各個key,及對應的資料型別。(允許在中途修改Schema)。這裡建立一個新的檔案./models/Users.js
const mongoose = require('mongoose');
const { Schema } = mongoose;
// es6 解構賦值 <=> const Schema = mongoose.Schema
const userSchema = new Schema({
    googleId: String
});複製程式碼
  • mongoose是通過建立一個class的方式建立一個collection的。接下來的程式碼建立一個名字為users的collection,使用的Schema就是上面建立的userSchema,這個Schema定義了這個collection的每個記錄都包含一個型別為string,名為googleId的資料。
mongoose.model('users', userSchema);複製程式碼
  • 最後再index.js中引入./models/Users.js檔案,以使這一堆程式碼執行。
require('./models/Users');複製程式碼
  • 然後我們要做的是就是要把從Google伺服器拿到的id,儲存為一個Collection為users的記錄。我們是在./services/passport使用new GoogleStrategy()方法中的回撥函式拿到使用者資料的。因此你我們將會在那個回撥函式中使用mongoose將資料儲存到Collection為users資料庫中。首先我們要拿到名為users的collection。程式碼如下,注意我們使用了同樣的函式mongoose.use,這個函式當傳入Schema時,是建立collection,當只傳名字的時候,就是取到Collection。
const mongoose = require('mongoose');
const User = mongoose.model('users');複製程式碼
  • new GoogleStrategy()傳入的回撥函式中,我們建立一個user例項。注意,這裡new User()是建立了一個JavaScript物件,並未將資料存入資料庫中(參考上面mongoose vs mongoDB的圖),要將資料寫入資料庫,必須呼叫這個物件的save方法。
new User({ googleId: profile.id }).save();複製程式碼
  • 注意我們是在./models/Users.js中定義名為users的collection的,但在./services/passport.js中使用了這個collection,因此在index.js中引入這兩個檔案時要注意先後順序,前者要先引用。
  • 現在訪問localhost:5000/auth/google,然後去mlab的皮膚上重新整理,可以看到Collection目錄下多了一條名為user的條目,點選進去可以看到有一條記錄,其中的googleId就是你剛才用於授權的googleId賬戶的id。但是現在有一個問題,當我們重複這個操作,就會發現我們的資料庫中多了一條重複的記錄。而我們想要的結果是,如果已經有了相同的記錄就不再建立記錄。
  • 我們接著使用mongoose class的查詢功能,檢查當前使用者是否存在,如果不存在才新建一個。邏輯變為:
User.findOne({ googleId: profile.id }).then((existingUser) => {
    if (!existingUser) {
        new User({
            googleId: profile.id
        }).save();
    }
});複製程式碼
  • 注意,所有的資料庫操作都是非同步的,mongoose為我們封裝了Promise來對返回結果進行操作,因此這裡將判斷邏輯寫在了then的回撥函式中。
  • 還沒完,我們還沒有使用者的資訊傳遞給passport。如何把使用者資訊傳遞給passport呢。注意之前的回撥函式中傳入了done引數,done是一個函式,其第一個引數是為了傳遞錯誤資訊,第二個引數是為了傳輸passport驗證所需的資訊。所以我們可以把user資訊傳入done的第二個引數,從而傳遞給passport,具體程式碼如下:
User.findOne({ googleId: profile.id }).then(existingUser => {
    if (!existingUser) {
        new User({
            googleId: profile.id
        })
            .save()
            .then(user => {
                done(null, user);
            });
    } else {
        done(null, existingUser);
    }
});複製程式碼
  • 為什麼我們要搞資料庫呢?——當然是為了驗證流程了。我們這次採用的是使用cookie的驗證流程,而所有資料庫這一套東西都是為了產生cookie。
  • 使用者訪問網站,通過查詢資料庫來判斷是新使用者還是老使用者。
  • 是新使用者,那麼在資料庫產生一個新的記錄,並用這個新的資料庫來產生cookie,並返回給瀏覽器。以後瀏覽器在對這個伺服器產生其他請求時,cookie將自動攜帶,伺服器就能識別這個請求是屬於這個使用者了。
  • 如果是老使用者,直接從資料庫中取出使用者資訊,產生cookie,並給瀏覽器設定cookie。設定cookie的目的同上。
  • 具體從使用者資訊到cookie是通過序列化(serialize)完成的,從cookie到使用者資訊是通過反序列化(deserialize)完成的。
  • 序列化和反序列化是passport幫我們完成的。分別如下:
// 序列化
passport.serializeUser((user, done) => {
    done(null user.id);
});複製程式碼
  • 這裡傳入的引數user正式我們在從資料庫取到(建立)一條使用者資訊後傳遞給done函式的值。實際上就是資料庫中的使用者資訊。這裡的user.id是資料庫自動生成的id,而非googleId。原因有兩個:1、我們可能會用到不同的驗證方法(Facebook、Wechat等),不同系統下采用profile.id無法保證唯一性;2、這裡我們使用googleId的唯一作用就是為了授權登陸,登陸後的一切請求都與googleId無關,所以之後請求中攜帶的cookie資訊(正是這次序列化所生成的)應該包含資料庫id而非googleId。
passport.deserializeUser((id, done) => {
    User.findById(id).then((user) => {
        done(null, user);
    })
})複製程式碼
  • 反序列化中id就是cookie資訊,也就是資料庫產生的id,我們在資料庫中根據這個id找到使用者資訊,以進行進一步操作,最後呼叫done函式,以完成反序列化。
  • 接下來我們要完成的就是讀寫cookie的操作。這裡我們使用cookie-session這個包,來幫助我們實現對cookie的操作。先看程式碼,然後解釋原理。
  • 注意,這裡引入了cookieKey,這其實是我們呢在./config/keys中加入的一段隨機字串(僅字母和數字),用於對cookie資訊加密。
// index.js
const passport from 'passport';
const cookieSession from 'cookie-session';

app.use(
    cookieSession({
        maxAge: 7*24*3600*1000,
        keys: [keys.cookieKey]
    )
);
app.use(passport.initialize());
app.use(passport.session());複製程式碼
  • 至此所有的授權、驗證工作已經做完了。cookie-session passport是怎麼完成這個工作呢。對於接下來的請求來說,每個請求都會先通過cookie-session,cookie-session從中提取cookie資訊、解密然後反序列化,得到一個使用者例項。最後把這個使用者例項掛在req物件中,然後才把這個req物件傳遞給實際的route handler。
  • 為了驗證上述邏輯是對的,我們新增一個route handler,其中只返回req中掛的user,看其中是否為例項化的model。然後我們先通過localhost:5000/auth/google登陸,然後再訪問localhost:5000/api/current_user,檢視當前請求所攜帶的user,不出意外正是googleId為剛才授權的user例項物件。
// ./routes/authRoutes.js
app.get('/api/current_user', (req, res) => {
  res.send(req.user);  
})複製程式碼
  • 接下來增加一個用於登出使用者的api,以方便我們之後的測試。我們之前提到,passport為傳遞給實際route handler的req物件增加了user,實際上passport還增加了別的東西,其中一個就是logout方法。我們通過呼叫req.logout(),就可以實現使用者的登出登入。
app.get('/api/logout', (req, res) => {
    req.logout();
    res.send(req.user); // logout後應該為undefined
});複製程式碼
  • 接下來解釋幾處比較奇怪的程式碼。
  • 首先是index.js中幾處app.use。我們知道express app的作用就是接受請求,並給出響應。app.use中傳入的是function,這些function叫做中介軟體,作用是修改接收的請求,然後再把它傳遞給實際處理請求的route handler。對於所有請求通用的邏輯比較適合寫在中介軟體中,比如這裡的驗證使用者的邏輯。因為很多請求都需要驗證使用者的身份才能給出合適的響應,與其在每個route handler都寫相同的邏輯(讀cookie->解密->反序列化->拿到user model例項),我們把邏輯寫在中介軟體中,所有的請求都會走一遍。這裡我們實際用到了兩個中介軟體的邏輯,一個是cookie-session,一個是passport。
  • cookie-session作用是從請求中拿到cookie並解密,那它是如何把解密後的cookie傳遞給passport的呢?如果我們把/api/current_user中的邏輯改為res.send(req.session),我們會看到一個實際返回的是一個像下面程式碼所示的物件。這說明此時req.session中儲存的是解密之後的cookie資訊,實際上是cookie-session把這段解密後的資訊掛在了req.session上傳遞給了passport。然後passport再拿這段資訊進行反序列化。
passport: {
    user: "59f893ef4a3dde26c5d9bce2"
}複製程式碼
  • express官方推薦處理的cookie的庫有兩個,一個是我們這次用的cookie-session,另一個是express-session,這裡主要講一下二者的區別:就是使用者資訊儲存方式不同。在cookie-session中,cookie就是session,也就是說cookie中包含了session的所有資訊。
  • 在express-cookie中,cookie提供對session的引用,具體講,session是有自己的儲存空間(session_store)的,實際要取的資料是從這個儲存空間中取的,cookie只提供對這個session的引用(通過session_id)。相比之下後者能儲存更多的資料,前者只能儲存4KB資料。但是後者可能要設定remote儲存,所以更麻煩。


相關文章