边写边学系列(二) —— 使用express-validator进行后端校验

6,595 阅读11分钟

边写边学系列目录

【一】:使用apidoc,搞定自动化文档

【二】:使用express-validator进行后端校验

这个系列文章主旨就是通过写代码来入门,并不深入。只是记录我平时使用到了什么新的技术或插件的入门过程~

express-validator

最近写node端后台写的比较多,慢慢的发现前端呢转node端虽然没有那么难,但是有很多细节的东西还没有掌握,比如以前前后端分离的时候,对于一些需求模糊的表单场景,前端可能约束会很松,大部分的约束都是后端去做的,然后用户提交的信息某个字段不合法也是后端反馈给我们异常,前端再做处理。

因为后端直接接触的就是数据库,每一个字段都必须严格约束,所以对于接口字段的验证,特别是post(往数据库insert)的时候,验证必须严格,我们总不能每一个接口都自己写一套正则来进行校验吧,想一想也是,express庞大的社区肯定已经有类似的中间件了。去npm搜了一下关键字express + validate。映入眼帘的就是这个 —— express-validator。

// express-validator官网描述是一个基于validator.js封装的express中间件。
express-validator is a set of express.js middlewares that wraps validator.js validator and sanitizer functions.

Getting Started

还是沿用第一节的套路,不管你三七二十一,先按照官网示例,跑通一个Demo,然后我在慢慢来弄~ 这里我依然节省时间,直接使用我之前写过的全栈脚手架express-react-scaffold来直接使用express-validator

关于这个脚手架的文章在这里新手搭建简洁的Node+React脚手架,正好也是我的第一篇文章,有很多小伙伴也点过赞,一直没时间维护,借此机会温故知新一下,简单回顾了一下,发现当时写的真心锉啊,借此机会小改一下吧~

其实对于后端接口字段校验,首先想到的就是表单提交了,因为对于GET请求,无论是query还是param,大部分校验工作前端来做就已经可以解决问题了,query和param的合法性通过了,一般后端也就不出问题了(当然,并不是说后端就不必校验了)。而post、put等这种涉及到操作数据库的请求,如果字段类型不匹配,就很容易发生未知错误,而且因为表单里不同表单项会有繁琐的校验规则,所以后端必须控制好~

以注册接口为例,跑第一个成功Demo

我们先来看一下以前的注册接口:

可以看到,需要三个字段,那么我们假设是这样的:

前端:
    用户名:非空
    邮箱:必须是邮箱类型
    密码:非空
后端:
    用户名:必须大于6位
    邮箱:必须是邮箱类型
    密码:必须大于6位

从上面我们可以看出,前后端约束条件不同,也就是说可能存在前端输入合法而后端输入不合法的场景~

从文档的例子我们可以知道,express-validator的校验只需要在路由path和handler中间插入校验规则数组,我们来写一下。

// 原来的接口
// 用户注册接口
router.post('/register', (req, res) => {
    User.findOne({ //查找是否存在
      username: req.body.username,
    },(err, user)=>{
        if (err) {
            res.send('server or db error');
        } else {
            if (user === null) {
                const insertObj = {
                  username: req.body.username,
                  password: md5(req.body.password + MD5_SUFFIX),
                  email: req.body.email,
                  role: 10.0
                };
                const newUser = new User(insertObj);
                newUser.save(insertObj, (err, doc) => {
                    if (err) {
                        res.json({ result: false, msg: '用户注册失败' });
                    } else {
                        console.log(doc);
                        res.json({ result: true, msg: '用户注册成功' });
                    }
                });
            } else {
                res.json({ result: false, msg: '用户名已存在'});
            }
        }
    });
});
// 增加验证过后的接口
// 用户注册接口
router.post('/register', [
  check('username').isLength({ min: 6 }),
  check('email').isEmail(),
  check('password').isLength({ min: 6 })
], (req, res) => {
  // Finds the validation errors in this request and wraps them in an object with handy functions
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({ errors: errors.array() });
  }
    User.findOne({ //查找是否存在
      username: req.body.username,
    },(err, user)=>{
        if (err) {
            res.send('server or db error');
        } else {
            if (user === null) {
                const insertObj = {
                  username: req.body.username,
                  password: md5(req.body.password + MD5_SUFFIX),
                  email: req.body.email,
                  role: 10.0
                };
                const newUser = new User(insertObj);
                newUser.save(insertObj, (err, doc) => {
                    if (err) {
                        res.json({ result: false, msg: '用户注册失败' });
                    } else {
                        console.log(doc);
                        res.json({ result: true, msg: '用户注册成功' });
                    }
                });
            } else {
                res.json({ result: false, msg: '用户名已存在'});
            }
        }
    });
});

