koa-passport學(xué)習(xí)筆記

koa-passport是koa的一個(gè)中間件题翻,它實(shí)際上只是對(duì)passport的一個(gè)封裝揩徊。利用koa-passport可以簡(jiǎn)便的實(shí)現(xiàn)登錄注冊(cè)功能,不但包括本地驗(yàn)證嵌赠,還有很多提供第三方登錄的模塊可以使用塑荒。

基本流程

passport的主要功能就是能夠提供一個(gè)用戶鑒權(quán)的框架,并把鑒權(quán)得到的用戶身份供后續(xù)的業(yè)務(wù)邏輯來使用猾普。而鑒權(quán)的具體過程是通過插件來實(shí)現(xiàn)袜炕,用戶可以自己來實(shí)現(xiàn),也可以使用已有的第三方模塊實(shí)現(xiàn)各種方式的鑒權(quán)(包括OAuth或者OpenID)初家。

passport的代碼還是有點(diǎn)復(fù)雜的偎窘,我們先看一個(gè)最簡(jiǎn)單的例子,然后逐步介紹后面的細(xì)節(jié)溜在。

最小樣例

下面是一個(gè)使用koa-passport的可以執(zhí)行的最小樣例

const Koa = require('koa')
const app = new Koa()

// 定義一個(gè)驗(yàn)證用戶的策略陌知,需要定義name作為標(biāo)識(shí)
const naiveStrategy = {
  name: 'naive',
  // 策略的主體就是authenticate(req)函數(shù),在成功的時(shí)候返回用戶身份掖肋,失敗的時(shí)候返回錯(cuò)誤
  authenticate: function (req) {
    let uid = req.query.uid
    if (uid) {
      // 策略很簡(jiǎn)單仆葡,就是從參數(shù)里獲取uid,然后組裝成一個(gè)user
      let user = {id: parseInt(uid), name: 'user' + uid}
      this.success(user)
    } else {
      // 如果找不到uid參數(shù)志笼,認(rèn)為鑒權(quán)失敗
      this.fail(401)
    }
  }
}

// 調(diào)用use()來為passport新增一個(gè)可用的策略
const passport = require('koa-passport')
passport.use(naiveStrategy)
// 添加一個(gè)koa的中間件沿盅,使用naive策略來鑒權(quán)。這里暫不使用session
app.use(passport.authenticate('naive', {session: false}))

// 業(yè)務(wù)代碼
const Router = require('koa-router')
const router = new Router()
router.get('/', async (ctx) => {
  if (ctx.isAuthenticated()) {
    // ctx.state.user就是鑒權(quán)后得到的用戶身份
    ctx.body = 'hello ' + JSON.stringify(ctx.state.user)
  } else {
    ctx.throw(401)
  }
})
app.use(router.routes())

// server
const http = require('http')
http.createServer(app.callback()).listen(3000)

這段代碼雖然沒有實(shí)際意義纫溃,但是已經(jīng)展示完整的鑒權(quán)流程和錯(cuò)誤處理腰涧,運(yùn)行結(jié)果如下

$ curl http://localhost:3000/\?uid\=128
hello {"id":128,"name":"user128"}%
$ curl http://localhost:3000
Unauthorized%  

可以看到這里鑒權(quán)的作用基本就是兩個(gè)

  1. 鑒權(quán)失敗可以拒絕用戶訪問(也可以不做處理)
  2. 鑒權(quán)成功會(huì)把用戶記錄到context

主流程

明白了上面的例子之后,我們就可以照著代碼來看一下passport的主流程紊浩,定義在passport/authenticator.js文件中窖铡。

首先看看上面使用的passport.use()函數(shù),其內(nèi)容非常簡(jiǎn)單坊谁,就是把一個(gè)策略保存在本地费彼,后續(xù)可以通過name來訪問

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;
};

上面樣例中使用的中間件passport/authenticate(),實(shí)際定義在passport/middleware/authenticate.js中口芍,其返回值是一個(gè)函數(shù)箍铲,具體的實(shí)現(xiàn)如下,刪減了部分代碼鬓椭,加了一下注釋

