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/...