passportjs 筆記

passportjs 筆記

十二月 06, 2017

passportjs 筆記

Imgur

環境要求

Nodejs,然後資料庫用來存儲用戶數據;另外passport作為中間件,需要依賴Express和Connect,還有由於Express 3.x或4.x以後的版本將一些中間件分離出去,因此你還需要先安裝它們。其中express-flash是用於顯示提示信息的中間件,是可選則的,如果需要用到passport中的提示,則需要安裝。

主要的中間件

  • Express:web框架。或其他支持的框架。
  • Connect:中間件框架。
  • cookie-parser:Connect的cookie解析中間件。
  • express-session:Connect的session解析中間件,依賴於cookie-parser。
  • express-flash:express的消息提示中間件,可選,但一般情況下都需要裝。

安裝和配置

你最少需要安裝一個passport 策略 來使用它,一般而言本地驗證策略passport-local是必裝的。

1
2
npm install passport
npm install passport-local

安裝完成後需要配置中間件,一般的順序如下:

1
2
3
4
5
6
7
8
9
10
11
12
var express = require('express');
var cookieParser = require('cookie-parser');
var session = require('express-session');
var flash = require('express-flash');
var passport = require('passport');
...

app.use(cookieParser());
app.use(session({...}));
app.use(passport.initialize());
app.use(passport.session());
app.use(flash())

其中重要的是app.use()部分,express中的中間件順序很重要,注意不要弄錯,除非你知道不同中間件間的準確相依關係。


基本用法

passport用的比較多的有local本地驗證和OAuth驗證。

你也可以看看這兩篇,Express結合Passport實現登陸認證Passport實現社交網絡OAuth登陸,裡面的示例涵蓋了基本的用法,本文也參考
了其中的一些例子。

  • local本地驗證
  • 配置策略
  • usernameField
  • 驗證回調
  • 密碼驗證
  • session序列化與反序列化
  • Authenticate驗證
  • HTTP請求操作
  • 完整範例
  • OAuth驗證
  • OAuth驗證流程
  • OAuth1.0
  • OAuth2.0的
  • 使用passport-x插件
  • OAuth驗證的邏輯

local本地驗證

本地驗證預設使用用戶名和密碼來進行驗證。

配置策略

在做驗證之前,首先需要對策略進行配置,官方的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var passport = require('passport')
, LocalStrategy = require('passport-local').Strategy;

passport.use(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: '用户名不存在.' });
}
if (!user.validPassword(password)) {
return done(null, false, { message: '密码不匹配.' });
}
return done(null, user);
});
}
));

其中的是MongoDB的語法,意思是從數據庫的User集合中查詢一條數據,第一個參數是查詢條件,後面是callback,一般在callback中進行後續操作。User.findOne()

這裡的邏輯很簡單,依次檢查、,如果出錯則返回錯誤信息,如果通過則返回。usernamepassworddone(null,user)

usernameField

前面說過passport默認使用用戶名和密碼來驗證,但實際上也有很多需要用郵箱來驗證的,那麼如何實現呢?

passport在策略配置裡提供了options參數,用來設置你要驗證的字段名稱,即usernameField,使用方法如下:

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

注意,這裡的字段名稱應該是頁面表單提交的名稱,即req.body.xxx,而不是user數據庫中的字段名稱。

將options作為LocalStrategy第一個參數傳入即可。

驗證callback

passport本身不處理驗證,驗證方法在策略配置的回調函數里由用戶自行設置,它又稱為驗證回調。驗證回調需要返回驗證結果,這是由done()來完成的。

在passport.use()裡面,done()有三種用法:

  • 當發生系統級異常時,返回done(err),這裡是數據庫查詢出錯,一般用next(err),但這裡用done(err),兩者的效果相同,都是返回error信息;

  • 當驗證不通過時,返回done(null, false, message),這裡的message是可選的,可通過express-flash調用;

  • 當驗證通過時,返回done(null, user)。

密碼驗證