module.exports = function authenticate(passport, name, options, callback) {
  // FK: 允許傳入一個(gè)策略的數(shù)組
  if (!Array.isArray(name)) {
    name = [ name ];
    multi = false;
  }
  
  return function authenticate(req, res, next) {
    // FK: 省略部分代碼...
    function allFailed() {
       // FK: 所有策略都失敗后虹钮,如果設(shè)置了回調(diào)就調(diào)用聋庵,否則找到第一個(gè)failure,根據(jù)option進(jìn)行flash/redirect/401
    }
    
    // FK: 這部分是主要邏輯
    (function attempt(i) {
      // FK: 嘗試第i個(gè)策略
      var layer = name[i];
      if (!layer) { return allFailed(); }
    
      var prototype = passport._strategy(layer);  
      var strategy = Object.create(prototype);
   
      strategy.success = function(user, info) {
        // FK: 省略部分代碼芙粱,用于記錄flash/message

        // FK: 鑒權(quán)成功后會(huì)調(diào)用logIn(),把用戶寫入當(dāng)前ctx
        req.logIn(user, options, function(err) {
          if (err) { return next(err); }          
          // FK: 成功跳轉(zhuǎn)氧映,代碼比較復(fù)雜春畔,省略了
      };
      
      strategy.fail = function(challenge, status) {
        // FK: 記錄錯(cuò)誤,使用下一個(gè)策略進(jìn)行嘗試岛都,省略部分代碼
        attempt(i + 1);
      };
      
      strategy.redirect = function(url, status) {
        // FK: 處理跳轉(zhuǎn)
        res.statusCode = status || 302;
        res.setHeader('Location', url);
        res.setHeader('Content-Length', '0');
        res.end();
      };
      
      strategy.authenticate(req, options);
    })(0); // attempt
  };  

這部分代碼就是passport的核心流程律姨,其實(shí)就是定義好一些鑒權(quán)成功/失敗/跳轉(zhuǎn)等處理機(jī)制,然后就調(diào)用具體的策略進(jìn)行鑒權(quán)臼疫。需要注意的是荣赶,雖然外面上面的樣例中只傳入了一個(gè)策略鸽斟,但是其實(shí)passport支持同時(shí)使用多個(gè)策略拔创,它會(huì)從頭開始嘗試各個(gè)策略,直到有一個(gè)策略做出處理或者已嘗試所有策略為止富蓄。

小結(jié)

通過上面的樣例和主流程的分析剩燥,大家應(yīng)該能清楚passport大概做的事情是什么,也能夠知道最基礎(chǔ)的使用方式立倍。

session

在上面的例子中灭红,我們沒有使用session变擒,因此每個(gè)請(qǐng)求都需要帶上uid參數(shù)。實(shí)際使用中疆导,一般會(huì)把鑒權(quán)后的用戶身份會(huì)保存在cookie中供后續(xù)請(qǐng)求來使用赁项。雖然passport并沒有要求一定使用session澈段,但其實(shí)是默認(rèn)會(huì)使用session败富。

在上面的樣例中悔醋,為了支持session,我們需要添加一些代碼兽叮。

首先芬骄,我們需要在app中開啟session支持猾愿,即使用koa-session

const session = require('koa-session')
app.keys = ['some secret']
const conf = {
  encode: json => JSON.stringify(json),
  decode: str => JSON.parse(str)
}
app.use(session(conf, app))

然后,因?yàn)槲覀兊挠脩粜畔⑿枰A粼趕ession存儲(chǔ)中(利用cookie或者服務(wù)端存儲(chǔ))账阻,因此需要定義序列化和反序列的操作蒂秘。下面的例子是一個(gè)示例。真實(shí)場(chǎng)景中淘太,反序列化的時(shí)候肯定需要根據(jù)uid來檢索真正的用戶信息姻僧。

passport.serializeUser(function (user, done) {
  // 序列化的結(jié)果只是一個(gè)id
  done(NO_ERROR, user.id)
})

passport.deserializeUser(async function (str, done) {
  // 根據(jù)id恢復(fù)用戶
  done(NO_ERROR, {id: parseInt(str), name: 'user' + str})
})

再然后,因?yàn)槲覀儾恍枰猲aiveStrategy作為默認(rèn)策略了蒲牧,因此要把相應(yīng)的use()語句去掉撇贺,轉(zhuǎn)而只在用戶明確要登錄的時(shí)候才調(diào)用

router.get('/login',
  passport.authenticate('naive', { successRedirect: '/' })
)

最后,我們需要在app中開啟koa-passport對(duì)session的支持

app.use(passport.initialize())
app.use(passport.session())

initialzie()函數(shù)的作用是只是簡(jiǎn)單為當(dāng)前context添加passport字段冰抢,便于后面的使用松嘶。而
passport.session()則是passport自帶的策略,用于從session中提取用戶信息挎扰,其代碼位于passport/strategies/session.js翠订,內(nèi)容如下

SessionStrategy.prototype.authenticate = function(req, options) {
  // FK: 確保已經(jīng)初始化
  if (!req._passport) { return this.error(new Error('passport.initialize() middleware not in use')); }
  options = options || {};

  // FK: 從session中獲取序列化后的user
  var self = this, su;
  if (req._passport.session) {
    su = req._passport.session.user;
  }

  if (su || su === 0) {
    // FK: 如果用戶字段存在,調(diào)用自定義的反序列化函數(shù)來獲取用戶信息
    this._deserializeUser(su, req, function(err, user) {
      if (err) { return self.error(err); }
      if (!user) {
        delete req._passport.session.user;
      } else {
        var property = req._passport.instance._userProperty || 'user';
        req[property] = user;
      }
      self.pass();
      // FK: 省略
    });
  } else {
    // FK: 如果在session中找不到用戶字段鼓鲁,直接略過
    self.pass();
  }
};

和我們上面自定義的naive策略類似蕴轨,session策略的作用也即生成用戶信息,只不過數(shù)據(jù)來源不是請(qǐng)求字段骇吭,而是session信息橙弱。

Session Manager

在上面的主流程里,我們看到當(dāng)鑒權(quán)成功時(shí)燥狰,會(huì)調(diào)用req.logIn()函數(shù)棘脐,其實(shí)還有l(wèi)ogOut()和isAuthenticated(),都定義在passport/http/request.js。

其中l(wèi)ogIn()和logOut()操作真正調(diào)用的是
SessionManager中的操作龙致,其定義在passport/sessionmanager.js,主要流程如下:

function SessionManager(options, serializeUser) {
  // FK: ... 省略
  this._key = options.key || 'passport';
  this._serializeUser = serializeUser;
}

SessionManager.prototype.logIn = function(req, user, cb) {
  this._serializeUser(user, req, function(err, obj) {
    if (err) {
      return cb(err);
    }
    // FK: ... 省略
    req._passport.session.user = obj;
    // FK: ... 省略
    req.session[self._key] = req._passport.session;
    cb();
  });
}

SessionManager.prototype.logOut = function(req, cb) {
  if (req._passport && req._passport.session) {
    delete req._passport.session.user;
  }
  cb && cb();
}

module.exports = SessionManager;

Session Manager里定義了login和logout兩個(gè)操作

  • logIn()操作蛀缝,會(huì)調(diào)用一個(gè)_serializeUser(), 然后把序列化的結(jié)果存到req.session['passport']。此時(shí)session的內(nèi)容類似 {"passport":{"user":128},"_expire":1517357892908,"_maxAge":86400000}
  • logOut()操作更加簡(jiǎn)單目代,就是直接刪除session里passport里的user字段屈梁。

此外,request還定義了isAuthenticated()函數(shù)榛了,用于檢查當(dāng)前是否已經(jīng)鑒權(quán)成功在讶,代碼如下

req.isAuthenticated = function() {
  var property = 'user';
  if (this._passport && this._passport.instance) {
    property = this._passport.instance._userProperty || 'user';
  }
  
  return (this[property]) ? true : false;
};

小結(jié)

至此,我們基本已經(jīng)看完了passport的主要工作霜大。passport之所以強(qiáng)大构哺,在于他定義好了框架,但并沒有確定具體的鑒權(quán)策略战坤,用戶可以根據(jù)需求來加入各種自定義的策略曙强,現(xiàn)在已經(jīng)有大量的模塊可以使用了残拐。

passport-local

在上面的樣例中,我們定義了自己的NaiveStrategy來實(shí)現(xiàn)對(duì)用戶的鑒權(quán)碟嘴,當(dāng)然上面的代碼毫無安全性可言溪食。在真實(shí)環(huán)境中,最簡(jiǎn)單的鑒權(quán)一般是用戶提交用戶名和密碼娜扇,然后服務(wù)端來校驗(yàn)密碼眠菇,準(zhǔn)確無誤后才認(rèn)為鑒權(quán)成功。

雖然這個(gè)過程可以通過擴(kuò)展我們的NaiveStrategy來實(shí)現(xiàn)袱衷,不過我們已經(jīng)有了passport-local這個(gè)庫提供了一個(gè)本地鑒權(quán)的代碼框架,可以直接使用笑窜。我們來看看其流程:

 // FK: passport-local 省略部分代碼
function Strategy(options, verify) {
  // FK: 記錄verify函數(shù)
  this._verify = verify;
  this._passReqToCallback = options.passReqToCallback;
}

Strategy.prototype.authenticate = function(req, options) {
  // 從req里找到username 和 pass
  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);
  }
  
  // FK: 省略部分代碼
  try {
    this._verify(username, password, verified);
  } catch (ex) {
    return self.error(ex);
  }
};

