Koa

Starter

Koa: next generation web framework for node.js

官网

一个比expess更简洁轻量的nodeJS web框架 (requires node v7.6.0 or higher) 如今版本已更新到了koa2,性能更优异,还支持async/await(JS原生支持,本身是generator语法糖,实现了同步写异步,终结异步回调)

Hello world

  1. init:

     cd koa-demo
     npm init
     npm install koa -s
    
  2. app.js:

     const Koa=require("koa");
     const app=new Koa();
    
     const main=ctx=>{
        ctx.response.type="json";
        ctx.response.body={
         success:1,
         msg:"Hello world!"
        }
     }
     app.use(main);
    
     // app.listen(...) return an HTTP Server
     app.listen(3000,()=>{
       console.log('app started at port 3000...');
     });
    
  3. start http server:

     > node app
     app started at port 3000...
    
  4. visit to verify:

     > curl -i http://localhost:3000
       % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                      Dload  Upload   Total   Spent    Left  Speed
     100    34  100    34    0     0    435      0 --:--:-- --:--:-- --:--:--   548HTTP/1.1 200 OK
     Content-Type: application/json; charset=utf-8
     Content-Length: 34
     Date: Thu, 25 Oct 2018 02:44:39 GMT
     Connection: keep-alive
    
     {"success":1,"msg":"Hello world!"}
    

API

const Koa=require("koa");
const app=new Koa();
  • app.listen(...): 创建并返回一个http服务器

      const http = require('http')
      const Koa = require('koa')
      const app = new Koa()
      http.createServer(app.callback()).listen(3000)
    
      // 等同于
      const Koa = require('koa')
      const app = new Koa()
      app.listen(3000);
    
  • app.callback():返回一个可被http.createServer() 接受的程序实例,可将此实例添加到Connect/Express应用中
  • app.use(function): 加载中间件
  • app.keys=:设置cookie 密钥,eg:app.keys = ['im a newer secret', 'i like turtle'];
  • app.context: 是ctx的来源,可使用app.context添加额外的属性到ctx

      // 从ctx添加一个数据库引用
      add.context.db = db()
    
      app.use(async (ctx)=>{
      console.log(ctx.db)
      })
    
  • app.on(eventName,function): 添加事件侦听器
      // 默认情况下,所有错误输出到 stderr( 若app.silent=true 或 err.status=404 或 err.expose=true,默认错误处理程序不会输出错误)
      app.on('error', (err, ctx) =>
        log.error('server error', err, ctx)
      );
    
  • app.env: 默认是 NODE_ENVdevelopment
  • app.proxy: 设置为true时,porxy头部将被信任
  • app.subdomainOffset: 设置.subdomains的偏移量

Middleware

Koa 应用程序是一个包含一组中间件函数的对象,按照类似堆栈的方式组织和执行。

Middleware 实际就是一个函数,使用use(function)加载middleware,使用next进入下一个middleware。

Express VS Koa

Express Middleware: 顺序执行,从第一个中间件执行到最后一个中间件,发出响应 Express Middleware

Koa Middleware: 洋葱模型,一层层的打开,再一层层的闭合,直到第一个中间件执行结束才发出响应 Koa Middleware

Express: A->B->C->...->N

// middleware A
app.use(function(request,response,next){
    //...
    next();        // move to next middleware in the stack
});

// middle B
// middle C
// ...

// middle N
app.use(function(request,response,next){
    //...
    response.send("done!");            // done!
});

Koa:A->B->C->...->N->...->C->B->A

// middleware A
app.use((ctx,next)=>{
    console.log("A begin");
    next();
    console.log("A end");            // done!
});

// middle B
// middle C
// ...

// middle N
app.use((ctx,next)=>{
    console.log("N begin");
    next();
    console.log("N end");
})

异步中间件

异步操作(比如读取数据库,文件等)

