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。緣由有兩個:一、咱們可能會用到不一樣的驗證方法(Facebook、Wechat等),不一樣系統下采用profile.id沒法保證惟一性;二、這裏咱們使用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存儲,因此更麻煩。