好,然后我们来试一下:

测试用例: 用户名 - aaa, 用户邮箱 - aaa@126.com, 密码 - aaa

如图,可以看到,前端通过之后,后端没通过,说明我们写的内容生效了。所以!我们的第一个validate demo也就写完了。

知其然也知其所以然

上面第一个例子虽然生效了,但是我其实还是有点稀里糊涂,相信小伙伴们也一样,凭啥?为啥就那么加就通过了?别急,我们一步一步来。 先来看看代码:

// 校验内容部分
[
  check('username').isLength({ min: 6 }),
  check('email').isEmail(),
  check('password').isLength({ min: 6 })
]

// 校验结果部分
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}

校验内容部分就很简单了,无非就是约束条件,现在很简单,以后肯能会变得很复杂,但是不是要考虑的。然后就来看结果部分了,可以看到,通过validationResult(req)获取校验结果,我们将它打印出来看一看:

{
    isEmpty: [Function],
    array: [Function],
    mapped: [Function],
    formatWith: [Function],
    throw: [Function] 
}

可以看到校验结果返回了几个api,我们来猜一猜或者打印一下就知道了,因为代码里只用到了isEmpty()和array(),而且意思很明显,就是如果errors.isEmpty()为真,就表示校验通过,所以用脑袋想一想isEmpty()应该是bool类型返回校验是否通过,如果为假就是校验不通过,然后把不通过的数组信息返回给我们。我们就打印一下二者:

// isEmpty
errors.isEmpty() ====> false // 返回的是bool值,表示结果
errors.array()
[ 
    { 
        location: 'body',
        param: 'username',
        value: 'aaa',
        msg: 'Invalid value,
    },
    { 
        location: 'body',
        param: 'password',
        value: 'aaa',
        msg: 'Invalid value' 
    } 
]

可以看到,errors.array()返回的是校验不通过的字段的数组以及对应的信息。所以关于整体的校验流程基本掌握了。接下来就是巩固加深提高的过程了~

学习使用express-validator的各种API

上面基本了解了如何在后端使用express-validator,但是有一些点还是不理解:

比如:在handler前面加上校验数组,数组的内容是我们写的字段,那么字段如果写错呢?
再比如:他怎么知道我想校验的字段在哪?是query还是param还是body还是header呢?

带着疑惑,我们在看读文档,等一下,读文档之前,其实我们可以再看看上面的错误数组:

[ 
    { 
        location: 'body',
        param: 'username',
        value: 'aaa',
        msg: 'Invalid value,
    },
    { 
        location: 'body',
        param: 'password',
        value: 'aaa',
        msg: 'Invalid value' 
    } 
]

嗯,很明显,错误数组对于我们的字段判断是正确的,location字段它定位的是body,确实,我们的post接口确实将数据放到了body里。因此,应该是express-validator会check所有与我们规定值相匹配的req字段吧,带着疑问去查阅一下文档~

还真是,我们的check还就是把能匹配的都匹配一下,那么问题又来了,这么是不是效率会很低,既然是我们自己写的,我们肯定知道在哪里去找,能提升效率啊~好吧,我都想到了,人家作者能想不到吗?

check API

  • 限定范围类(check, body, query, header, param, cookie)

check API就是校验各种规则的api,其中包括各种封装好的校验函数,如:isString()、isInt()、isLength({})等,除此之外还有很多限定范围的api,如图

可见,也就是上述我们说的问题,我们可以通过约定检索范围提升效率,比如register的接口,我们只需要检验body的字段就行了,那么就可以使用body来进行check,我们来试一试:

const { body, validationResult } = require('express-validator/check');
[
  body('username').isLength({ min: 6 }),
  body('email').isEmail(),
  body('password').isLength({ min: 6 })
]

换完过后,结果依然成立,其他类似的check API也类似了,就是你校验的字段在哪里就用对应API去检验就好了,提升效率~。

  • 自定义限定范围(buildCheckFunction)

出了上述限定范围,我们还可以通过buildCheckFunction来自定义范围,比如我们校验某个字段id只有在body或query才有效,并且是UUID类型的数据,代码如下:

const { buildCheckFunction } = require('express-validator/check');
const checkBodyAndQuery = buildCheckFunction(['body', 'query']);

app.put('/update-product', [
  // id must be either in req.body or req.query, and must be an UUID
  checkBodyAndQuery('id').isUUID()
], productUpdateHandler)
  • 校验结果validationResult(req) 这个很简单了,就是把express req传进去,然后返回我们上面提到的那个error对象~这个就不多做介绍了,因为官方也没有详细介绍。

  • oneOf(validationChains[, message])