处理方式:

  1. callback:回调函数(Express使用这种方式);缺点:层层回调,若逻辑很复杂,可能会陷入回调地狱

     fs.readFile('a.txt',function(err,data){
         // do something
         fs.readFile('b.txt',function(err,data){
             //...
         });
         // do something
     });
    
  2. promise: 用来传递异步操作流的对象;缺点:代码冗余,只是回调函数的改进

     // 创造一个Promise实例
     const promise = new Promise(function(resolve, reject) {
       // ... some code
    
       if (/* 异步操作成功 */){
         resolve(value);
       } else {
         reject(error);
       }
     });
    
     // 后续处理
     promise
       .then(function(data) { //cb
         // success
       })
       .then(function(data) { //cb
         // success
       })
       .catch(function(err) {
         // error
       })
       .finally(function(){
         // no args,always exec
       });
    
  3. generator/yield: es6引入,以同步的方式来写异步编程;异步操作需要暂停的地方,都用yield语句注明;

     function* gen(x){
       try {
         var y = yield x + 2;
       } catch (e){
         console.log(e);
       }
       return y;
     }
    
     var g = gen(1);
     //next:分阶段执行Generator函数
     g.next()             // { value: 3, done: false }
     g.next(2)             // { value: 2, done: true }
     g.throw('出错了');    // 出错了
    
     // 引入co模块,用于 Generator 函数的自动执行,返回一个Promise对象
     var co = require('co');
     var gen = function* () {
       var f1 = yield readFile('/etc/fstab');
       var f2 = yield readFile('/etc/shells');
       console.log(f1.toString());
       console.log(f2.toString());
     };
     co(gen).then(function (){
       console.log('Generator 函数执行完成');
     });
    
  4. async/await: es7引入,generator/yield的语法糖,语义更清晰,且javascript原生支持;注:await命令只能用在async函数之中,如果用在普通函数,就会报错

     async function main() {
       try {
         const val1 = await firstStep();
         const val2 = await secondStep(val1);
         const val3 = await thirdStep(val1, val2);
         console.log('Final: ', val3);
       }
       catch (err) {
         console.error(err);
       }
     }
    
     main();
    
     main()
     .then(v => console.log(v))
     .catch(e => console.log(e))
     ;
    
  5. generator/yield vs. async/await

    generator/yield async/await
    1 执行必须靠执行器(eg:co模块--用于 Generator 函数的自动执行) async函数自带执行器
    2 co模块约定yield命令后面只能是 Thunk函数(自动执行 Generator 函数的一种方法)或 Promise 对象 await后面可以是 Promise 对象和原始类型的值(这时等同于同步操作)
    3 Generator 函数的返回值是 Iterator 对象 async函数返回值是 Promise对象(可以用then方法指定下一步的操作)
    • koa@1.x: 使用generator/yield语法
        app.use(function *(next){
            this.response.type='html';
            this.response.body=yield fs.readFile('./demos/template.html', 'utf8');
        });
      
    • koa@2.x: 使用async/await语法
        app.use((ctx,next)=>{
            ctx.response.type = 'html';
            ctx.response.body = await fs.readFile('./demos/template.html', 'utf8');
        });
      

Context

Koa Context对象: 表示一次对话的上下文,每个请求会创建属于此请求的context对象,并在koa中间件中传递

  • 封装了node的request和response对象,通过加工这个对象,就可以控制返回给用户的内容。
  • 对 Koa 内部一些常用的属性或者方法做了代理操作,使得我们可以直接通过这个context对象获取(eg:ctx.url与ctx.request.url等同)
  • 约定了一个中间件的存储空间state,可以通过context对象的state存储一些数据,比如用户数据,版本信息等
  • 注:
    • koa@1.x: 使用this引用Context对象
    • koa@2.x: 使用ctx来访问Context对象

示例:

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

app.use((ctx,next)=>{
    console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
    next();
})

app.use((ctx,next)=>{
    ctx.response.body = { data: 'Hello World' };
})

app.use(async (ctx,next)=>{
    next();
    ctx.response.type="json";
});

app.listen(3000);