一般對密碼進行hash和鹽化(solt)的Nodejs模塊是bcrypt,它提供一個compare方法來驗證密碼,如何使用它則超出本文的範圍,這裡就不講了。

session序列化與反序列化

驗證用戶提交的憑證是否正確,是與session中儲存的對象進行對比,所以涉及到從session中存取數據,需要做session對象序列化與反序列化。調用代碼如下:

1
2
3
4
5
6
7
8
9
passport.serializeUser(function(user, done) {
done(null, user.id);
});

passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});

這裡第一段代碼是將環境中的user.id序列化到session中,即sessionID,同時它將作為憑證存儲在用戶cookie中。

第二段代碼是從session反序列化,參數為用戶提交的sessionID,若存在則從數據庫中查詢user並存儲與req.user中。

這段代碼的順序可以放在passport.use()的前面或後面,但需要在app.configure()之前。

PS:其實這段只要到官方文件照抄就好不太需要變動到。

Authenticate驗證

做完了上面這些設置,我們終於可以開始做驗證了。

1
2
3
4
5
6
7
8
9
app.post('/login',
passport.authenticate('local',
{ successRedirect: '/',
failureRedirect: '/login',
failureFlash: true }),
function(req, res) {
// 验证成功则调用此回调函数
res.redirect('/users/' + req.user.username);
});

這裡的passport.authenticate(‘local’)就是中間件,若通過就進入後面的回調函數,並且給res加上res.user,若不通過則默認返回401錯誤。
uthenticate()方法有3個參數,第一是name,即驗證策略的名稱,第二個是options,包括下列屬性:

  • session:Boolean。設置是否需要session,默認為true
  • successRedirect:String。設置當驗證成功時的跳轉鏈接
  • failureRedirect:String。設置當驗證失敗時的跳轉鏈接
  • failureFlash:Boolean or String。設置為Boolean時,express-flash將調用use()裡設置
    的message。設置為String時將直接調用這裡的信息。
  • successFlash:Boolean or String。使用方法同上。

第三個參數是callback。注意如果使用了callback,那麼驗證之後建立session和發出響應都應該由這個callback來做,passport中間件之後不應該再有其他中間件或callback。以下是代碼:

1
2
3
4
5
6
7
8
9
10
app.get('/login', function(req, res, next) {
passport.authenticate('local', function(err, user, info) {
if (err) { return next(err); }
if (!user) { return res.redirect('/login'); }
req.logIn(user, function(err) {
if (err) { return next(err); }
return res.redirect('/users/' + user.username);
});
})(req, res, next);
});

HTTP請求操作

注意上面的代碼裡有個req.logIn(),它不是http模塊原生的方法,也不是express中的方法,而是passport加上的,passport擴展了HTTP request,添加了四種方法。

  • logIn(user, options, callback):用login()也可以。作用是為登錄用戶初始化session。options可設置session為false,即不初始化session,默認為true。
  • logOut():別名為logout()。作用是登出用戶,刪除該用戶session。不帶參數。
  • isAuthenticated():不帶參數。作用是測試該用戶是否存在於session中(即是否已登錄)。若存在返回true。事實上這個比登錄驗證要用的更多,畢竟session通常會保留一段時間,在此期間判斷用戶是否已登錄用這個方法就行了。
  • isUnauthenticated():不帶參數。和上面的作用相反。

完整範例

基本上passport本地驗證的知識點就是這些,下面給出一個相對完整的示例,包括bcrypt的實現,這裡借用了nodeclub中的方法,為實現它你需要自己配置hash:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
var express = require('express');
var cookieParser = require('cookie-parser');
var session = require('express-session');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
//User模型需自己實作
var User = require('../models/User');
var bcrypt = require('bcrypt');

passport.serializeUser(function(user, done) {
done(null, user.id);
});

passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});

