Node 在 Controller 層如何進行數據校驗

本文收錄於 GitHub 山月行博客: shfshanyue/blog,內含我在實際工做中碰到的問題、關於業務的思考及在全棧方向上的學習javascript

幽默風趣的後端程序員通常自嘲爲 CURD Boy。CURD, 也就是對某一存儲資源的增刪改查,這徹底是面向數據編程啊。php

真好呀,面向數據編程,每每會對業務理解地更加透徹,從而寫出更高質量的代碼,造出更少的 BUG。既然是面向數據編程那更須要避免髒數據的出現,增強數據校驗。不然,難道要相信前端的數據校驗嗎,畢竟前端數據校驗直達用戶,是爲了 UI 層更友好的用戶反饋。html

數據校驗層

後端因爲重業務邏輯以及待處理各類數據,以至於分紅各類各樣的層級,以我經歷過的後端項目就有分爲 ControllerServiceModelHelperEntity 等各類命名的層,五花八門。但這裏確定有一個層稱爲 Controller,站在後端最上層直接接收客戶端傳輸數據。前端

因爲 Controller 層是服務器端中與客戶端數據交互的最頂層,秉承着 Fail Fast 的原則,肩負着數據過濾器的功能,對於不合法數據直接打回去,如同秦瓊與尉遲恭門神般威嚴。java

數據校驗同時衍生了一個半文檔化的副產品,你只須要看一眼數據校驗層,便知道要傳哪些字段,都是些什麼格式。node

如下都是常見的數據校驗,本文講述如何對它們進行校驗:git

  1. required/optional
  2. 基本的數據校驗,如 number、string、timestamp 及值須要知足的條件
  3. 複雜的數據校驗,如 IP、手機號、郵箱與域名
const body = {
  id,
  name,
  mobilePhone,
  email
}

山月接觸過一個沒有數據校驗層的後端項目,if/else 充斥在各類層級,萬分痛苦,分分鐘向重構。程序員

JSON Schema

JSON Schema 基於 JSON 進行數據校驗格式,並附有一份規範 json-schema.org,目前 (2020-08) 最新版本是 7.0。各類服務器編程語言都對規範進行了實現,如 gojavaphp 等,固然偉大的 javascript 也有,如不溫不火的 ajvgithub

如下是校驗用戶信息的一個 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": "用戶郵箱",
      "type": "string",
      "format": "email",
      "maxLength": 20
    },
    "mobilePhone": {
      "description": "用戶手機號",
      "type": "string",
      "pattern": "^(?:(?:\+|00)86)?1[3-9]\d{9}$",
      "maxLength": 15
    }
  },
  "required": ["id", "name"]
}

對於複雜的數據類型校驗,JSON Schema 內置瞭如下 Format,方便快捷校驗

  • Dates and times
  • Email addresses
  • Hostnames
  • IP Addresses
  • Resource identifiers
  • URI template
  • JSON Pointer
  • Regular Expressions

對於不在內置 Format 中的手機號,使用 ajv.addFormat 可手動添加 Format

ajv.addFormat('mobilePhone', (str) => /^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(str));

Joi

joi 自稱最強大的 JS 校驗庫,在 github 也斬獲了一萬六顆星星。相比 JSON Schema 而言,它的語法更加簡潔而且功能強大。

The most powerful data validation library for JS

完成相同的校驗,僅須要更少的代碼,並可以完成更增強大的校驗。如下僅作示例,更多示例請前往文檔。

const schema = 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}$/),
  // 與 password 相同的校驗
  repeatPassword: Joi.ref('password'),
})
  // 密碼與重複密碼須要同時發送
  .with('password', 'repeat_password');
  // 郵箱與手機號提供一個便可
  .xor('email', 'mobilePhone')

數據校驗與路由層集成

因爲數據直接從路由傳遞,所以 koajs 官方基於 joi 實現了一個 joi-router,前置數據校驗到路由層,對前端傳遞來的 querybodyparams 進行校驗。

joi-router 也同時基於 co-body 對前端傳輸的各類 content-type 進行解析及限制。如限制爲 application/json,也可在必定程度上防止 CSRF 攻擊。

const router = require('koa-joi-router');
const public = router();

public.route({
  method: 'post',
  path: '/signup',
  validate: {
    header: joiObject,
    query: joiObject,
    params: joiObject,
    body: joiObject,
    maxBody: '64kb',
    output: { '400-600': { body: joiObject } },
    type: 'json',
    failure: 400,
    continueOnError: false
  },
  pre: async (ctx, next) => {
    await checkAuth(ctx);
    return next();
  },
  handler: async (ctx) => {
    await createUser(ctx.request.body);
    ctx.status = 201;
  },
});

正則表達式與安全正則表達式

山月在一次排查性能問題時發現,一條 API 竟在數據校驗層耗時太久,這是我不曾想到的。而問題根源在於不安全的正則表達式,那什麼叫作不安全的正則表達式呢?

好比下邊這個能把 CPU 跑掛的正則表達式就是一個定時炸彈,回溯次數進入了指數爆炸般的增加。

能夠參考文章 淺析 ReDos 原理與實踐
const safe = require('safe-regex')
const re = /(x+x+)+y/

// 能跑死 CPU 的一個正則
re.test('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')

// 使用 safe-regex 判斷正則是否安全
safe(re)   // false

數據校驗,針對的大可能是字符串校驗,也會充斥着各類各樣的正則表達式,保證正則表達式的安全至關緊要。safe-regex 可以發現哪些不安全的正則表達式。

總結

  1. Controller 層須要進行統一的數據校驗,能夠採用 JSON Schema (Node 實現 ajv) 與 Joi
  2. JSON Schema 有官方規範及各個語言的實現,但語法繁瑣,可以使用校驗功能更爲強大的 Joi
  3. 進行字符串校驗時,注意不安全的正則引發的性能問題
相關文章
相關標籤/搜索