API :

  • ctx.req: Node 的 request 对象
  • ctx.res: Node 的 response 对象,注:Koa 不支持直接调用底层res进行响应处理,请避免使用以下 node 属性:res.statusCode(),res.writeHead(),res.write(),res.end()
  • ctx.request: Koa 的 request 对象
  • ctx.response: Koa 的 response 对象
  • ctx.app: 应用实例引用
  • ctx.cookies.get(name, [options]): 获得 cookie 中名为 name 的值,options:
    • signed: 所请求的cookie应该被签名
  • ctx.cookies.set(name, value, [options]): 设置 cookie 中名为 name 的值,options:
    • signed: 是否要做签名
    • expires: cookie 有效期时间
    • path: cookie 的路径,默认为 /
    • domain: cookie 的域
    • secure: false 表示 cookie 通过 HTTP 协议发送,true 表示 cookie 通过 HTTPS 发送。
    • httpOnly: true 表示 cookie 只能通过 HTTP 协议发送
  • ctx.throw(msg, [status]): 抛出包含 .status 属性的错误,默认为 500
      ctx.throw('name required', 400) 
      //等价于:
      var err = new Error('name required');
      err.status = 400;
      throw err;
    
  • ctx.respond: 设为false,则表示绕过 Koa 的内置 response 处理,直接操作原生 res 对象

  • 与Request等价的API:

    • ctx.header
    • ctx.method
    • ctx.method=
    • ctx.url
    • ctx.url=
    • ctx.originalUrl
    • ctx.path
    • ctx.path=
    • ctx.query
    • ctx.query=
    • ctx.querystring
    • ctx.querystring=
    • ctx.host
    • ctx.hostname
    • ctx.fresh
    • ctx.stale
    • ctx.socket
    • ctx.protocol
    • ctx.secure
    • ctx.ip
    • ctx.ips
    • ctx.subdomains
    • ctx.is()
    • ctx.accepts()
    • ctx.acceptsEncodings()
    • ctx.acceptsCharsets()
    • ctx.acceptsLanguages()
    • ctx.get()
  • Response等价的API:

    • ctx.body
    • ctx.body=
    • ctx.status
    • ctx.status=
    • ctx.length=
    • ctx.length
    • ctx.type=
    • ctx.type
    • ctx.headerSent
    • ctx.redirect()
    • ctx.attachment()
    • ctx.set()
    • ctx.remove()
    • ctx.lastModified=
    • ctx.etag=

Exception

抛出错误

const main = ctx => {
  ctx.response.status = 404;
  ctx.response.body = 'Page Not Found';
};
const main = ctx => {
  ctx.throw('Page Not Found',404); // ctx.throw(404)
};

监听错误

app.on('error',function(err){
    console.log('logging error ', err.message);
    console.log(err);
});

处理错误

在最外层添加一个中间件,使用try...catch捕获错误

app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.response.status = err.statusCode || err.status || 500;
    ctx.response.body = {
      message: err.message
    };
    ctx.app.emit('error', err, ctx);    // 调用ctx.app.emit(),手动释放error事件,才能让监听函数生效
  }
});

app.use(ctx => {
  ctx.throw(500);
});

常用第三方插件

static

const path=require('path');
const static = require('koa-static');
app.use(static(path.resolve(__dirname, "./public")));

body

koa-body模块可以用来从请求的数据体里面提取键值对(方便处理表单,上传文件:ctx.request.files)

const KoaBody = require('koa-body');
app.use(KoaBody());    
app.use(async(ctx,next)=>{
    console.log("request body:");
    console.log(ctx.request.body);
    if (!body.name) 
        ctx.throw(400, '.name required');
    await next();
});

Router

app.js:

const Router=require("koa-router");
const router=new Router();

var catalogueController=require("./controller/catalogueController");
router.get('/catalogues',catalogueController.list);
router.get('/catalogues/:id',catalogueController.get);
router.put('/catalogues/:id',catalogueController.update);
router.post('/catalogues',catalogueController.create);
router.delete('/catalogues/:id',catalogueController.delete);