这个也很简单,就是只要几个条件之中的一个满足,我们就认为校验是通过的~这个场景说实话我还确实没想过哪里能用到,不过还是试一试,我们在登录接口尝试,将用户名验证是否是字符串,密码验证变成是否是数组,这肯定是个假命题,不过最后结果不出意外是通过,因为用户名是正确的:

// router login - 登录接口
oneOf([
    body('username').isString(),
    body('password').isArray()
])

最后打印出来的结果:validationResult(req).isEmpty() === true

Validation Result API

验证结果的API,也算是最重要的API了,因为校验通过不通过,要返回给客户端什么信息,都是通过这个API获取的。

validationResult(req)

  • isEmpty()

    这个上面说过了,就是返回一个bool值,表示check部分是否有错,有错就是false,没错就是true。一般使用就是:

    if (!validationResult(req).isEmpty()) {
        res.status(错误码).json({
           错误信息 
        });
    }
    
  • formatWith(formatter) 这个api意义我个人觉得也不是很大,不过算是锦上添花吧。就是可以自定义错误信息格式。

    app.post('/create-user', yourValidationChains, (req, res, next) => {
    const errorFormatter = ({ location, msg, param, value, nestedErrors }) => {
        // 定义返回错误的样式,存入array数组
        return `${location}[${param}]: ${msg}`;
      };
      const result = validationResult(req).formatWith(errorFormatter);
      if (!result.isEmpty()) {
        // { errors: [ "body[password]: must be at least 10 chars long" ] }
        return res.json({ errors: result.array() });
      }
      ...
    });
    
    
  • array([options])

    存放返回的错误信息,参数可以设置是否只返回所有错误的第一条,默认返回所有错误。

    Default : { onlyFirstError: false },如果想要默认返回第一条,设置该参数为true即可

  • mapped()

    这个API跟array()基本一致,就是返回错误,不过array()mapped()的区别就是一个返回的是数组,一个返回的是对象,mapped()返回的是 key 和 value 键值对,value跟 array 数组返回的内容一致。

    // 假设我把login接口的username和password的check验证都改成isArray()。
     [
        body('username').isArray(), 
        body('password').isArray()
     ],
     
    // validationResult(req).mapped()
    { 
        username: { 
          location: 'body',
          param: 'username',
          value: 'luffy',
          msg: 'Invalid value' 
        },
       password: { 
          location: 'body',
          param: 'password',
          value: '123456',
          msg: 'Invalid value' 
        } 
    }
    
  • throw()

    使用这个api就是不使用isEmpty(),通过throw()一个error来返回错误。

    try {
      validationResult(req).throw();
      // Oh look at ma' success! All validations passed!
    } catch (err) {
      console.log(err.mapped()); // Oh noes!
    }
    

filter API + Validation Chain API

其实上面两个API我觉得已经足够了,基本满足业务场景了,不过express-validator还提供很多更完善的功能。下面这些API就简单过一下吧,如果有我觉得能用得到的,就写个例子,我觉得check就够用了~哈哈。

  • sanitize系列

    这个与check API类似,也可以限定范围和自定义范围,用处与check API不一样,check API是检验对应参数是否合法,sanitize系列API是可以帮我们提前做一个转换工作,比如我们后台要求的是数字1,但是前端传过来的是字符串'1',就可以通过sanitize系列API进行转换。

    const { buildSanitizeFunction } = require('express-validator/filter');
    const sanitizeBodyAndQuery = buildSanitizeFunction(['body', 'query']);
    
    app.put('/update-product', [
      // 限定范围在body和query内,将id转换成整型
      sanitizeBodyAndQuery('id').toInt()
    ], productUpdateHandler)
    
  • validation chain

    这个也不算新的API,应该就是特性吧,感觉跟jQuery一样,不断的链式调用,每一次调用都返回新的结果~

    // 检验weekday字段是否不在['sunday', 'saturday']内。
    check('weekday').not().isIn(['sunday', 'saturday'])
    
  • withMessage

    这个不是新API,官方是列在Validation Chain API里的,不过我觉得这个是个很有用的API,所有就单独拿出来说一下,就是错误消息可以自定义,我们可以设置返回消息放在里面。

    // 验证部分
    [
        body('username').isArray().withMessage('username类型不正确'), 
        body('password').isArray().withMessage('password类型不正确')
     ],
    // 结果部分
    { 
        username: { 
          location: 'body',
          param: 'username',
          value: 'luffy',
          msg: 'username类型不正确' 
        },
       password: { 
          location: 'body',
          param: 'password',
          value: '123456',
          msg: 'password类型不正确' 
        } 
    }
    

结尾

这篇文章,一如既往,还是我的个人学习过程,如果有人没用过或者想要在自己的项目中使用,应该还是个不错的教程~

Demo代码地址