//這裡username可以改成前端表單對應的名稱,如:
// <form><input type="text" name="hehe">...</form>
//這裡將所有的username改成hehe
passport.use(new LocalStrategy({ usernameField: 'username' }, function(username, password, done) {
//實現用户名或電子郵件登錄
//這裡判斷提交上的username是否含有@,來決定查詢的字段是哪一個
var criteria = (username.indexOf('@') === -1) ? {username: username} : {email: username};
User.findOne(criteria, function(err, user) {
if (!user) return done(null, false, { message: '用戶名或電子郵件 ' + username + ' 不存在'});
bcompare(password, hash, function(err, isMatch) {
if (isMatch) {
return done(null, user);
} else {
return done(null, false, { message: '密码不符' });
}
});
});
}));

...

app.use(cookieParser());
app.use(session({secret: "need change"}));
app.use(passport.initialize());
app.use(passport.session());
app.use(flash());

...

app.post('/login', passport.authenticate('local', function(err, user, info) {
if (err) return next(err);
if (!user) {
req.flash('errors', { msg: info.message });
return res.redirect('/login');
}
req.logIn(user, function(err) {
if (err) return next(err);
req.flash('success', { msg: '登入成功!' });
res.redirect('/');
});
})(req, res, next)
);
//getUser方法需要自定義
app.get('/user', isAuthenticated, getUser);
app.get('/logout', function(req, res){
req.logout();
res.redirect('/');
});

//將req.isAuthenticated()封裝成中間件
var isAuthenticated = function(req, res, next) {
if (req.isAuthenticated()) return next();
res.redirect('/login');
};

var bcompare = function (str, hash, callback) {
bcrypt.compare(str, hash, callback);
};

基本的local驗證就到這裡,下篇還有進階的驗證技巧,比如在RESTful API中使用passport,驗證多個條件等。

OAuth驗證

OAuth驗證是體現passport強大的地方,如果你看過nodeclub的源碼,會發現它自己實現了local驗證,但它的Github驗證是用passport來實現的。

OAuth標準分為兩個版本,1.0版和2.0版,兩者被使用的都很廣泛,passport通過passport-oauth為兩者提供支持,使用下面的命令可以安裝。

1
npm install passport-oauth

OAuth驗證流程

OAuth1.0和2.0的使用流程都差不多,一般來說如下:

  1. 為你的app去第三方服務商處申請標識和令牌appkey和secret;
  2. 在你的app裡添加按鈕或鏈接,將用戶引導至服務商的授權頁,用戶在這裡選擇授權給你的app;
  3. 授權成功後跳轉回你的app,同時還傳遞回access_token和一些用戶資料。

到這里首次驗證流程就完成了,之後只要拿access_token去就可以做登錄驗證或者其他事了。

OAuth1.0

要使用passport OAuth1.0驗證你需要先引入:

1
2
var passport = require('passport')
, OAuthStrategy = require('passport-oauth').OAuthStrategy;

然後是配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
passport.use('provider', new OAuthStrategy({
requestTokenURL: 'https://www.provider.com/oauth/request_token',
accessTokenURL: 'https://www.provider.com/oauth/access_token',
userAuthorizationURL: 'https://www.provider.com/oauth/authorize',
consumerKey: '123-456-789',
consumerSecret: 'shhh-its-a-secret'
callbackURL: 'https://www.example.com/auth/provider/callback'
},
function(token, tokenSecret, profile, done) {
User.findOrCreate(..., function(err, user) {
done(err, user);
});
}
));

這裡比通用流程多的一點就是,你的App需要先訪問第三方服務,獲取request token,這個request token是未授權的,等用戶授權之後,可以拿這個request token去換取access token。

在passport中你不必管這些細節,找到第三服務的文檔找到對應的URL添上即可。當然你還得申請key和secret。

use方法的回調接受四個參數,token就是access token,和tokenSecret一起好好保存。profile則是用戶在第三方服務上的一些公開資料,它的模型在這裡,不過返回的資料不一定全面,在使用前需要驗證是否存在。

OAuth2.0的

OAuth2.0的驗證不需要request_token,但比1.0多了scope和refresh token,我們先來看看具體的配置方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var passport = require('passport')
, OAuth2Strategy = require('passport-oauth').OAuth2Strategy;