app.use(router.routes());
app.use(router.allowedMethods());

./controller/catalogueController.js:

module.exports={
  list:async (ctx,next)=>{
      ...
  },
  get:async (ctx,next)=>{
    let id=ctx.params.id;
      ...
  },
  update:async (ctx,next)=>{
    let id=ctx.params.id;
    let catalogue=ctx.request.body;
    ...
  }
  ...
}

Session

const session=require('koa-session2');
app.keys=['a secret key'];    // if set signed:true,need setting the .keys.
app.use(session({
    key:"SESSIONID",
    signed:true,        // SESSIONID.sig,need to set app.keys,作用:给cookie加上一个sha256的签名,防止cookie被篡改
    maxAge:86400000,    // cookie expire after maxAge ms: 1 day = 24h*60m*60s*1000=86400,000ms
}));


const logger=async (ctx,next)=>{
  let path=ctx.request.path;
  console.log("log:"+ctx.request.method+" "+path+","+ctx.request.url);
  if(ctx.session){
        console.log("loginUser",ctx.session.loginUser);
      console.log("cookie",ctx.request.header.cookie);    //ctx.cookies
    }
  await next();
}
app.use(logger);

app.use(async(ctx,next)=>{
    // get: ctx.session.xxx
    // set: ctx.session.Xxx=xxx
    // remove: delete ctx.session.Xxx
    ...
})

扩展:使用MongoDB/Redis等存储Session (具体参看下节)

示例

Koa

const Koa=require("koa");
const app=new Koa();

const errorHandler=async(ctx,next)=>{
    try{
        await next();
    }catch(e){
        console.log("catch exception");
        ctx.response.type="json";
        ctx.response.body={
            success:0,
            status: e.statusCode || e.status || 500,
            message: e.message
        }
    }
}

const prefixHandler=async(ctx,next)=>{
    console.log("prefix Handler..."+ctx.request.path);
    if(ctx.request.path=='/error')
        ctx.throw("go to error!");
    await next();
}

const postHandler=async(ctx,next)=>{
    await next();
    console.log("post Handler..."+ctx.request.path);
    ctx.response.type="json";
    ctx.set("micro-test",true);        // set header
}

const fs = require('fs');
const main=async(ctx,next)=>{
    console.log("main")
    let data = await fs.readFileSync('./public/1.txt','utf8');
    console.log("readed:"+data);
    ctx.response.body={
        success:1,
        data:data
    }
}

app.use(errorHandler);
app.use(prefixHandler);
app.use(postHandler);
app.use(main);

app.listen(3000,()=>{
  console.log('app started at port 3000...');
});
  1. 测试成功情况:

     > curl -i http://localhost:3000/1
     HTTP/1.1 200 OK
     Content-Type: application/json; charset=utf-8
     micro-test: true
     Content-Length: 35
     Date: Sun, 09 Sep 2018 16:07:48 GMT
     Connection: keep-alive
     {"success":1,"data":"Static File!"}
    
     # server console:
     prefix Handler.../1
     main
     readed:Static File!
     post Handler.../1
    
  2. 测试异常情况:

     > curl -i http://localhost:3000/error
     HTTP/1.1 200 OK
     Content-Type: application/json; charset=utf-8
     Content-Length: 51
     Date: Sun, 09 Sep 2018 16:09:19 GMT
     Connection: keep-alive
     {"success":0,"status":500,"message":"go to error!"}
    
     # server console:
     prefix Handler.../error
     catch exception
     */
    

