本文收錄於 GitHub 山月行部落格: shfshanyue/blog,內含我在實際工作中碰到的問題、關於業務的思考及在全棧方向上的學習
幽默風趣的後端程式設計師一般自嘲為 CURD Boy。CURD, 也就是對某一儲存資源的增刪改查,這完全是面向資料程式設計啊。
真好呀,面向資料程式設計,往往會對業務理解地更加透徹,從而寫出更高質量的程式碼,造出更少的 BUG。既然是面向資料程式設計那更需要避免髒資料的出現,加強資料校驗。否則,難道要相信前端的資料校驗嗎,畢竟前端資料校驗直達使用者,是為了 UI 層更友好的使用者反饋。
資料校驗層
後端由於重業務邏輯以及待處理各種資料,以致於分成各種各樣的層級,以我經歷過的後端專案就有分為 Controller
、Service
、Model
、Helper
、Entity
等各種命名的層,五花八門。但這裡肯定有一個層稱為 Controller
,站在後端最上層直接接收客戶端傳輸資料。
由於 Controller
層是伺服器端中與客戶端資料互動的最頂層,秉承著 Fail Fast
的原則,肩負著資料過濾器的功能,對於不合法資料直接打回去,如同秦瓊與尉遲恭門神般威嚴。
資料校驗同時衍生了一個半文件化的副產品,你只需要看一眼資料校驗層,便知道要傳哪些欄位,都是些什麼格式。
以下都是常見的資料校驗,本文講述如何對它們進行校驗:
- required/optional
- 基本的資料校驗,如 number、string、timestamp 及值需要滿足的條件
- 複雜的資料校驗,如 IP、手機號、郵箱與域名
const body = {
id,
name,
mobilePhone,
email
}
山月接觸過一個沒有資料校驗層的後端專案,if/else
充斥在各種層級,萬分痛苦,分分鐘向重構。
JSON Schema
JSON Schema
基於 JSON 進行資料校驗格式,並附有一份規範 json-schema.org,目前 (2020-08) 最新版本是 7.0。各種伺服器程式語言都對規範進行了實現,如 go
、java
、php
等,當然偉大的 javascript 也有,如不溫不火的 ajv。
以下是校驗使用者資訊的一個 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,前置資料校驗到路由層,對前端傳遞來的 query
、body
與 params
進行校驗。
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 能夠發現哪些不安全的正規表示式。
總結
- Controller 層需要進行統一的資料校驗,可以採用 JSON Schema (Node 實現 ajv) 與 Joi
- JSON Schema 有官方規範及各個語言的實現,但語法繁瑣,可使用校驗功能更為強大的 Joi
- 進行字串校驗時,注意不安全的正則引起的效能問題