passport.use('provider', new OAuth2Strategy({
authorizationURL: 'https://www.provider.com/oauth2/authorize',
tokenURL: 'https://www.provider.com/oauth2/token',
clientID: '123-456-789',
clientSecret: 'shhh-its-a-secret'
callbackURL: 'https://www.example.com/auth/provider/callback'
},
function(accessToken, refreshToken, profile, done) {
User.findOrCreate(..., function(err, user) {
done(err, user);
});
}
));

refreshToken是重新獲取access token的方法,因為access token是有使用期限的,到期了必須讓用戶重新授權才行,現在有了refresh token,你可以讓應用定期的用它去更新access token,這樣第三方服務就可以一直綁定了。不過這個方法並不是每個服務商都提供,注意看服務商的文檔。

下面是路由,OAuth2.0也有一點不同:

1
2
3
4
5
6
app.get('/auth/provider',
passport.authenticate('provider', { scope: 'email' })
);
app.get('/auth/provider/callback',
passport.authenticate('provider', { successRedirect: '/',
failureRedirect: '/login' }));

scope是權限範圍,需要在服務商處事先申請,想進一步了解可參考微博的scope文檔。它可以只有一項,也可以有多項,當為多項時以數組形式表示。

使用passport-x插件

passport-oauth包含通用的驗證方法,基本山任何提供OAuth的服務都能用上面的方法來驗證,但大部分提供第三方登錄的網站都有passport的插件,它們的列表見官網Github wiki。使用它們可以讓app綁定第三方服務更加簡單和模塊化。
passport-x插件的一般用法如下(以Github為例)。

首先安裝passport-github,注意這種情況不需要安裝passport-oauth:

npm install passport-github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var passport = require('passport')
, GithubStrategy = require('passport-github').Strategy;

//passport设置部分
passport.use(new GithubStrategy({
clientID: GITHUB_CLIENT_ID,
clientSecret: GITHUB_CLIENT_SECRET,
callbackURL: "http://www.example.com/auth/github/callback"
},
function(accessToken, refreshToken, profile, done) {
User.findOrCreate(..., function(err, user) {
if (err) { return done(err); }
done(null, user);
});
}
));

...
//路由部分
app.get('/auth/github', passport.authenticate('github'));
app.get('/auth/github/callback',
passport.authenticate('github', { failureRedirect: '/login' }),
function(req, res) {
res.redirect('/');
});
```
與通用OAuth驗證流程對比,上面的代碼少了服務商的驗證頁部分,你只需要將獲得的appkey和secret填到對應地方即可。
### OAuth驗證的邏輯
OAuth驗證的麻煩之處主要是處理邏輯,很多網站將第三方的OAuth作為一種用戶註冊手段,當用戶點擊第三方登錄時,若用戶未註冊會為他們創建賬號,這裡面的邏輯就比較繞了。比如Hackathon Starter的處理邏輯如下:

/**

  • OAuth驗證策略
  • 用戶點擊“使用XX登陸”連接
    • 若用户已登陸
    • 檢查該用户是否已經绑定XX服務
  • - 如果已绑定,返回錯誤(不允許帳戶合併)
    
  • - 否則開始驗證流程,為該用户綁定XX服務
    
    • 用戶未登入
    • 檢查是否是老用戶
  • - 如果是老用户,則登入
    
  • - 否則檢查OAuth返回profile中的email,是否在用戶存在於資料庫中
    
  •   - 如果存在,返回錯誤訊息
    
  •   - 否則創建一個新帳號
    
  • /
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    另外還有平常驗證用戶是否已綁定某個服務,可以封裝成中間件:
    ```js
    var isAuthorized = function(req, res, next) {

    if (req.user.provider)) {
    next();
    } else {
    //do something else
    }
    };
    學習OAuth驗證最好的項目是Hackathon Starter,它實現了十幾種的第三方網站和服務的OAuth驗證,推薦學習。下面進階學習裡面還有如何開發一個passport OAuth驗證插件。