async/await

  1. 计时器函数执行顺序:

     function sleep(time){
         console.log("sleep func start")                        // 1
         setTimeout(()=>{
             console.log("sleep wakeup after "+time+" !")    // 3 [ execute after sleep `${time} ms` ]
         },time);
         console.log("sleep func end")                        // 2
     }
    
  2. Promise函数(+timeout函数)执行顺序:

     function promiseFunc(timeout){
         console.log("a1. build promise instance");            // 1
         let inst=new Promise((resolve,reject)=>{                    
             console.log("b1. start promise instance func")    // 2
             setTimeout(()=>{                        
                 console.log("promise wakeup")            
                 resolve("promise resolved after "+timeout);    // 5 <pending> -> <resolved>        
             },timeout);    
             console.log("b2. end promise instance func")    // 3
         });
    
         console.log("a2. return promise instance");                
         return inst;                                        // 4
     }
    
  3. async函数使用await和不使用await:

     async function asyncFunc(timeout){
         console.log("async 1->2")
         let msg=await promiseFunc(timeout);    // 1 
         console.log("await result:"+msg);    // 2 [ 1->2 `blocked ${timeout} ms`, get resolved data ]
    
         console.log("async 3->4")
         sleep(timeout-2000);                // 3 
         return msg;                            // 4  [ 3->4 no block,return a promise instance ] 
     }
    
  4. async函数调用async函数:

     async function test1(timeout){
         console.log("test timeout:"+timeout);
    
         console.log("1->2")
         let result=asyncFunc(timeout);        // 1
         console.log(result);                // 2 [ 1 -> 2 no block,get a promise instance]
    
         console.log("3->4")
         let msg=await result;                // 3 
         console.log(msg);                    // 4 [ 3 -> 4 blocked `${timeout} ms`, get resolved data ]
     };
    
  5. async/await+Promise 多条异步:顺序trigger,reject同throw error)

     function promiseFunc2(code){
         console.log("a1. build promise instance");            // 1
         let inst=new Promise((resolve,reject)=>{                    
             console.log("b1. start promise instance func")    
             if(code==1)                            
                 resolve(code+" resolve!");            // 2 <pending> -> <fulfilled>    
             else if(code==0)
                 reject(code+" reject!");            // 2 <peinding> -> <rejected> 
             else if(code==-1)
                 throw new Error(code+" error!");    // 2 exception
             else{                                    // 2 reject(e) == throw new Error(xxx);
                 try {
                     throw new Error(`${code} error! [same with code -1 error!]`);
                 } catch(e) {
                     reject(e);
                 }
             }
             console.log("b2. end promise instance func")
         });
         console.log("a2. return promise instance");            // 3                
         return inst;        
     }
    
     async function test2(code){
         console.log("test code:"+code);
         try{
             let msg=await promiseFunc2(code);
             console.log("get return: "+msg);
         }catch(e){
             console.log("get error: "+(e.message||e));
         }
     }
    
     //exec test:
    
     test2(1);
     test2(0);
     test2(-1);
     test2(-2);
     /*
     test code:1
     a1. build promise instance
     b1. start promise instance func
     b2. end promise instance func
     a2. return promise instance
     test code:0
     a1. build promise instance
     b1. start promise instance func
     b2. end promise instance func
     a2. return promise instance
     test code:-1
     a1. build promise instance
     b1. start promise instance func
     a2. return promise instance
     test code:-2
     a1. build promise instance
     b1. start promise instance func
     b2. end promise instance func
     a2. return promise instance
     get return: 1 resolve!
     get error: 0 reject!
     get error: -1 error!
     get error: -2 error! [same with code -1 error!]
     */
    

Mongoose

  1. MongoDB Data Model: catalogues(name,description,meta:createTime,updateTime,updator)

     > db.catalogues.insert([
     {name:"Spring",description:"spring framework",meta:{createTime:new Date(),updateTime:new Date(),updator:"Tom"}},
     {name:"ReactJS",description:"reactJS front framework",meta:{createTime:new Date(),updateTime:new Date(),updator:"Tom"}},
     {name:"NoSql",description:"not only sql databases",meta:{createTime:new Date(),updateTime:new Date(),updator:"Tom"}}
     ]);
    
  2. mongoTest.js

     const mongoose =require("mongoose");
     mongoose.connect('mongodb://cj:123456@localhost:27017/demo?authSource=admin',{ useNewUrlParser: true});
     mongoose.set('useCreateIndex',true);
    
     let catalogueSchema=mongoose.Schema({
         name:{type:String,required:true,unique:true},
         description:{type:String},
         meta:{
           createTime:{type:Date,default:Date.now},
           updateTime:{type:Date,default:Date.now},
           updator:{type:String}
         }
     },{versionKey: false});
    
     // Pre and post save() hooks are not executed on update(), findOneAndUpdate()
     catalogueSchema.pre('save',function(next){
       console.log('pre save:'+this.isNew);
       if(this.isNew)
         this.meta.createTime=this.meta.updateTime=Date.now();
       else
         this.meta.updateTime=Date.now();
       next();
     });
    
     let catalogueDao=mongoose.model('catalogues',catalogueSchema);
    
     /* Operation */
    
     async function list(){
         let catalogueList= await catalogueDao.find({},{meta:0});
         return catalogueList;
     };
    
     async function get(id){
         let catalogue=await catalogueDao.findOne({_id:id});
         return catalogue;
     }
    
     async function create(catalogue){
         let result=await catalogueDao.create(catalogue);
         console.log("create:");
         console.log(result);
         return result;
     }
    
     async function udpateCatalogue(id,catalogue){
       let result=await catalogueDao.updateOne({_id:id}
         ,{$set:{name:catalogue.name,description:catalogue.description
           ,"meta.updateTime":new Date(),"meta.updator":catalogue.updator}},{new:true});
         return result;
     }
    
     async function deleteCatalogue(id){
         let result=await catalogueDao.deleteOne({_id:id});
         return result;
     }
    
     // test
     async function test(){
       console.log("start test");
    
       let catalogue={name:"Docker",description:"Build, Ship, and Run Any App, Anywhere"};
       let newCatalogue= await create(catalogue);
       let id=newCatalogue._id;
    
       console.log("list:")
       let catalogueList=await list();
       console.log(catalogueList);
    
       console.log(`update ${id}:`);
       let catalogueChange={name:"Docker Feature",
         description:"Container:Build, Ship, and Run Any App, Anywhere",
         updator:"anomy"
       };
       let result=await udpateCatalogue(id,catalogueChange);
       console.log(result);
    
       console.log(`get ${id}:`)
       result=await get(id);
       console.log(result);
    
       console.log(`delete ${id}:`)
       result=await deleteCatalogue(newCatalogue._id);
       console.log(result);
    
       console.log("end test");
       mongoose.disconnect();
       console.log("finish!");
     }
    
     // clear same records in mongodb first
     // db.catalogues.remove({name:{$regex:"Docker*"}})
     test();
    

koa-session2+mongo

