本文收录于GitHub山月行博客:shfshanyue/blog,里面包含了我在实际工作中遇到的问题,思考业务和学习前端的整体方向stack工程系列Node进阶系列幽默风趣的后端程序员通常自嘲是CURDBoy。CURD,即对某个存储资源的增删改查,是完全面向数据的编程。太好了,面向数据的编程通常对业务有更透彻的理解,这样才能编写出更高质量的代码,产生的错误也会更少。既然是面向数据的编程,就更需要避免脏数据的出现,加强数据校验。否则,我们应该信任前端数据验证吗?毕竟前端的数据校验是直接到用户那里去的,在UI层进行更友好的用户反馈。数据验证层的后端由于业务逻辑繁重,需要处理的数据种类繁多,所以分为多个层级。我经历过的后端项目分为Controller、Service、Model、Helper、Entity等,有各种命名层。但是这里肯定有一个层叫Controller,它站在后端的顶层,直接接收客户端传来的数据。由于Controller层是服务端与客户端数据交互的最顶层,秉承FailFast原则,肩负着数据过滤器的作用,直接将非法数据送回,就像秦琼和尉迟恭的门一样神。数据验证还会生成半文档化的副产品。数据校验层只需要看一眼就知道要传递哪些字段,是什么格式。以下是常见的数据校验。本文介绍如何检查它们:必需/可选的基本数据检查,如数字、字符串、时间戳,以及值需要满足的复杂数据检查,如IP和手机号、邮箱和域名constbody={id,name,mobilePhone,email}山月接触过一个没有数据校验层的后端项目。if/else层层泛滥,异常痛苦,分分钟需要重构。JSONSchemaJSONSchema是基于JSON的数据验证格式,附有规范json-schema.org。最新版本(2020-08)为7.0。各种服务器编程语言都实现了规范,比如go、java、php等,当然也有很棒的javascript,比如不温不火的ajv。下面是一个验证用户信息的Schema。可见语法复杂繁琐:{"$schema":"http://json-schema.org/draft-04/schema#","title":"User","description":"用户信息","type":"object","properties":{"id":{"description":"用户ID","type":"integer"},"name":{"description":"用户名","type":"string"},"email":{"description":"UserEmail","type":"string","format":"email","maxLength":20},"mobilePhone":{"description":"用户手机号","type":"string","pattern":"^(?:(?:\+|00)86)?1[3-9]\d{9}$","maxLength":15}},"required":["id","name"]}对于复杂的数据类型校验,JSONSchema内置了如下格式,方便又方便quickVerifyDatesandtimesEmailaddressesHostnamesIPAddressesResourceidentifiersURItemplateJSONPointerRegularExpressions对于没有内置Format的手机号码,使用ajv.addFormat手动添加Formatajv.addFormat('mobilePhone',(str)=>/^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(str));Joijoi号称是最强大的JS验证库,在github上也获得了16000星.与JSONSchema相比,它的语法更加简洁和强大。最强大的JS数据验证库,同样的验证,需要更少的代码,可以进行更强大的验证。以下仅为示例,更多示例请查看文档。constschema=Joi.object({id:Joi.number().required(),name:Joi.number().required(),email:Joi.string().email({minDomainSegments:2,tlds:{allow:['com','net']}}),mobilePhone:Joi.string().pattern(/^(?:(?:\+|00)86)?1[3-9]\d{9}$/),password:Joi.string().pattern(/^[a-zA-Z0-9]{3,30}$/),//检查是否与密码相同repeatPassword:Joi.ref('password'),})//密码和重复密码需要同时发送。with('password','repeat_password');//只能提供一个邮箱地址和手机号码。xor('email','mobilePhone')数据校验和路由层集成因为数据是直接从路由传过来的,所以koajs官方在joi的基础上实现了一个joi-router,前置数据校验到路由层,前端传过来的query,body,params都经过验证。joi-router还根据co-body对前端传输的各种内容类型进行分析和限制。如果仅限于application/json,也能在一定程度上防止CSRF攻击。constrouter=require('koa-joi-router');constpublic=router();public.route({方法:'post',路径:'/signup',验证:{header:joiObject,查询:joiObject,参数:joiObject,body:joiObject,maxBody:'64kb',输出:{'400-600':{body:joiObject}},类型:'json',失败:400,continueOnError:false},pre:异步(ctx,next)=>{awaitcheckAuth(ctx);returnnext();},handler:async(ctx)=>{awaitcreateUser(ctx.request.body);ctx.status=201;},});正则表达式与安全正则表达式山月在排查性能问题时发现一个API在数据验证层耗时过长,这是我从来没有想到的。问题的根源在于不安全的正则表达式,那么什么是不安全的正则表达式呢?比如下面这个能挂CPU的正则表达式就是一个定时炸弹,回溯的次数进入了指数增长。可以参考文章分析ReDosconstsafe=require('safe-regex')constre=/(x+x+)+y///正则re.test('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')//使用safe-regex判断正则表达式是否安全safe(re)//虚假数据校验多为字符串校验,也会充斥着各种正则表达式。确保正则表达式的安全性非常重要。safe-regex可以找出哪些不安全的正则表达式。综上所述,Controller层需要进行统一的数据校验。JSONSchema(Node实现了ajv)和JoiJSONSchema都有各种语言的官方规范和实现,但是语法比较繁琐。Joi,它有更强大的验证功能,可以用来做字符串验证。测试时,注意不安全的正则表达式带来的性能问题
