koa-passport學習筆記

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%

能夠看到這裏鑑權的做用基本就是兩個數組

  1. 鑑權失敗能夠拒絕用戶訪問(也能夠不作處理)
  2. 鑑權成功會把用戶記錄到context

主流程

明白了上面的例子以後,咱們就能夠照着代碼來看一下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

在上面的例子中,咱們沒有使用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信息。

Session Manager

在上面的主流程裏,咱們看到當鑑權成功時,會調用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兩個操做

  • logIn()操做,會調用一個_serializeUser(), 而後把序列化的結果存到req.session['passport']。此時session的內容相似 {"passport":{"user":128},"_expire":1517357892908,"_maxAge":86400000}
  • logOut()操做更加簡單,就是直接刪除session裏passport裏的user字段。

此外,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之因此強大,在於他定義好了框架,但並無肯定具體的鑑權策略,用戶能夠根據需求來加入各類自定義的策略,如今已經有大量的模塊可使用了。

passport-local

在上面的樣例中,咱們定義了本身的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);
  }
};

看代碼其實流程也很是簡單,就是自動從請求中獲取usernamepassword兩個字段,而後提交給用戶自定義的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/...

相關文章
相關標籤/搜索