Node 認(rèn)證中間件 Passport 學(xué)習(xí)筆記

1. 綜述 GENERAL

Simple, unobtrusive authentication for Node.js

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ù)接受 usernamepassword 兩個(gè)參數(shù)。

1.4.2. 表格 Form

表格提供了 usernamepassword 這兩個(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 策略使用 usernamepassword 作為認(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 通過暴露給 reqlogin() 方法建立登錄會(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ù)器重定向流妻。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市笆制,隨后出現(xiàn)的幾起案子绅这,更是在濱河造成了極大的恐慌,老刑警劉巖在辆,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件君躺,死亡現(xiàn)場離奇詭異,居然都是意外死亡开缎,警方通過查閱死者的電腦和手機(jī)棕叫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奕删,“玉大人俺泣,你說我怎么就攤上這事⊥瓴校” “怎么了伏钠?”我有些...
    開封第一講書人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長谨设。 經(jīng)常有香客問我熟掂,道長,這世上最難降的妖魔是什么扎拣? 我笑而不...
    開封第一講書人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任赴肚,我火速辦了婚禮素跺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘誉券。我一直安慰自己指厌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開白布踊跟。 她就那樣靜靜地躺著踩验,像睡著了一般。 火紅的嫁衣襯著肌膚如雪商玫。 梳的紋絲不亂的頭發(fā)上箕憾,一...
    開封第一講書人閱讀 49,816評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音拳昌,去河邊找鬼袭异。 笑死,一個(gè)胖子當(dāng)著我的面吹牛地回,可吹牛的內(nèi)容都是我干的扁远。 我是一名探鬼主播俊鱼,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼刻像,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了并闲?” 一聲冷哼從身側(cè)響起细睡,我...
    開封第一講書人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎帝火,沒想到半個(gè)月后溜徙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡犀填,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年蠢壹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片九巡。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡图贸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出冕广,到底是詐尸還是另有隱情疏日,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布撒汉,位于F島的核電站沟优,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏睬辐。R本人自食惡果不足惜挠阁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一宾肺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鹃唯,春花似錦爱榕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至洪橘,卻和暖如春跪者,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背熄求。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來泰國打工渣玲, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人弟晚。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓忘衍,卻偏偏與公主長得像,于是被迫代替她去往敵國和親卿城。 傳聞我的和親對(duì)象是個(gè)殘疾皇子枚钓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容