store session on mongodb

  1. app.js

     const session=require('koa-session2');
     const MongoStore=require('./util/mongoStore');
    
     const mongoose =require("mongoose");
     mongoose.connect('mongodb://cj:123456@localhost:27017/demo?authSource=admin'
       ,{ useNewUrlParser: true});
     mongoose.set('useCreateIndex',true);
    
     app.keys=['a secret key'];    // if set signed:true,need setting the .keys.
     app.use(session({
         key:"SESSIONID",
         //signed:true,        // SESSIONID.sig,need to set .keys,作用:给cookie加上一个sha256的签名,防止cookie被篡改
         maxAge:86400000,    // cookie expire after maxAge ms: 1 day = 24h*60m*60s*1000=86400,000ms
         store: new MongoStore({
             collection:"sessions",
             connection:mongoose,
             expireAfterSeconds:30    // mongo TTL expireAfterSeconds ( unit:s )
         })
     }));
    
     ...
    
     app.use(async(ctx,next)=>{
         /*
             login: do get/set session
                 get session: ctx.session.Xxx , eg: ctx.session.loginUser
                 set session: ctx.session.Xxx=xxx , eg: ctx.session.loginUser={username:result.username,roles:result.roles};
    
             logout: do remove session
                 delete ctx.session.Xxx , eg: delete ctx.session.loginUser;
         */ 
         ...
     })
    
  2. mongoStore.js

     const mongoose = require('mongoose');
     const { Store } = require("koa-session2");
    
     class MongoStore extends Store {
       constructor({connection=mongoose,collection='sessions',expireAfterSeconds=86400000}={}){
         super();
         let storeSchema=new connection.Schema({
           _id:String,
           data:Object,
           updatedAt: {
             default: new Date(),
             expires: expireAfterSeconds, // 1 day: 86400 s = 60s*60m*24h => expireAfterSeconds
             type: Date
           }
         })
         this.modelDao=connection.model(collection,storeSchema);
       }
    
       async get(sid,ctx){
         console.log("get mongo session:"+sid);
         let result= await this.modelDao.findOne({_id:sid});
         console.log(result);
         return result?result.data:null;
       }
    
       async set(session, { sid =  this.getID(24), maxAge = 86400000 } = {}, ctx) {
         console.log("set mongo session:"+sid+",cookie maxAge:"+maxAge);
           try {
               let record={_id:sid,data:session,updatedAt:new Date()};
               console.log(record);
               await this.modelDao.updateOne({_id:sid}, record, { upsert: true, safe: true });
           } catch (e) {
             console.log("set mongo session fail:");
             console.log(e);
           }
           return sid;
       }
    
       async destroy(sid){
         console.log("destroy mongo session:"+sid);
         return await this.modelDao.deleteOne({_id:sid});
       }
    
     }
    
     module.exports = MongoStore;
     session stored in mongodb:
    
  3. Session Stored on MongoDB:

     > db.sessions.find().pretty()
     {
         "_id" : "53094f8db3e399e17616a4d910676c58b6cde92f3b9435be",
         "__v" : 0,
         "data" : {
             "loginUser" : {
                 "username" : "admin",
                 "roles" : [
                     "admin"
                 ]
             }
         },
         "updatedAt" : ISODate("2018-09-09T09:40:40.277Z")
     }
    
     > db.sessions.getIndexes()
     [
       {
         "v" : 2,
         "key" : {
           "_id" : 1
         },
         "name" : "_id_",
         "ns" : "demo.sessions"
       },
       {
         "v" : 2,
         "key" : {
           "updatedAt" : 1
         },
         "name" : "updatedAt_1",
         "ns" : "demo.sessions",
         "expireAfterSeconds" : 30,
         "background" : true
       }
     ]
    

Verify:

  1. login

     > curl -i -c cookie.txt -d "username=admin&password=123" http://localhost:3000/login
    
     HTTP/1.1 200 OK
     Content-Type: application/json; charset=utf-8
     Set-Cookie: SESSIONID=8cbe57db55bb6c944df99ec9a10481a431c41e5ba1400a4a; path=/; expires=Wed, 12 Sep 2018 16:29:57 GMT; httponly
     Set-Cookie: SESSIONID.sig=TsESPv34Ny1rOA9jimTrlybpHDE; path=/; expires=Wed, 12 Sep 2018 16:29:57 GMT; httponly
     Content-Length: 59
     Date: Tue, 11 Sep 2018 16:29:57 GMT
     Connection: keep-alive
    
     {"success":1,"data":{"username":"admin","roles":["admin"]}}
    
  2. logout

     > curl -i -b cookie.txt -X POST http://localhost:3000/logout
    
     HTTP/1.1 200 OK
     Content-Type: application/json; charset=utf-8
     Set-Cookie: SESSIONID=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; httponly
     Content-Length: 13
     Date: Tue, 11 Sep 2018 16:00:55 GMT
     Connection: keep-alive
    
     {"success":1}
    

