koa-passport是koa的一箇中間件,它實際上只是對passport的一個封裝。利用koa-passport能夠簡便的實現登陸註冊功能,不但包括本地驗證,還有不少提供第三方登陸的模塊可使用。git
passport
的主要功能就是可以提供一個用戶鑑權的框架,並把鑑權獲得的用戶身份供後續的業務邏輯來使用。而鑑權的具體過程是經過插件來實現,用戶能夠本身來實現,也可使用已有的第三方模塊實現各類方式的鑑權(包括OAuth或者OpenID)。github
passport
的代碼仍是有點複雜的,咱們先看一個最簡單的例子,而後逐步介紹後面的細節。數據庫
下面是一個使用koa-passport
的能夠執行的最小樣例json
const Koa = require('koa') const app = new Koa() // 定義一個驗證用戶的策略,須要定義name做爲標識 const naiveStrategy = { name: 'naive', // 策略的主體就是authenticate(req)函數,在成功的時候返回用戶身份,失敗的時候返回錯誤 authenticate: function (req) { let uid = req.query.uid if (uid) { // 策略很簡單,就是從參數裏獲取uid,而後組裝成一個user let user = {id: parseInt(uid), name: 'user' + uid} this.success(user) } else { // 若是找不到uid參數,認爲鑑權失敗 this.fail(401) } } } // 調用use()來爲passport新增一個可用的策略 const passport = require('koa-passport') passport.use(naiveStrategy) // 添加一個koa的中間件,使用naive策略來鑑權。這裏暫不使用session app.use(passport.authenticate('naive', {session: false})) // 業務代碼 const Router = require('koa-router') const router = new Router() router.get('/', async (ctx) => { if (ctx.isAuthenticated()) { // ctx.state.user就是鑑權後獲得的用戶身份 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)
這段代碼雖然沒有實際意義,可是已經展現完整的鑑權流程和錯誤處理,運行結果以下segmentfault
$ curl http://localhost:3000/\?uid\=128 hello {"id":128,"name":"user128"}% $ curl http://localhost:3000 Unauthorized%
能夠看到這裏鑑權的做用基本就是兩個數組
明白了上面的例子以後,咱們就能夠照着代碼來看一下passport
的主流程,定義在passport/authenticator.js文件中。安全
首先看看上面使用的passport.use()
函數,其內容很是簡單,就是把一個策略保存在本地,後續能夠經過name來訪問cookie
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()
,實際定義在passport/middleware/authenticate.js中,其返回值是一個函數,具體的實現以下,刪減了部分代碼,加了一下注釋session
module.exports = function authenticate(passport, name, options, callback) { // FK: 容許傳入一個策略的數組 if (!Array.isArray(name)) { name = [ name ]; multi = false; } return function authenticate(req, res, next) { // FK: 省略部分代碼... function allFailed() { // FK: 全部策略都失敗後,若是設置了回調就調用,不然找到第一個failure,根據option進行flash/redirect/401 } // FK: 這部分是主要邏輯 (function attempt(i) { // FK: 嘗試第i個策略 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: 鑑權成功後會調用logIn(),把用戶寫入當前ctx req.logIn(user, options, function(err) { if (err) { return next(err); } // FK: 成功跳轉,代碼比較複雜,省略了 }; strategy.fail = function(challenge, status) { // FK: 記錄錯誤,使用下一個策略進行嘗試,省略部分代碼 attempt(i + 1); }; strategy.redirect = function(url, status) { // FK: 處理跳轉 res.statusCode = status || 302; res.setHeader('Location', url); res.setHeader('Content-Length', '0'); res.end(); }; strategy.authenticate(req, options); })(0); // attempt };
這部分代碼就是passport
的核心流程,其實就是定義好一些鑑權成功/失敗/跳轉等處理機制,而後就調用具體的策略進行鑑權。須要注意的是,雖然外面上面的樣例中只傳入了一個策略,可是其實passport
支持同時使用多個策略,它會從頭開始嘗試各個策略,直到有一個策略作出處理或者已嘗試全部策略爲止。app
經過上面的樣例和主流程的分析,你們應該能清楚passport
大概作的事情是什麼,也可以知道最基礎的使用方式。
在上面的例子中,咱們沒有使用session,所以每一個請求都須要帶上uid參數。實際使用中,通常會把鑑權後的用戶身份會保存在cookie中供後續請求來使用。雖然passport並無要求必定使用session,但實際上是默認會使用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))
而後,由於咱們的用戶信息須要保留在session存儲中(利用cookie或者服務端存儲),所以須要定義序列化和反序列的操做。下面的例子是一個示例。真實場景中,反序列化的時候確定須要根據uid來檢索真正的用戶信息。
passport.serializeUser(function (user, done) { // 序列化的結果只是一個id done(NO_ERROR, user.id) }) passport.deserializeUser(async function (str, done) { // 根據id恢復用戶 done(NO_ERROR, {id: parseInt(str), name: 'user' + str}) })
再而後,由於咱們不須要naiveStrategy做爲默認策略了,所以要把相應的use()語句去掉,轉而只在用戶明確要登陸的時候才調用
router.get('/login', passport.authenticate('naive', { successRedirect: '/' }) )
最後,咱們須要在app中開啓koa-passport
對session的支持
app.use(passport.initialize()) app.use(passport.session())
initialzie()
函數的做用是隻是簡單爲當前context添加passport字段,便於後面的使用。而passport.session()
則是passport
自帶的策略,用於從session中提取用戶信息,其代碼位於passport/strategies/session.js,內容以下
SessionStrategy.prototype.authenticate = function(req, options) { // FK: 確保已經初始化 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: 若是用戶字段存在,調用自定義的反序列化函數來獲取用戶信息 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策略的做用也即生成用戶信息,只不過數據來源不是請求字段,而是session信息。
在上面的主流程裏,咱們看到當鑑權成功時,會調用req.logIn()函數,其實還有logOut()和isAuthenticated(),都定義在passport/http/request.js。
其中logIn()和logOut()操做真正調用的是
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兩個操做
{"passport":{"user":128},"_expire":1517357892908,"_maxAge":86400000}
此外,request還定義了isAuthenticated()函數,用於檢查當前是否已經鑑權成功,代碼以下
req.isAuthenticated = function() { var property = 'user'; if (this._passport && this._passport.instance) { property = this._passport.instance._userProperty || 'user'; } return (this[property]) ? true : false; };
至此,咱們基本已經看完了passport
的主要工做。passport
之因此強大,在於他定義好了框架,但並無肯定具體的鑑權策略,用戶能夠根據需求來加入各類自定義的策略,如今已經有大量的模塊可使用了。
在上面的樣例中,咱們定義了本身的NaiveStrategy來實現對用戶的鑑權,固然上面的代碼毫無安全性可言。在真實環境中,最簡單的鑑權通常是用戶提交用戶名和密碼,而後服務端來校驗密碼,準確無誤後才認爲鑑權成功。
雖然這個過程能夠經過擴展咱們的NaiveStrategy來實現,不過咱們已經有了passport-local
這個庫提供了一個本地鑑權的代碼框架,能夠直接使用。咱們來看看其流程:
// FK: passport-local 省略部分代碼 function Strategy(options, verify) { // FK: 記錄verify函數 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); } };
看代碼其實流程也很是簡單,就是自動從請求中獲取username
和password
兩個字段,而後提交給用戶自定義的verify函數進行鑑權,而後處理鑑權的結果。
能夠看到,這個框架作的事情其實頗有限,主要的校驗操做仍是須要用戶本身來定義,一個簡單用法樣例以下:
const LocalStrategy = require('passport-local').Strategy passport.use(new LocalStrategy(async function (username, password, done) { // FK: 根據username從數據庫或者其餘存儲中拿到用戶信息 let user = await userStore.getUserByName(username) // FK: 把傳入的password和數據庫中存儲的密碼進行比較。固然這裏不該該是明文,通常是加鹽的hash值 if (user && validate(password, user.hash)) { done(null, user) } else { log.info(`auth failed for`, username) done(null, false) } }))
自此,咱們看到用戶只須要再定義一下用戶的存儲流程,基本上就能夠實現一個簡單的用戶註冊登陸功能了。
本文記錄對koa-passport
相關模塊的代碼學習過程,裏邊細節比較多,行文有些亂,還請見諒。若是你們只是想看使用方法,能夠參考其餘的文章,好比這篇。
和以前看koa-session
模塊相比,passport
模塊沒有使用async語法,就能明顯感覺到回調地獄的威力,代碼一開始看的仍是比較痛苦。整體感受js仍是過於靈活了,若是用來寫業務,必定須要很是強健的工程規範才行。
文中兩個樣例的完整代碼請參見 https://github.com/fankaigit/...