在 Express 中使用 Passport 進行使用者驗證

Express 是在 nodejs 下著名的 web framework,
由於可透過 middleware 的設計, 掛載各式各樣的 pluging 來完成各項功能,
因此可以在很多 framework 中看到

而 Passport 則是一個 Express middleware, 可以提供 web 權限驗證的功能,
除了基本的 username/password 外, 也可以使用 OAuth 進行第三方登入驗證

做完基本介紹, 這邊簡單說明如何使用 Passport 完成基本的使用者驗證功能

首先當然要先在我們的 Express 專案中安裝 Passport,
而 Passport 對於不同的驗證方式, 需要載入相對應的 Strategies,
這邊先採用 LocalStrategy 也就是常見的 username/password 驗證,
另外要保持使用者的登入狀態, 就需要 session 機制, 所以也需要預先安裝 express-session,

npm i --save passport passport-local express-session

再來就是需要一些使用者的資料囉!
基本上應該要存放在 DB 上來管理, 不過這邊我們是要先實作驗證機制,
就直接在 app.js 程式中放些使用者的資料!

app.js
1
2
3
4
5
6
7
8
9
10
var Users = {
test1:{
name: 'test1',
password: 'password1'
},
test2:{
name: 'test2',
password: 'password2'
}
};

接著在程式中引用 passportpassport-local
並且在 Express routing 設定前將 passport middleware 載入

app.js
1
2
3
4
5
6
7
8
9
var passport = require('passport')
, LocalStrategy = require('passport-local').Strategy;
app.use(require('express-session')({
secret: 'keyboard cat', // <= 這邊為官網範例預設值, 正式環境下請記得修改
resave: false,
saveUninitialized: false
}));
app.use(passport.initialize());
app.use(passport.session());

這邊有幾點要注意的

  1. express-session 必須在 passport.session() 使用前先載入
  2. express-session 的 option 中有個 secre 是必要選項,
    主要是將存在使用者端的 session cookie, 計算 hash 後存放,
    這邊使用的是官網的範例, 建議正式發布需要進行修改, 以免被竄改
  3. passport.initialize() 務必在 express 設定 routing 前使用,
    否則登入驗證程式無法正常運作

接著將加入驗證檢查的程式

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
passport.use(new LocalStrategy(
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' });
}
return done(null, user);
}
));
passport.serializeUser(function (user, done) {
done(null, user.name);
});
passport.deserializeUser(function (username, done) {
done(null, Users[username]);
});

LocalStrategy 的 callback function 中, username 及 password 欄位預設對應 login form 的帳號/密碼欄位名稱,
如果不想使用預設欄位名稱則可以參考以下用法

1
2
3
4
5
6
7
8
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'passwd'
},
function(username, password, done) {
// ...
}
));

另外 serializeUser 是將使用者資訊存在 session 中,
一般是使用 user ID 以便在資料庫中查詢, 這邊則是用 username
deserializeUser 則是讓 express 的 req.user 可以取得目前的使用者詳細資料

由於 Express 預設的 view engine 是 jade, 接著就在 views 資料中加入 login.jade

login.jade
1
2
3
4
5
6
7
8
9
10
11
12
extends layout
block content
form(action="/login", method="post")
div
label Username:
input(type="text", name="username")
div
label Password:
input(type="password", name="password")
div
input(type="submit", value="Log In")

接著繼續在 app.js 中加入登入頁面的 routing 設定, 當然也別忘了登出功能

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.get('/login', function(req, res, next) {
res.render('login');
});
app.post('/login',
passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/login'
})
);
app.get('/logout', function (req, res) {
req.logout();
res.redirect('/');
});

到這邊基本的使用者驗證功能就完成了,
而我們要如何在頁面中取得使用者的資訊呢?
還記得前面所說的, 可以從 req.user 可以取得目前的使用者詳細資料,
例如我們要在 index 頁面中顯示使用者資訊, 就可以如下面的程式傳遞到 view 頁面中

1
2
3
app.get('/', function(req, res, next) {
res.render('index', { title: 'Express', user: req.user });
});

但是想要在每個頁面都知道登入狀態及使用者資訊, 這麼做就很麻煩,
這時候就可以使用 express middleware 的方式, 讓每個頁面載入前呼叫這段程式

app.js
1
2
3
4
5
app.use(function (req, res, next) {
res.locals.user = req.user;
res.locals.isAuthenticated = req.isAuthenticated();
next();
});

這樣每個 view 頁面就可以加入下面的程式,
或是放到 partial 的頁面, 這樣整個功能就完成了!

1
2
3
4
5
if(isAuthenticated)
p Hello #{user.name}!
a(href="/logout") 登出
else
a(href="/login") 登入

至於後續的修改有時間再繼續寫吧!

參考資料

Express API reference
Passport Document

範例程式下載