参数校验在那里做?

一般来说,我们为了提升用户体验,在前端通常都会做一次表单输入参数校验。
然而,我们又要在服务端严格遵循「不要相信任何客户端输入」。
所以我们必须在服务端再做一次参数校验。

为什么在 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 的实现

  1. // context.js
  2. permitAndValidateParams(rules) {
  3. const params = this.params.permit(Object.keys(rules));
  4. const errors = this.app.validator.validate(rules, params);
  5. if (errors) {
  6. const keys = errors.map(error => error.field).join(',');
  7. throw this.validateParamsError(`${keys} invalid`, {
  8. code: 'invalid',
  9. status: 422,
  10. detail: errors,
  11. });
  12. }
  13. return params;
  14. }

使用例子

  1. class RecommendController extends LarkController {
  2. getRules(isHeadline) {
  3. return {
  4. title: { required: false, type: 'string' },
  5. url: 'url',
  6. recommend_type: { required: false, type: 'enum', values: [ 0, 1 ] },
  7. description: { required: isHeadline, type: 'string' },
  8. cover: { required: isHeadline, type: 'url' },
  9. };
  10. }
  11. * create() {
  12. const { ctx } = this;
  13. const isHeadline = ctx.params.recommend_type === ctx.model.SpaceRecommend.TYPE.headline;
  14. const createParams = ctx.permitAndValidateParams(this.getRules(isHeadline));
  15. createParams.space_id = ctx.space_id;
  16. const spaceRecommend = yield ctx.model.SpaceRecommend.create(createParams);
  17. ctx.success(spaceRecommend);
  18. }

参数校验失败处理

一旦有任何一个参数校验失败,会自动按 422 标准响应返回。

  1. {
  2. code: 'invalid',
  3. status: 422,
  4. detail:
  5. [
  6. { message: 'should be a url', code: 'invalid', field: 'cover' }
  7. ],
  8. message: 'cover invalid'
  9. }

更多内置的参数规则

目前内置了所有 parameter 自带的参数规则。同时还提供了自定义规则的扩展方式,可以参考 app/validator_rule.js 中的自定义规则。

如果你想要更多通用的自定义规则,可以基于 validator 来实现自定义规则。

基于 validator 实现自定义规则

以中国手机号码规则为例子。

先定义规则 ChinaMobilePhone

  1. // app/validator_rule.js
  2. app.validator.addRule('ChinaMobilePhone', (rule, value) => {
  3. if (!validator.isMobilePhone(value, 'zh-CN')) {
  4. return 'must be China mobile phone number';
  5. }
  6. });

使用 ChinaMobilePhone 规则

  1. const params = ctx.permitAndValidateParams({
  2. mobile: 'ChinaMobilePhone',
  3. });

了解更多实现细节

此方案实现是结合 egg-parametersegg-validate 来实现的,详细可参考它们的源代码。

再次提醒

⏰「不要相信任何客户端输入」。