本篇文章中,我們想要知道以下的重點 :
- passport 是啥鬼 ?
- 要如何使用它呢 ?
- 要如何使用一個 passport 的登入系統呢 ?
passport 是啥 ?
passport.js
是 node 中的一段登入驗證中間層(middleware),也就是說可以讓你簡單的使用 google 登入
或使用 fb 登入
,它的架構就是所謂的策略模式
,接下來我們來實際上看看他是如何使用的。
passort.js 活著的目的就是為了驗證 request
要使用 passport 來進行驗證,需要設定三個東西 :
- 驗證策略 (Authentication strategies)
- 應用程式的中間件 (Application middleware)
- Sessions (可選擇)
驗證策略的建立
上面我們有提到 passport 本身就是使用策略模式
的實作,而它的定義就是 :
定義一系列的演算法,把它們一個個封裝起來,並且可以相互替換。
所以在這邊,我們需要定義驗證的策略(演算法)
,例如使用 facebook 登入驗證、google 登入驗證或自訂的驗證策略。
而我們這裡直接看官網的自訂驗證策略localStrategy
,下面的程式碼中,我們會定義一個localStrategy
,它準備用來驗證我們的request
。
而LocalStrategy
的兩個參數為options
和verify
,我們option
需要先定義要用來驗證的欄位username
、passowrd
,然後verify
就是驗證規則,就是下面那個function
裡面的東東。
var users = {
zack: {
username: 'zack',
password: '1234',
id: 1,
},
node: {
username: 'node',
password: '5678',
id: 2,
},
}
// LacalStrategy(options,verify)
var localStrategy = new LocalStrategy({
usernameField: 'username',
passwordField: 'password',
},
function (username, password, done) {
user = users[username];
if (user == null) {
return done(null, false, { message: 'Invalid user' });
};
if (user.password !== password) {
return done(null, false, { message: 'Invalid password' });
};
done(null, user);
}
)
###那這邊有個問題 ~ 那就是奇怪,為什麼他沒有驗證 username 或 password 這兩個欄位是否合法呢 ?
因為LocalStrategy
已經幫我們處理好了,我們直接來看一下它的原始碼 :
Strategy.prototype.authenticate = function(req, options) {
options = options || {};
var username = lookup(req.body, this._usernameField) || lookup(req.query, this._usernameField);
var password = lookup(req.body, this._passwordField) || lookup(req.query, this._passwordField);
if (!username || !password) {
return this.fail({ message: options.badRequestMessage || 'Missing credentials' }, 400);
}
var self = this;
function verified(err, user, info) {
if (err) { return self.error(err); }
if (!user) { return self.fail(info); }
self.success(user, info);
}
try {
if (self._passReqToCallback) {
this._verify(req, username, password, verified);
} else {
this._verify(username, password, verified);
}
} catch (ex) {
return self.error(ex);
}
};
上面這一段是當我們執行了下面這段時程式碼時,就會執行的東東,從上面程式碼中我們可以知道,它會先去req.body
中,尋找我們定義的兩個欄位username
和passowrd
,然後檢查看看他是否合法,當一切都 ok 時,我們就會執行上面有提到的verify
函數,來進行驗證。
Passport.authenticate('local', { session: false })
中間件的設定
接下來我們將要在route
中,增加 passport 這個中間件 (middleware),我們這邊選擇使用 express 來當我們的 web framework。
我們在使用時需要先選擇我們要使用的策略,我們直接用上面所建立的localStrategy
,如果你有建立其它的例如facebook
或google
的策略也都可以使用。
// 註冊策略
Passport.use('local', localStrategy);
var app = Express();
app.use(BodyParser.urlencoded({ extended: false }));
app.use(BodyParser.json());
app.use(Passport.initialize());
其中Passport.use('local', localStrategy);
這行就是將我們剛剛建立的策略註冊到 passport 中,我們直接看他的原始碼,會更了解它在做啥 :
嗯他非常的簡單,就是將我們的註冊的策略丟到一個物件中。
Authenticator.prototype.use = function(name, strategy) {
if (!strategy) {
strategy = name;
name = strategy.name;
}
if (!name) { throw new Error('Authentication strategies must have a name'); }
this._strategies[name] = strategy;
return this;
};
然後呢我們就要在 route 上加 passport 中間件,這樣的話,我們每一個進來到這個 route 的 request 都會被 passport 我們指定的策略進行驗證。
app.post(
'/login',
Passport.authenticate('local', { session: false }),
function (req, res) {
res.send('User ID ' + req.user.id.toString());
}
);
Session的設定
我們在驗證完畢後應該是會取得到某個使用者,像我們上面範例中的這行 :
user = users[username];
當然,這只是範例,正常情況下應該是去 db 或其它地方取得使用者,但我們這裡就一切從簡。
接下來我們兩個問題 ~
session 和 cookie 是啥 ?
雖然我還算理解,但是這邊還是簡單的說一下。
首先這兩個都是個儲放機制。
再來 session 是只能在 server 進行維持,每當 client 在連接 server 時,會由 server 產生成一個唯一的 sessionId,並用它來連接 server 內的 session 存放空間,而通常來說 sessionId,也同時會保存在客戶端的 cookie 中,每次 client 在訪問 server 時都會用它來存取 session 資料。
而 cookie 則是在客戶端的儲放機制,它是由瀏覽器來維持,但注意,它可以在 client 端與 server 端進行修改,為什麼會有 cookie 呢 ? 主要就是因為 http 是無狀態的協議,每一次讀取頁面時,都是獨立的狀態,所以就需要使用 cookie 來連結前後文。
對了 cookie 還有一點要記,那就是每一次的請求,cookie 都會一起被發送到 server 端喔。
我們懂了以下的基本知道後,再來問個問題。
我們每一次登入,就要取資料庫驗證和取得一次使用者嗎 ????
答案是否定
的。
正常不太會這樣處理,假設我們是用 fb 登入,那不就變成,每一次使用者到這頁面時,畫面都會掉到 fb 要你登入,然後在跳回來原本頁面,這樣太浪費時間了。
所以說,passport 它會做以下兩件事情 :
- 將使用者資訊存放在 server 的 session 中。
- 然後會在使用者的瀏覽器設定 cookie。
那我們在 passport 要如何使用呢 ? 首先關於第一點,passport 提供了serializeUser
讓我們將使用者資訊存放置 server 的 session中。
passport.serializeUser(function(user, done) {
done(null, user.id);
});
然後關於第二點,每一次進行請求時,passport 都會將傳進來的 cookie 中某個存放該session資訊的欄位,取得到我們剛剛存的user.id
,然後在使用它,來取得完整的user
資訊,並將它存放到req.user
之中。
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
實作一個登入系統
我們這邊來實作個登入系統,使用者只要有登入後,接下來的 route 都可以從 session 中取得到該名使用者的資訊。
1. app.js 基本的註冊
var Passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var Express = require('express');
var BodyParser = require('body-parser');
var session = require('express-session');
var cookieParser = require('cookie-parser');
var app = Express();
app.use(BodyParser.urlencoded({ extended: false }));
app.use(BodyParser.json());
app.use(cookieParser());
app.use(session({
secret: "test",
resave: false,
saveUninitialized: false,
}))
app.use(Passport.initialize());
app.use(Passport.session()); // 一定要在 initialize 之後
2. 驗證策略
var users = {
mark: {
username: 'mark',
password: '1234',
id: 1,
},
node: {
username: 'node',
password: '5678',
id: 2,
},
}
var localStrategy = new LocalStrategy({
usernameField: 'username',
passwordField: 'password',
},
function (username, password, done) {
user = users[username];
if (user == null) {
return done(null, false, { message: 'Invalid user' });
};
if (user.password !== password) {
return done(null, false, { message: 'Invalid password' });
};
done(null, user);
}
)
Passport.use('local', localStrategy);
3. 建立登入的route
下面為我們登入的 route 建立。
app.post(
'/login',
Passport.authenticate('local',{session: true}),
function (req, res) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.send('User ID ' + req.user.id.toString());
}
);
那 passport 是那裡置至 session 和 cookie 呢 ?
答案是在這裡 :
passport.serializeUser(function(user, done) {
done(null, user.id);
});
然後我們來呼叫這個 route,然後你到 chrome 的 application 看,你會發現,他有存放個 cookie 。
4. 建立取得使用者的 route
然後接下來,我們執行http://127.0.0.1:3000/getInfo
後,這段程式碼app.use(Passport.session());
就會將我們從前端傳回來的 cookie,進行分析,並和 session 進行比對,然後就會將使用者資料存放到req.user
裡囉
app.get('/getInfo',function(req,res){
const user = req.user;
res.send(user);
})
注意點 : deserializeUser 無法被呼叫到
有一點要注意一下,如果你發現你的deserializeUser
老是無法被呼叫到,那問題是在下面這段 :
app.use(session({
secret: "test",
resave: false,
saveUninitialized: false,
}))
有些人會寫成下面這樣,cookie: { secure: true }
這個參數需要配合https
才能使用。
app.use(session({
secret: 'goodjob secret',
resave: false, // don't save session if unmodified
saveUninitialized: false,
cookie: { secure: true },
}));