Koa 使用小技巧(小结)

cookie的安全保护

基于cookie来验证用户状态的系统中,如何提高cookie的安全级别是首要因素,最简单直接的方式就生成的cookie值随机而且复杂。一般使用uuid来生成cookie,生成的随机串在复杂度上已满足需求,但是如果真被攻击者尝试到一个可用的值,那怎么防范呢?使用signed的cookie设置,如下所示:

app.keys = ["token"]; ... ctx.cookies.set("jt", "abcd", { signed: true, });

在设置 jt 这个cookie的时候,koa会以 jt 的值 abcd 加上设置的密钥,生成校验值,并写入至 jt.sig 这个cookie中,所以能看到响应的HTTP头中如下所示:

Set-Cookie: jt=abcd; path=https://www.jb51.net/; httponly Set-Cookie: jt.sig=gpDbdxr25sarDhE_1yMSAnIn_bU; path=https://www.jb51.net/; httponly

在后续的请求中,获取 jt 这个cookie时,则会根据 jt.sig 的值判断是否合法,安全性上又明显提升。

那么 app.keys 为什么是设计为数组呢?先来考虑以下的一种场景,当希望更换密钥的时候,原有的的cookie都将因为密钥更新而导致校验失败,则用户的登录状态失效。一次还好,如果需要经常需要更新密钥(我一般一个月更换一次),那怎么处理好?这就是 app.keys 为配置为数组的使用逻辑了。

当生成cookie时,使用keys中的第一个元素来生成,而校验的时候,是从第一个至最后一个,一个个的校验,直到通过为止,所以在更新密钥的时候,只需要把新的密钥加到数组第一位则可以。我一般再保留两组密钥,因为更新是一个月一次,因此如果客户的cookie是三个月前生成的,那就会失效了。

cookie的校验是基于 keygrip 来处理的,大家也可以使用它来做自己的一些数据校验,如验证码之类。

异常处理

在使用koa时,一般出错都是使用 ctx.throw 来抛出一个error,中断处理流程,接口响应出错,处理逻辑如下所未:

app.on('error', (err, ctx) => { // 记录异常日志 console.error(err); }); app.use((ctx) => { ctx.throw(400, '参数错误'); });

此处只利用了koa自带的异常出错,过于简单,我们希望能针对主动抛出的异常与程序异常能加以区分,因此需要自定义异常处理的中间件,如下:

app.on('error', (err, ctx) => { // 记录异常日志 console.error(err); }); app.use(async(ctx, next) => { try { await next() } catch (err) { let status = 500; const message = err.message; // koa的throw使用http-errors来生成error // 此处只判断是否有status,有则认为是http-errors if (err.status) { status = err.status } else { // 非主动抛出异常,则触发error事件,记录异常日志 ctx.app.emit("error", err, ctx); } ctx.status = status; ctx.body = { message, }; } }) app.use((ctx) => { // 代码异常 // ctx.i.j = 0; // 主动抛出异常 ctx.throw(400, '参数错误'); });

通过此调整后,将逻辑主动抛出异常与程序异常区分开,定时去查看异常日志,减少程序异常。此例子只是简单的使用了http-errors来创建主动抛出的异常,在实际使用中,可以根据自己的场景创建自定义的Error类,定制相应的异常信息。

当前正在处理请求数

得益于nodejs的IO处理,koa在高并发的场景下的CPU、内存都占用并不高,但是也因为这样,如果只通过CPU、内存来监控程序运行状态并不全面,因此需要增加当前处理请求数的监控,代码如下:

let processingCount = 0; const maxProcessingCount = 1000; app.use(async (ctx, next) => { processingCount++; if (processingCount > maxProcessingCount) { // 如果需要也可以直接在处理请求超时时,直接出错 console.error("processing request over limit"); } try { await next(); } catch (err) { throw err; } finally { processingCount--; } }); app.use(async (ctx) => { // 延时一秒 await new Promise(resolve => setTimeout(resolve, 1000)); ctx.body = { account: 'vicanso', }; });

此中间件在接收到请求时,将处理请求数加一,在处理完成后减一。最大的处理请求数根据系统的性能与用户数量选择合理的值。如果接口处理慢或者突然并发请求暴涨的时,可以尽早得知异常情况,尽早排查。

延时响应

接口的处理一般而言都是希望越快越好,但有些场景我们不希望接口响应的太快(如注册),避免恶意者迅速尝试功能,因此需要一个延时响应的中间件,代码如下:

function delayResponse(delayMs) { const delay = (t) => { const d = delayMs - (Date.now() - t); // 如果处理时长已超过delayMs,无需等待 if (d <= 0) { return Promise.resolve(); } return new Promise(resolve => setTimeout(resolve, d)); } return async(ctx, next) => { const startedAt = Date.now(); try { await next(); // 成功处理时等待 await delay(startedAt); } catch (err) { // 失败时也等待 await delay(startedAt); throw err; } } } router.post('/users/v1/register', delayResponse(1000), (ctx) => { ctx.body = { account: 'vicanso', }; });

通过此中间件,可以限制某些功能的响应时长(保证每次处理时间都大于期望值),需要注意的是,延时响应的不要超过全局的超时配置。

接口性能统计

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/ac7b1a49546ab2a8610deba2c5993d38.html