看代碼其實(shí)流程也非常簡(jiǎn)單致燥,就是自動(dòng)從請(qǐng)求中獲取usernamepassword兩個(gè)字段,然后提交給用戶自定義的verify函數(shù)進(jìn)行鑒權(quán)排截,然后處理鑒權(quán)的結(jié)果嫌蚤。

可以看到,這個(gè)框架做的事情其實(shí)很有限断傲,主要的校驗(yàn)操作還是需要用戶自己來定義脱吱,一個(gè)簡(jiǎn)單用法樣例如下:

const LocalStrategy = require('passport-local').Strategy
passport.use(new LocalStrategy(async function (username, password, done) {
  // FK: 根據(jù)username從數(shù)據(jù)庫或者其他存儲(chǔ)中拿到用戶信息
  let user = await userStore.getUserByName(username)
  // FK: 把傳入的password和數(shù)據(jù)庫中存儲(chǔ)的密碼進(jìn)行比較。當(dāng)然這里不應(yīng)該是明文认罩,一般是加鹽的hash值
  if (user && validate(password, user.hash)) {
    done(null, user)
  } else {
    log.info(`auth failed for`, username)
    done(null, false)
  }
}))

自此箱蝠,我們看到用戶只需要再定義一下用戶的存儲(chǔ)流程,基本上就可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的用戶注冊(cè)登錄功能了垦垂。