Privilege Filter

  1. MongoDB data Model: privileges(path,method,roles)

     > db.privileges.insert([
     {path:"/catalogues",method:"GET",roles:[]},
     {path:"/catalogues/:id",method:"GET"},
     {path:"/catalogues/:id",method:"PUT",roles:["admin"]},
     {path:"/catalogues/:id",method:"DELETE",roles:["admin"]},
     {path:"/catalogues",method:"POST",roles:["admin"]},
     {path:"/articles",method:"GET"},
     {path:"/articles/:id",method:"GET"},
     {path:"/articles/:id",method:"PUT",roles:["user"]},
     {path:"/articles/:id",method:"DELETE",roles:["user","admin"]},
     {path:"/articles",method:"POST",roles:["user"]},
     {path:"/login",method:"POST"},
     {path:"/register",method:"POST"},
     {path:"/logout",method:"POST"},
     ]);
    
  2. app.js:

     const PrivilegeStore=require('./util/PrivilegeStore');
     let privilegeStore=new PrivilegeStore();
     const privilegeFilter=async (ctx,next)=>{
         /*
         Note:
             1. ctx.request.path: /catalogues/1 ; ctx.request.url: /catalogues/1?x=1
             2. roles: get from ctx.session.loginUser,eg: {username:"Tom",roles:["user"]}
             3. url-pattern:
                 let pattern=new UrlPattern('/catalogues/:id');
                 console.log(pattern.match('/catalogues'));            //null
                 console.log(pattern.match('/catalogues/1'));        //{id:'1'}
                 console.log(pattern.match('/catalogues/1/2'));        //null
                 console.log(pattern.match('/catalogues/1?x=3'));    //null
                 console.log(pattern.match('catalogues'));            //null
         */
         let reqItem={path:ctx.request.path,method:ctx.request.method
             ,roles:(ctx.session && ctx.session.loginUser)?ctx.session.loginUser.roles||[]:[]};
         let privileges = await privilegeStore.privileges;
         let matched = privileges.find((item)=>{
             return privilegeStore.verify(item,reqItem);
         });
         console.log("pass priv:"+matched);
         if(matched)
           await next();
         else
           ctx.throw(401);
     }
     app.use(privilegeFilter);
    
  3. PrivilegeStore.js:

     const mongoose = require('mongoose');
     const UrlPattern=require('url-pattern');
    
     class PrivilegeStore{
    
       constructor({connection=mongoose,collection='privileges'}={}){
         let storeSchema=this.initSchema();
         this.privilegeDao=connection.model(collection,storeSchema);
         this.privileges=this.loadPrivileges();
       }
    
       initSchema(){
         let privilegeSchema=mongoose.Schema({
             path:{type:String,required:true},
             method:{type:String,required:true},
             roles:{type:Array},
             meta:{
               createTime:{type:Date,default:Date.now},
               updateTime:{type:Date,default:Date.now},
               updator:{type:String}
             }
         });
         privilegeSchema.pre('save',function(next){
           if(this.isNew)
             this.meta.createTime=this.meta.updateTime=Date.now();
           else
             this.meta.updateTime=Date.now();
           next();
         });
         return privilegeSchema;
       }
    
       async loadPrivileges(){
         let result=await this.privilegeDao.find({},{_id:0,meta:0});
         result.map((item,index,arr)=>{
            item.pattern=new UrlPattern(item.path);
            return item;
         });
         console.log(result);
         return result;
       }
    
       async refresh(){
         this.privileges=this.loadPrivileges();
       }
    
       verify(item,reqItem){
         // console.log(item);
         if(item.method!=reqItem.method)
           return false;
         let pattern = item.pattern;
         if(!pattern){
           console.log("init pattern");
           pattern=new UrlPattern(item.path);
         }
         let match=pattern.match(reqItem.path);
         if(match==null){
           // console.log("path not match");
           return false;
         }
         if(!item.roles || item.roles.length==0){
           console.log("guest pass");
           return true;
         }
         if(item.roles.find((n)=>reqItem.roles.includes(n))){
           console.log("auth pass");
           return true;
         }
         return false; 
       }
    
     }
     module.exports = PrivilegeStore;
    

Reference

my demo

Blog:

Useful npm: