参数校验在那里做?
一般来说,我们为了提升用户体验,在前端通常都会做一次表单输入参数校验。
然而,我们又要在服务端严格遵循「不要相信任何客户端输入」。
所以我们必须在服务端再做一次参数校验。
为什么在 Controller 做?
语雀服务端目前可以大致分为 Controller,Service,Model 三层。
如果将参数校验放 Model 定义里面做,那么在 Controller 和 Service 的代码中,必然会有不少 TypeError 和必要的参数校验,那么相同的参数校验逻辑会被分散到各个地方。
如手机号码,如果只在 Model 层校验,那么在 Service 调用短信网关发送信息之前,必然会做一次手机号码校验,因为这个时候数据还没法被保存到数据库。
而 Controller 的逻辑我们认为它就做 2 件事情,一件是参数校验,一件是构造响应数据结果。
统一在 Controller 做参数校验,所有逻辑只会在这一层被处理完,而且它是入口层,经过它校验过的所有用户参数,都应该被标记为「合法参数」,那么在后续的 Service 和 Model 层都不需要再进行参数校验了,因为它们是可信的合法参数。
未来的架构演变看参数校验
假设当前的单一应用架构无法满足业务需求,应用系统需要分拆成多个,那么当前应用还是属于入口层应用,在 Controller 做参数校验依旧保持着未拆分前的各种优势。
所以在 Controller 做参数校验是比较合理的,并且能够适应未来的架构变化。
整合参数提取和校验
我们已经通过 ctx.params.permit(names) 来提取足够小的参数集合了,那么参数校验应该很自然地一起完成。于是我们封装了 ctx.permitAndValidateParams(rules)
来将两项基本工作变成标准动作。
permitAndValidateParams 的实现
// context.js
permitAndValidateParams(rules) {
const params = this.params.permit(Object.keys(rules));
const errors = this.app.validator.validate(rules, params);
if (errors) {
const keys = errors.map(error => error.field).join(',');
throw this.validateParamsError(`${keys} invalid`, {
code: 'invalid',
status: 422,
detail: errors,
});
}
return params;
}
使用例子
class RecommendController extends LarkController {
getRules(isHeadline) {
return {
title: { required: false, type: 'string' },
url: 'url',
recommend_type: { required: false, type: 'enum', values: [ 0, 1 ] },
description: { required: isHeadline, type: 'string' },
cover: { required: isHeadline, type: 'url' },
};
}
* create() {
const { ctx } = this;
const isHeadline = ctx.params.recommend_type === ctx.model.SpaceRecommend.TYPE.headline;
const createParams = ctx.permitAndValidateParams(this.getRules(isHeadline));
createParams.space_id = ctx.space_id;
const spaceRecommend = yield ctx.model.SpaceRecommend.create(createParams);
ctx.success(spaceRecommend);
}
参数校验失败处理
一旦有任何一个参数校验失败,会自动按 422 标准响应返回。
{
code: 'invalid',
status: 422,
detail:
[
{ message: 'should be a url', code: 'invalid', field: 'cover' }
],
message: 'cover invalid'
}
更多内置的参数规则
目前内置了所有 parameter 自带的参数规则。同时还提供了自定义规则的扩展方式,可以参考 app/validator_rule.js
中的自定义规则。
如果你想要更多通用的自定义规则,可以基于 validator 来实现自定义规则。
基于 validator 实现自定义规则
以中国手机号码规则为例子。
先定义规则 ChinaMobilePhone
// app/validator_rule.js
app.validator.addRule('ChinaMobilePhone', (rule, value) => {
if (!validator.isMobilePhone(value, 'zh-CN')) {
return 'must be China mobile phone number';
}
});
使用 ChinaMobilePhone
规则
const params = ctx.permitAndValidateParams({
mobile: 'ChinaMobilePhone',
});
了解更多实现细节
此方案实现是结合 egg-parameters 和 egg-validate 来实现的,详细可参考它们的源代码。
再次提醒
⏰「不要相信任何客户端输入」。