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 相关文章推荐
Kibo 用于处理键盘事件的Javascript工具库
Oct 28 Javascript
jquery利用ajax调用后台方法实例
Aug 23 Javascript
js实现幻灯片播放图片示例代码
Nov 07 Javascript
JSON无限折叠菜单编写实例
Dec 16 Javascript
javascript获取当前鼠标坐标的方法
Jan 10 Javascript
jQuery+html5+css3实现圆角无刷新表单带输入验证功能代码
Aug 21 Javascript
JS实现密码框根据焦点的获取与失去控制文字的消失与显示效果
Nov 26 Javascript
微信小程序 触控事件详细介绍
Oct 17 Javascript
BootStrap Fileinput的使用教程
Dec 30 Javascript
js模块加载方式浅析
Aug 12 Javascript
微信小程序实现的五星评价功能示例
Apr 25 Javascript
layer弹出层取消遮罩的方法
Sep 25 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 pack与unpack 摸板字符字符含义
2009/10/29 PHP
PHP实现利用MySQL保存session的方法
2014/08/23 PHP
PHP四种基本排序算法示例
2015/04/09 PHP
php版本CKEditor 4和CKFinder安装及配置方法图文教程
2019/06/05 PHP
javascript 浏览器判断 绑定事件 arguments 转换数组 数组遍历
2009/07/06 Javascript
jquery 插件学习(三)
2012/08/06 Javascript
window.location.href中url中数据量太大时的解决方法
2013/12/23 Javascript
javascript面向对象快速入门实例
2015/01/13 Javascript
window.onload使用指南
2015/09/13 Javascript
webpack入门必知必会
2017/01/16 Javascript
基于Vue.js实现tab滑块效果
2017/07/23 Javascript
微信小程序时间标签和时间范围的联动效果
2019/02/15 Javascript
js 计算图片内点个数的示例代码
2019/04/04 Javascript
vue组件间的参数传递实例详解
2019/04/26 Javascript
jsonp实现百度下拉框功能的方法分析
2019/05/10 Javascript
javascript合并两个数组最简单的实现方法
2019/09/14 Javascript
webpack常用配置总览(小结)
2019/11/18 Javascript
微信小程序开发摇一摇功能
2019/11/22 Javascript
基于进程内通讯的python聊天室实现方法
2015/06/28 Python
ubuntu安装mysql pycharm sublime
2018/02/20 Python
python实现简单的文字识别
2018/11/27 Python
python消费kafka数据批量插入到es的方法
2018/12/27 Python
python实现祝福弹窗效果
2019/04/07 Python
python简单鼠标自动点击某区域的实例
2019/06/25 Python
python实现tail -f 功能
2020/01/17 Python
让IE下支持Html5的placeholder属性的插件
2014/09/02 HTML / CSS
日常奢侈品,轻松购物:Verishop
2019/08/20 全球购物
管理学专业个人求职信范文
2013/09/21 职场文书
《诺贝尔》教学反思
2014/02/17 职场文书
物理教学随笔感言
2014/02/22 职场文书
党的生日活动方案
2014/08/15 职场文书
个性发展自我评价2015
2015/03/09 职场文书
python批量更改目录名/文件名的方法
2021/04/18 Python
如何使用Python对NetCDF数据做空间相关分析
2021/04/21 Python
Android Studio实现带三角函数对数运算功能的高级计算器
2022/05/20 Java/Android
pd.drop_duplicates删除重复行的方法实现
2022/06/16 Python