1. 綜述 GENERAL
Simple, unobtrusive authentication for Node.js
- 1. 綜述 GENERAL
- 2. 內(nèi)置操作 Operations
- 3. 具體流程
1.1. 概覽 Overview
Passport 是 Node 的認(rèn)證中間件,它的存在只有一個(gè)單一的目的凌停,就是認(rèn)證請(qǐng)求。
在現(xiàn)今的網(wǎng)絡(luò)應(yīng)用中認(rèn)證方式多種多樣成榜。經(jīng)典做法是用戶提供用戶名和密碼進(jìn)行登錄,但隨著社交網(wǎng)絡(luò)的興起劫哼,OAuth 等基于口令的方式越來越受到歡迎划乖。
在 Passport 設(shè)計(jì)理念中就認(rèn)為每一個(gè)網(wǎng)絡(luò)應(yīng)用擁有不同的認(rèn)證需求。因此為了滿足各種網(wǎng)絡(luò)應(yīng)用,被稱作策略的認(rèn)證機(jī)制被封裝成單一的模塊中木张,網(wǎng)絡(luò)應(yīng)用可自由選擇不同策略來實(shí)現(xiàn)認(rèn)證功能。
認(rèn)證機(jī)制本身可能比較復(fù)雜端三,但是在 Passport 中編寫的代碼并沒有那么繁瑣:
app.post('/login', passport.authenticate('local', { successRedirect: '/',
failureRedirect: '/login' }));
當(dāng)然首先需要安裝 Passport:
$ npm install passport
1.2. 認(rèn)證 Authenticate
通過調(diào)用 passport.authenticate()
方法及配置相應(yīng)的策略舷礼,就可實(shí)現(xiàn)認(rèn)證網(wǎng)絡(luò)請(qǐng)求。 authenticate()
方法是標(biāo)準(zhǔn)的Node 中間件郊闯,在 Express 應(yīng)用中可以非常方便的作為路由使用妻献。
app.post('/login',
passport.authenticate('local'),
function(req, res) {
// 如果認(rèn)證通過,將觸發(fā)該函數(shù)
// `req.user` 字段內(nèi)有認(rèn)證的用戶名.
res.redirect('/users/' + req.user.username);
});
在默認(rèn)情況下团赁,認(rèn)證失敗后 Passport 會(huì)返回 401 Unauthorized
狀態(tài)的響應(yīng)育拨,其后面的處理函數(shù)也不會(huì)被觸發(fā)。當(dāng)然認(rèn)證成功后欢摄,處理函數(shù)被觸發(fā)并將認(rèn)證用戶信息作為值賦值給 req.user
熬丧。
注意: 策略必須在路由使用它之前進(jìn)行配置。
1.2.1. 重定向 Redirects
在認(rèn)證請(qǐng)求后通常接下來需要處理的事務(wù)就是重定向怀挠。
app.post('/login',
passport.authenticate('local', { successRedirect: '/',
failureRedirect: '/login' }));
上述代碼中析蝴,如果認(rèn)證成功將會(huì)被重定向到首頁,如果失敗會(huì)重新來到登錄頁面绿淋。
1.2.2. 快報(bào) Flash Messages
當(dāng)認(rèn)證失敗后客戶端被重定向到登錄頁面闷畸,重定向后的頁面中經(jīng)常會(huì)顯示一些狀態(tài)提示信息,如:登錄失敗吞滞。那么如何分辨登錄頁面是認(rèn)證失敗后的重定向還是第一次訪問佑菩?區(qū)別它們的方法就是在 session 中寫入一個(gè)特殊的標(biāo)識(shí) flash
。
app.post('/login',
passport.authenticate('local', { successRedirect: '/',
failureRedirect: '/login',
failureFlash: true })
);
上述代碼會(huì)在認(rèn)證失敗后裁赠,將策略的認(rèn)證函數(shù)中返回的信息寫入 flash
殿漠。這是最常用和最好的方法,因?yàn)槊總€(gè)策略的認(rèn)證函數(shù)都會(huì)把認(rèn)證失敗的準(zhǔn)確信息返回佩捞。
當(dāng)然也可以自定義快報(bào):
passport.authenticate('local', { failureFlash: 'Invalid username or password.' });
或者是認(rèn)證成功后的快報(bào):
passport.authenticate('local', { successFlash: 'Welcome!' });
注意: 在 Express 4.x 中使用快報(bào)需要添加
connect-flash
中間件凸舵。
1.2.3. 禁用會(huì)話 Disable Sessions
由于 HTTP 是無狀態(tài)協(xié)議,所以在認(rèn)證成功后通常是將登錄信息保存在 session 中失尖。但是在某些情況下并不需要 session啊奄,如 API 服務(wù)需要證書來認(rèn)證,此時(shí)可以放心的禁用會(huì)話掀潮。
app.get('/api/users/me',
passport.authenticate('basic', { session: false }),
function(req, res) {
res.json({ id: req.user.id, username: req.user.username });
});
1.2.4. 自定義回調(diào)函數(shù) Custom Callback
在處理認(rèn)證請(qǐng)求時(shí)菇夸,可自定義回調(diào)函數(shù)來處理成功或失敗的認(rèn)證。
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);
});
上例中仪吧,方法 authenticate()
沒有作為路由的中間件出現(xiàn)庄新,而是在路由的處理函數(shù)中被調(diào)用。該方法的回調(diào)函數(shù)的參數(shù)通過閉包從路由中傳入。如果認(rèn)證失敗 user
將被賦值為 false
择诈。出現(xiàn)異常 err
會(huì)被初始化并返回異常信息械蹋。可選的 info
則返回策略中相關(guān)信息羞芍。
注意: 使用自定義回調(diào)函數(shù)哗戈,應(yīng)用必須建立一個(gè) session (上例通過
req.logIn()
) 并發(fā)送響應(yīng)。
1.3. 配置 Configure
在認(rèn)證過程中 Passport 需要配置三個(gè)部分:
- 認(rèn)證策略
- 應(yīng)用中間件
- 會(huì)話(可選)
1.3.1. 策略 Strategies
Passport 通過策略來認(rèn)證網(wǎng)絡(luò)請(qǐng)求荷科。策略可以是用戶名和密碼的確認(rèn)認(rèn)證唯咬,可以是 OAuth 的委派認(rèn)證,還可以是 OpenID 的聯(lián)合認(rèn)證畏浆。正如上文所提到的策略在使用前必須進(jìn)行配置胆胰。
策略及其配置通過 use()
方法實(shí)現(xiàn)。比如接下來的通過用戶名和密碼的認(rèn)證策略 LocalStrategy
:
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: 'Incorrect username.' });
}
if (!user.validPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
}
));
1.3.2. 驗(yàn)證回調(diào) Verify Callback
上面的例子中有個(gè)很重要的概念:策略中需要驗(yàn)證回調(diào)刻获。驗(yàn)證回調(diào)目的是找到擁有認(rèn)證信息的用戶蜀涨。
當(dāng) Passport 驗(yàn)證一個(gè)請(qǐng)求時(shí),會(huì)解析請(qǐng)求中的認(rèn)證信息蝎毡,然后將認(rèn)證信息發(fā)送給驗(yàn)證回調(diào)函數(shù)同時(shí)觸發(fā)該函數(shù)勉盅。如果認(rèn)證信息有效,驗(yàn)證回調(diào)函數(shù)觸發(fā) done
函數(shù)顶掉,將認(rèn)證的用戶信息返回給 Passport 草娜。
return done(null, user);
如果認(rèn)證信息無效, done
函數(shù)同樣也會(huì)被觸發(fā)痒筒,但是返回的是 false
宰闰。
return done(null, false);
一些附加信息可以追加在 done
函數(shù)的第三個(gè)參數(shù)中,這些信息可用來呈現(xiàn)快報(bào)簿透。
return done(null, false, { message: 'Incorrect password.' });
最后如果在驗(yàn)證過程中出現(xiàn)異常移袍,done
函數(shù)可以傳遞一個(gè) Node 風(fēng)格的 err 信息。
return done(err);
注意: 區(qū)分開兩種驗(yàn)證失敗的原因老充,如果是服務(wù)器的異常
done
函數(shù)的第一個(gè)參數(shù)err
設(shè)置為非空值葡盗;驗(yàn)證條件的失敗要確保err
為 null 。
這種委派方式確保了 Passport 數(shù)據(jù)庫對(duì)驗(yàn)證回調(diào)函數(shù)的透明啡浊,應(yīng)用可任意選擇用戶信息的存儲(chǔ)方式觅够。
1.3.3. 中間件 Middleware
在 Express 應(yīng)用中, passport.initialize()
中間件可初始化 Passport巷嚣, passport.session()
中間件用來存儲(chǔ)用戶登錄的 session 信息喘先。
var app = express();
app.use(require('serve-static')(__dirname + '/../../public'));
app.use(require('body-parser').urlencoded({ extended: true }));
app.use(require('express-session')({ secret: 'keyboard cat', resave: true, saveUninitialized: true }));
app.use(passport.initialize());
app.use(passport.session());
注意:
express.session()
中間件應(yīng)該在passport.session()
中間件前面。
1.3.4. 會(huì)話 Session
在典型的網(wǎng)絡(luò)應(yīng)用中廷粒,登錄請(qǐng)求中包含驗(yàn)證用戶的認(rèn)證信息窘拯。如果認(rèn)證成功红且,用戶瀏覽器中通過 cookie 創(chuàng)建并保存 sessionID。隨后所有的請(qǐng)求不再需要驗(yàn)證涤姊,而是通過 sessionID 來識(shí)別用戶暇番。Passport 可以將 session 中的用戶信息序列化或反序列化,以此支持 session 機(jī)制思喊。
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 中,接下來的請(qǐng)求中搔涝,通過 user ID 獲取用戶信息并將其存入到 req.user
中。
序列化和反序列化的邏輯由應(yīng)用提供和措,可以選擇適合的數(shù)據(jù)庫存儲(chǔ)會(huì)話庄呈,在認(rèn)證層面這些操作沒有任何的限制。
1.4. 用戶名和密碼 Username & Password
網(wǎng)絡(luò)中最常用的方式就是通過用戶名和密碼進(jìn)行認(rèn)證派阱,提供這種認(rèn)證的策略是 passport-local
诬留。
$ npm install passport-local
1.4.1. 配置 Configuration
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: 'Incorrect username.' });
}
if (!user.validPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
}
));
本地認(rèn)證的驗(yàn)證回調(diào)函數(shù)接受 username
和 password
兩個(gè)參數(shù)。
1.4.2. 表格 Form
表格提供了 username
和 password
這兩個(gè)參數(shù):
<form action="/login" method="post">
<div>
<label>Username:</label>
<input type="text" name="username"/>
</div>
<div>
<label>Password:</label>
<input type="password" name="password"/>
</div>
<div>
<input type="submit" value="Log In"/>
</div>
</form>
1.4.3. 路由 Route
登錄表格信息通過 POST
方法提交給服務(wù)器贫母, local
策略使用 authenticate()
函數(shù)處理登錄請(qǐng)求:
app.post('/login',
passport.authenticate('local', { successRedirect: '/',
failureRedirect: '/login',
failureFlash: true })
);
設(shè)置 failureFlash
選項(xiàng)為 true
文兑,意味著在驗(yàn)證回調(diào)函數(shù)中返回的 message
信息將成為錯(cuò)誤快報(bào)的值。
1.4.4. 參數(shù) Parameters
默認(rèn)情況下腺劣,localStrategy
策略使用 username
和 password
作為認(rèn)證機(jī)制的參數(shù)绿贞,實(shí)際上其他字段也可以作為參數(shù)進(jìn)行驗(yàn)證:
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'passwd'
},
function(username, password, done) {
// ...
}
));
1.5. OpenID
暫時(shí)用不到 _
1.6. OAuth
暫時(shí)用不到 _
2. 內(nèi)置操作 Operations
2.1. 登錄 Log In
Passport 通過暴露給 req
的 login()
方法建立登錄會(huì)話。
req.login(user, function(err) {
if (err) { return next(err); }
return res.redirect('/users/' + req.user.username);
});
登錄操作完成后橘原,用戶信息被指派在 req.user
上籍铁。
注意:
passport.authenticate()
中間件會(huì)自動(dòng)觸發(fā)req.login()
。 這項(xiàng)功能主要使用在用戶注冊(cè)時(shí)趾断,調(diào)用req.login()
方法自動(dòng)登錄新注冊(cè)的用戶拒名。
2.2. 注銷 Log Out
與 login()
相反,logout()
方法用來結(jié)束登錄會(huì)話芋酌。 調(diào)用 logout()
方法會(huì)刪除 req.user
屬性并清除登錄會(huì)話增显。
app.get('/logout', function(req, res){
req.logout();
res.redirect('/');
});
2.3. 授權(quán) Authorize
暫時(shí)用不到 _
3. 具體流程
3.1. 第一次訪問 (GET)
- 客戶端訪問服務(wù)器登錄頁面。
- 服務(wù)器生成sessionid脐帝。 (express-session)
- 服務(wù)器將seesionid對(duì)應(yīng)的session保存在數(shù)據(jù)庫中同云。 (express-session)
- 在session中添加crsf。 (cursf)
- 服務(wù)器將sessionid保存到cookie中堵腹,返回給客戶端梢杭,同時(shí)將csrf token返回給客戶端。 (express-session)
- 客戶端保存cookie秸滴。
- 客戶端填寫登錄內(nèi)容武契。
3.2. 第二次訪問 (POST)
- 客戶端將登陸內(nèi)容、cookie和csrf token發(fā)送給服務(wù)器。
- 服務(wù)器查找對(duì)應(yīng)sessionid的session咒唆。 (express-session)
- 服務(wù)器驗(yàn)證csrf token届垫。 (cursf)
- 服務(wù)器驗(yàn)證登錄內(nèi)容。 (passport/autenticate)
- 服務(wù)器將用戶信息掛載在 req.user全释。 (passport/req.login)
- 將用戶信息序列化成userid保存到session中装处。 (passport/serialize)
- 服務(wù)器重定向。
3.3. 第三次訪問 (GET)
- 客戶端訪問頁面浸船。
- 客戶端發(fā)送cookie和csrf token妄迁。
- 服務(wù)器查找與sessionid對(duì)應(yīng)session。 (express-session)
- 服務(wù)器驗(yàn)證csrf token李命。(GET 方法忽略驗(yàn)證)
- 服務(wù)器將session中的userid反序列化登淘,附加到 req.user。 (passport/deserialize)
- 服務(wù)器返回用戶內(nèi)容封字。
3.4. 用戶注銷 (GET)
- 客戶端發(fā)送注銷信息
- 客戶端發(fā)送出去cookie和csrf token黔州。
- 服務(wù)器查找sessionid對(duì)應(yīng)的session。 (express-session)
- 服務(wù)器調(diào)用req.logout將userid從session刪除阔籽。 (passport/req.logout)
- 服務(wù)器重定向流妻。