后記

本文記錄對(duì)koa-passport相關(guān)模塊的代碼學(xué)習(xí)過程宦搬,里邊細(xì)節(jié)比較多,行文有些亂劫拗,還請(qǐng)見諒间校。如果大家只是想?看使用方法,可以參考其他的文章页慷,比如這篇憔足。

和之前看koa-session模塊相比,passport模塊沒有使用async語法酒繁,就能明顯感受到回調(diào)地獄的威力滓彰,代碼一開始看的還是比較痛苦∮樱總體感覺js還是過于靈活了找蜜,如果用來寫業(yè)務(wù),一定需要非常強(qiáng)健的工程規(guī)范才行稳析。

文中兩個(gè)樣例的完整代碼請(qǐng)參見 https://github.com/fankaigit/vauth/tree/master/server/sample

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末洗做,一起剝皮案震驚了整個(gè)濱河市弓叛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌诚纸,老刑警劉巖撰筷,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異畦徘,居然都是意外死亡毕籽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門井辆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來关筒,“玉大人,你說我怎么就攤上這事杯缺≌舨ィ” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵萍肆,是天一觀的道長(zhǎng)袍榆。 經(jīng)常有香客問我,道長(zhǎng)塘揣,這世上最難降的妖魔是什么包雀? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮亲铡,結(jié)果婚禮上才写,老公的妹妹穿的比我還像新娘。我一直安慰自己奴愉,他們只是感情好琅摩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锭硼,像睡著了一般房资。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上檀头,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天轰异,我揣著相機(jī)與錄音,去河邊找鬼暑始。 笑死搭独,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的廊镜。 我是一名探鬼主播牙肝,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了配椭?” 一聲冷哼從身側(cè)響起虫溜,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎股缸,沒想到半個(gè)月后衡楞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡敦姻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年瘾境,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片镰惦。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡迷守,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出旺入,到底是詐尸還是另有隱情盒犹,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布眨业,位于F島的核電站,受9級(jí)特大地震影響沮协,放射性物質(zhì)發(fā)生泄漏龄捡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一慷暂、第九天 我趴在偏房一處隱蔽的房頂上張望聘殖。 院中可真熱鬧,春花似錦行瑞、人聲如沸奸腺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽突照。三九已至,卻和暖如春氧吐,著一層夾襖步出監(jiān)牢的瞬間讹蘑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來泰國打工筑舅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留座慰,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓翠拣,卻偏偏與公主長(zhǎng)得像版仔,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

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

  • koa-session是koa的session管理中間件蛮粮,最近在寫登錄注冊(cè)模塊的時(shí)候?qū)W習(xí)了一下這部分的代碼益缎,感覺還...
    fankaife閱讀 22,226評(píng)論 4 26
  • 1. 綜述 GENERAL Simple, unobtrusive authentication for Node...
    wlszouc閱讀 4,752評(píng)論 1 7
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法蝉揍,內(nèi)部類的語法链峭,繼承相關(guān)的語法,異常的語法又沾,線程的語...
    子非魚_t_閱讀 31,631評(píng)論 18 399
  • 這件事很小弊仪,舉手動(dòng)嘴之勞,發(fā)生也有些日子了杖刷,但其中的一些細(xì)節(jié)卻像刻在腦子里励饵,總惦記著擠點(diǎn)時(shí)間把它記下來。 ...
    慢靜舍閱讀 295評(píng)論 1 4
  • 為了柴米油鹽撒潑的時(shí)候滑燃,你可還記得役听,你一直不想改變的少女夢(mèng),曾經(jīng)多么夢(mèng)幻表窘,多么浪漫典予。 不知道為啥你會(huì)變成這樣,每當(dāng)...
    云莉6閱讀 244評(píng)論 0 1