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
init:
cd koa-demo npm init npm install koa -s
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...'); });
start http server:
> node app app started at port 3000...
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_ENV
或development
app.proxy
: 设置为true时,porxy头部将被信任app.subdomainOffset
: 设置.subdomains
的偏移量
Middleware
Koa 应用程序是一个包含一组中间件函数的对象,按照类似堆栈的方式组织和执行。
Middleware 实际就是一个函数,使用use(function)
加载middleware,使用next
进入下一个middleware。
Express VS Koa
Express 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");
})
异步中间件
异步操作(比如读取数据库,文件等)
处理方式:
callback
:回调函数(Express使用这种方式);缺点:层层回调,若逻辑很复杂,可能会陷入回调地狱fs.readFile('a.txt',function(err,data){ // do something fs.readFile('b.txt',function(err,data){ //... }); // do something });
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 });
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 函数执行完成'); });
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)) ;
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'); });
- koa@1.x: 使用generator/yield语法
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
属性的错误,默认为 500ctx.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...');
});
测试成功情况:
> 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
测试异常情况:
> 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
计时器函数执行顺序:
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 }
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 }
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 ] }
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 ] };
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
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"}} ]);
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
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; */ ... })
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:
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:
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"]}}
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
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"}, ]);
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);
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
Blog:
- ruanyf/koa | ruanyf/koa demo
- 如何使用koa2+es6/7打造高质量Restful API
- koa2入门笔记
- koa2从起步到填坑
- koa2 async和await 实战详解
- koa-session学习笔记
- 从koa-session中间件源码学习cookie与session
- Mongoose
- mongoose使用之查询篇
- Mongoose初使用总结
- koa2+mongodb搭建简易nodejs后台接口服务
Useful npm: