Koa 使用小技巧(小结)


Posted in Javascript onOctober 22, 2018

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=/; httponly
Set-Cookie: jt.sig=gpDbdxr25sarDhE_1yMSAnIn_bU; path=/; 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',
 };
});

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

接口性能统计

系统是否稳定,性能是否需要优化等都依赖于统计,为了能及时反应出系统状态,并方便添加告警指标,我将相关的统计数据写入influxdb,主要指标如下:

tags:

  • method,请求类型
  • type,根据响应状态码分组,1xx -> 1, 2xx -> 2
  • spdy,根据自定义的响应时间划分区间,方便将接口响应时间分组
  • route,接口路由

fields:

  • connecting,处理请求数
  • use,处理时长
  • bytes,响应数字长度
  • code,响应状态码
  • url,请求地址
  • ip,用户IP

在influxdb中,tags可用于对数据分组,根据 type 将接口请求分组,将 4 与 5 的单独监控,可以简单快速的把当前接口出错汇总。统计中间件代码如下:

function stats() {
 let connecting = 0;
 const spdyList = [
  100,
  300,
  1000,
  3000,
 ];
 return async (ctx, next) => {
  const start = Date.now();
  const tags = {
   method: ctx.method,
  };
  connecting++;
  const fields = {
   connecting,
   url: ctx.url,
  }
  let status = 0;
  try {
   await next();
  } catch (err) {
   // 出错时状态码从error中获取
   status = err.status;
   throw err;
  } finally {
   // 如果非出错,则从ctx中取状态码
   if (!status) {
    status = ctx.status;
   }
   const use = Date.now() - start;
   connecting--;
   tags.route = ctx._matchedRoute;
   tags.type = `${status / 100 | 0}`
   let spdy = 0;
   // 确认处理时长所在区间
   spdyList.forEach((v, i) => {
    if (use > v) {
     spdy = i + 1;
    }
   });
   tags.spdy = `${spdy}`;

   fields.use = use;
   fields.bytes = ctx.length || 0;
   fields.code = status;
   fields.ip = ctx.ip;
   // 统计数据写入统计系统(如influxdb)
   console.info(tags);
   console.info(fields);
  }
 };
}

app.use(stats());

router.post('/users/v1/:type', async (ctx) => {
 await new Promise(resolve => setTimeout(resolve, 100))
 ctx.body = {
  account: 'vicanso',
 };
});

接口全日志记录

为了方便排查问题,需要将接口的相关信息输出至日志中,中间件的实现如下:

function tracker() {
 const stringify = (data) => JSON.stringify(data, (key, value) => {
  // 对于隐私数据做***处理
  if (/password/.test(key)) {
   return '***';
  }
  return value;
 });
 return async (ctx, next) => {
  const trackerInfo = {
   url: ctx.url,
   form: ctx.request.body,
  };
  try {
   await next();
  } catch (err) {
   trackerInfo.error = err.message;
   throw err;
  } finally {
   trackerInfo.params = ctx.params;
   if (!trackerInfo.error) {
    trackerInfo.body = ctx.body;
   }
   console.info(stringify(trackerInfo))
  }
 };
}

app.use(bodyParser());
app.use(tracker());

router.post('/users/v1/:type', async (ctx) => {
 // ctx.throw(400, '密码出错');
 await new Promise(resolve => setTimeout(resolve, 100))
 ctx.body = {
  account: 'vicanso',
 };
});

使用此中间件之后,可以将所有接口的参数、正常响应数据或出错信息都全部输出至日志中,可根据需要调整 stringify 的实现,将一些隐私数据做***处理。需要注意的是,由于部分接口的body响应体部分较大,是否需要将所有数据都输出至日志最好根据实际情况衡量。如可根据HTTP Method过滤,或者根据url规则等。

参数校验

由于javascript的弱类型,接口参数校验一直是要求最严格的一点,而在了解过 joi 之后,我就一直使用它来做参数校验,如注册功能,账号、密码为必选参数,而邮箱为可选,接口校验的代码如下:

function validate(data, schema) {
 const result = Joi.validate(data, schema);
 if (result.error) {
  // 出错可创建自定义的校验出错类型
  throw result.error;
 }
 return result.value;
}

router.post('/users/v1/register', async (ctx) => {
 const data = validate(ctx.request.body, Joi.object({
  // 账号限制长度为3-20个字符串
  account: Joi.string().min(3).max(20).required(),
  // 密码限制长度为6-30,而且只允许字母与数字
  password: Joi.string().regex(/^[a-zA-Z0-9]{6,30}$/).required(),
  email: Joi.string().email().optional(),
 }));
 ctx.body = {
  account: data.account,
 };
});

通过joi简单快捷实现了参数的校验,不过在实际使用中,有部分的参数校验规则是通用的,如账号、密码这些的校验规则在注册和登录中都通过,但是有些接口是可选,有一些是必须,怎么才能更通用一些呢?代码调整如下:

const userSchema = {
 // 账号限制长度为3-20个字符串
 account: () => Joi.string().min(3).max(20),
 // 密码限制长度为6-30,而且只允许字母与数字
 password: () => Joi.string().regex(/^[a-zA-Z0-9]{6,30}$/),
 email: () => Joi.string().email(),
}

router.post('/users/v1/register', async (ctx) => {
 const data = validate(ctx.request.body, Joi.object({
  account: userSchema.account().required(),
  password: userSchema.password().required(),
  email: userSchema.email().optional(),
 }));
 ctx.body = {
  account: data.account,
 };
});

经此调整后,将用户参数校验的基本规则都定义在 userSchema 中,每个接口在各自的场景下选择不同的参数以及增加规则,提高代码复用率以及校验准确性。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
js 提交和设置表单的值
Dec 19 Javascript
基于JQuery.timer插件实现一个计时器
Apr 25 Javascript
详解JavaScript中undefined与null的区别
Mar 29 Javascript
jQuery实现点击该行即可删除HTML表格行
Oct 17 Javascript
javascript实现动态导入js与css等静态资源文件的方法
Jul 25 Javascript
JavaScript计划任务后台运行的方法
Dec 18 Javascript
JS实现上下左右对称的九九乘法表
Feb 22 Javascript
基于jQuery倒计时插件实现团购秒杀效果
May 13 Javascript
JS闭包与延迟求值用法示例
Dec 22 Javascript
Angular2 Service实现简单音乐播放器服务
Feb 24 Javascript
提升node.js中使用redis的性能遇到的问题及解决方法
Oct 30 Javascript
eslint+prettier统一代码风格的实现方法
Jul 22 Javascript
在js代码拼接dom对象到页面上的模板总结
Oct 21 #Javascript
如何安装控制器JavaScript生成插件详解
Oct 21 #Javascript
Node.js中的不安全跳转如何防御详解
Oct 21 #Javascript
详解Axios 如何取消已发送的请求
Oct 20 #Javascript
使用Vue做一个简单的todo应用的三种方式的示例代码
Oct 20 #Javascript
Iview Table组件中各种组件扩展的使用
Oct 20 #Javascript
详解webpack打包第三方类库的正确姿势
Oct 20 #Javascript
You might like
用PHP与XML联手进行网站编程代码实例
2008/07/10 PHP
PHP文件上传原理简单分析
2011/05/29 PHP
PHP 转义使用详解
2013/07/15 PHP
PHP exif扩展方法开启详解
2014/07/28 PHP
php读取文件内容到数组的方法
2015/03/16 PHP
Laravel框架用户登陆身份验证实现方法详解
2017/09/14 PHP
jquery实现的让超出显示范围外的导航自动固定屏幕最顶上
2011/09/22 Javascript
jquery插件制作 图片走廊 gallery
2012/08/17 Javascript
angular.foreach 循环方法使用指南
2015/01/06 Javascript
JavaScript、tab切换完整版(自动切换、鼠标移入停止、移开运行)
2016/01/05 Javascript
AngularJS 2.0新特性有哪些
2016/02/18 Javascript
js判断请求的url是否可访问,支持跨域判断的实现方法
2016/09/17 Javascript
JS封装的选项卡TAB切换效果示例
2016/09/20 Javascript
jQuery实现简单的手风琴效果
2020/04/17 jQuery
JavaScrpt中如何使用 cookie 设置查看与删除功能
2017/07/09 Javascript
VUE前端cookie简单操作
2017/10/17 Javascript
NodeJs实现简单的爬虫功能案例分析
2018/12/05 NodeJs
JS中的一些常用的函数式编程术语
2019/06/15 Javascript
vue 使用element-ui中的Notification自定义按钮并实现关闭功能及如何处理多个通知
2019/08/17 Javascript
vue组件中实现嵌套子组件案例
2020/08/31 Javascript
Python cookbook(数据结构与算法)实现优先级队列的方法示例
2018/02/18 Python
python爬虫爬取网页表格数据
2018/03/07 Python
将tensorflow.Variable中的某些元素取出组成一个新的矩阵示例
2020/01/04 Python
在PyTorch中使用标签平滑正则化的问题
2020/04/03 Python
pandas按条件筛选数据的实现
2021/02/20 Python
英国在线女鞋目的地:SIMMI
2018/12/27 全球购物
Orlebar Brown官网:设计师泳裤和泳装
2020/12/08 全球购物
阿拉伯时尚购物网站:Nisnass
2021/02/07 全球购物
什么是数据库锁?Oracle中都有哪些类型的锁?
2015/08/21 面试题
外语专业毕业生自荐信
2014/04/14 职场文书
2014超市收银员工作总结
2014/11/13 职场文书
幼儿园家长工作总结2015
2015/04/25 职场文书
小学家庭教育心得体会
2016/01/14 职场文书
护士爱岗敬业心得体会
2016/01/25 职场文书
关于CSS自定义属性与前端页面的主题切换问题
2022/03/21 HTML / CSS
教你win10系统中APPCRASH事件问题解决方法
2022/07/15 数码科技