前言
在当前web项目的前后端分离已经大行其道的时代,通过json进行前后端交互已经成为了必不可少的部分,而对json数据的校验工作会随着项目的逐步扩大而变得日益臃肿且难以维护,尤其是中后台项目提交的有些表单含有的参数特别庞大且结构复杂,如果不能很好的设计一种参数校验的方案,对后续的开发工作将会造成巨大的困扰.
在开发nodejs后端项目时,我们希望最好的结果便是将所有的参数校验部分单独抽离出来放在另外的文件中不和路由模块绑定在一起.将参数校验的逻辑代码封装到一个中间件函数里,如果用户上传的参数不符合规范便阻止其往下运行并返回错误描述.其次有很多字段普遍使用的约束规范能够通过简单的属性值限制就可以达到目的,比如username字段的最小长度为2,最大长度为25,即可这样来表述:{minLength: 2,maxLength: 25}.
方案选择
1.ajv主要参数配置讲解
ajv是一个非常流行的JSON Schema验证工具,并且拥有非常出众的性能表现,我们可以使用它作为后端参数校验的工具库.
首先引入ajv,并生成一个新的实例,随后引入ajv-errors包裹ajv让其具备有自定义错误的功能.ajv.validate是验证参数是否正确的函数,它可以传入两个参数,第一个是参数的类型定义schema,第二个参数是具体被校验的数据.ajv会将数据按照定义的schema规范来一一验证参数的正确性.如果符合schema定义的约束,返回true,否则返回false.当校验为false,我们可以通过ajv.errors获取到具体的错误信息.
下面来看一下schema的定义,type定义了数据的第一级是一个什么类型,如下所示它应该是一个对象,properties表示该对象要包含的属性.如此便确定了第二级有一个data属性它也是一个对象,它下面有code和status两个属性,其中status属性不能缺省.code属性的值的类型只能是string或者integer中的一种,并且最小长度为1最大长度为30,默认值为0.而status是枚举类型,值只能是0和1当中的一种.
另外还可以在每个字段下面定义一个errorMessage属性,一旦ajv.validate方法执行完毕后返回的结果为false,我们可以通过ajv.errors获取到自己定义的errorMessage错误信息.
如果想了解ajv的更多用法,可去其官网查看文档介绍.
const Ajv = require('ajv');
ajv = new Ajv({ allErrors: true, jsonPointers: true });
require('ajv-errors')(ajv);
var schema = {
"type": 'object',
"properties": {
"data": {
"type": "object",
"required": ["status"],
"properties": {
"code": {
"type": ['string', 'integer'],
"minLength": 1,
"maxLength": 30,
"default": "0",
"errorMessage": 'code的格式错了'
},
"status": {
"type": "number",
"enum": [0, 1]
}
}
}
}
};
var validateData = {
data: {
code: "12",
status: 0,
}
};
const valid = ajv.validate(schema, validateData );
2.安装:
在项目的根目录下运行下面代码
npm i ajv ajv-errors --save
集成项目
现在假设当前项目正在开发一个user(用户)模块,它的router层(src/router/user.js)有两个接口注册和登录,代码如下:
const Router = require('@koa/router');
const { genValidator } = require('../middleWares/genValidator');
const { userValidate } = require('../validator/user');
const router = new Router({
prefix: '/api/user',
});
router.post('/register', genValidator(userValidate), async (ctx) => {
//注册接口
});
router.post('/login', genValidator(userValidate), async (ctx) => {
//登录接口
});
exports.router = router;
userValidate 是用户模块的对应的校验器
genValidator是我们用于参数校验的通用中间件函数,我们在根目录下创建middleWares/genValidator.js,通过validateFn运行后的error如果存在则表示校验出错了,我们获取到错误描述并返回给用户.而validateFn是在哪里编写的呢?继续往下.
//定义一个json格式的返回值
class Response {
constructor(error_no, data, message) {
this.error_no = error_no;//错误码
if (data) {
this.data = data; //返回给用户的数据
}
if (message) {
this.message = message;//错误描述
}
}
}
class Fail extends Response {
constructor(error_no = 1, message) {
super(error_no);
if (message) {
this.message = message;
}
}
}
exports.genValidator = (validateFn) => {
return async (ctx, next) => {
const data = ctx.data;
const url = ctx.request.url;
const error = validateFn(data, url.slice(url.lastIndexOf('/') + 1), ctx);
if (error) {
if (Array.isArray(error) && error.length > 0) {
ctx.body = new Fail(50, error[0].message);
} else {
ctx.body = new Fail(50, '请求参数错误!');
}
} else {
await next();
}
};
};
在上面的代码中validateFn其实是在router层传递进去的userValidate,它对应的就是user用户模块的校验器.此时我们可以在src下面创建一个validator文件夹,它下面专门存放每个模块的校验器,我们现在在该文件夹下创建一个user模块的校验器user.js
如果只是对字段的类型,长度,还有能不能为空这些基本属性的检验定义一个schema就可以做到了,但有时候我们需要自定义校验函数.比如我们需要校验两次输入的密码是否相等,再比如我们需要判断用户传递过来的验证码和session中的验证码是否相等,这些校验的结果仅仅依靠定义schema的结构是无法验证正确性的,我们必须要获取到用户数据,通过解析处理数据再做进一步的判断.
如何自定义校验逻辑呢?我们可以使用ajv.addKeyword定义新属性来实现,并且在schema定义的结构中置为true.
我们将每个校验模块的复用代码抽离出来放到一个js中,此后每开发一个模块就引入这些复用的代码,创建validator/common.js
const Ajv = require('ajv');
ajv = new Ajv({ allErrors: true, jsonPointers: true });
require('ajv-errors')(ajv);
//校验函数
exports.validate = (schema, data = {}, ctx) => {
data.ctx = ctx;
const valid = ajv.validate(schema, data);
if (!valid) {
return ajv.errors;
} else {
return false;
}
};
//自定义字段校验逻辑
exports.addKey = (keyword, callback) => {
ajv.addKeyword(keyword, {
modifying: false,
schema: false, // keyword value is not used, can be true
validate: function validateFn(data, dataPath, parentData) {
const message = callback(parentData);
validateFn.errors = [
{
keyword: dataPath.slice(1),
message,
params: { keyword: dataPath.slice(1) },
},
];
return !message;
},
errors: true,
});
};
开始编写user.js(用户模块)的参数校验
const { validate,addKey } = require('./common');
addKey('validateCaptcha', (data) => {
const { ctx } = data;
if (ctx.session && ctx.session['captcha']) {
const captcha = ctx.session['captcha'].toLocaleLowerCase();
if (data.captcha == captcha) {
return false; //验证成功
} else {
return '验证码不正确';
}
} else {
return '验证码已过期';
}
});
addKey('validatePwd', (data) => {
const { password,twice_password} = data;
if (password== twice_password) {
return false;
} else {
return '两次输入密码不一致';
}
});
const SCHEMA = {
type: 'object',
properties: {
user_id: {
type: ['string', 'integer'],
pattern: '[0-9]+',
},
nick: {
type: 'string',
minLength: 1,
maxLength: 255,
default: '无名',
},
password: {
type: 'string',
minLength: 6,
maxLength: 60,
validatePwd:true
},
twice_password: {
type: 'string',
minLength: 6,
maxLength: 60,
},
new_password: {
type: 'string',
minLength: 6,
maxLength: 60,
},
user_name: {
type: 'string',
minLength: 2,
maxLength: 25
},
captcha: {
type: ['string', 'integer'],
minLength: 4,
maxLength: 8,
validateCaptcha: true,
}
},
};
const getScheme = (key) => {
const data = { ...SCHEMA };
if (key === 'login') {
data['required'] = ['user_name', 'password', 'captcha'];
return data;
} else if (key === 'register') {
data['required'] = ['user_name', 'password', 'nick'];
return data;
}
};
exports.userValidate = (data, key, ctx) => {
return validate(getScheme(key), data, ctx);
};
此处定义的userValidate在路由层引入便完成了整个用户模块的参数校验,上面定义的getScheme方法中的key对应的是用户访问的接口地址.对于同属用户模块的登录接口和注册接口而言,他们要求必传的参数是不一样的,我们可以在getScheme方法中给data添加required属性和必须校验的参数数组如此便达到了校验某单一接口参数是否正确的目的.