驗證與序列化
驗證與序列化
Fastify 採用基於 Schema 的方法,即使不是強制性的,我們仍建議使用 JSON Schema 來驗證您的路由並序列化您的輸出。在內部,Fastify 會將 Schema 編譯成高效能的函式。
只有在內容類型為 application-json
時才會嘗試驗證,如內容類型剖析器的文件所述。
本節中的所有範例均使用 JSON Schema Draft 7 規格。
⚠ 安全注意事項
將 Schema 定義視為應用程式程式碼。驗證和序列化功能會使用
new Function()
動態評估程式碼,這對於使用者提供的 Schema 來說是不安全的。有關更多詳細資訊,請參閱 Ajv 和 fast-json-stringify。無論如何,Fastify 支援
$async
Ajv 功能,但不應將其用作第一個驗證策略的一部分。此選項用於存取資料庫,在驗證過程中讀取它們可能會導致應用程式遭受阻斷服務攻擊。如果您需要執行async
工作,請在驗證完成後使用 Fastify 的鉤子,例如preHandler
。
核心概念
驗證和序列化任務由兩個不同的、可自訂的參與者處理
- Ajv v8 用於驗證請求
- fast-json-stringify 用於序列化回應的內容
這兩個獨立的實體只共用透過 .addSchema(schema)
新增到 Fastify 實例的 JSON Schema。
新增共用 Schema
透過 addSchema
API,您可以將多個 Schema 新增到 Fastify 實例,然後在應用程式的多個部分重複使用它們。與往常一樣,此 API 是封裝的。
共用 Schema 可以透過 JSON Schema $ref
關鍵字重複使用。以下是如何參考的概述
myField: { $ref: '#foo'}
將在目前的 Schema 內搜尋具有$id: '#foo'
的欄位myField: { $ref: '#/definitions/foo'}
將在目前的 Schema 內搜尋欄位definitions.foo
myField: { $ref: 'http://url.com/sh.json#'}
將搜尋透過$id: 'http://url.com/sh.json'
新增的共用 SchemamyField: { $ref: 'http://url.com/sh.json#/definitions/foo'}
將搜尋透過$id: 'http://url.com/sh.json'
新增的共用 Schema,並使用欄位definitions.foo
myField: { $ref: 'http://url.com/sh.json#foo'}
將搜尋透過$id: 'http://url.com/sh.json'
新增的共用 Schema,並在其中尋找具有$id: '#foo'
的物件
簡單用法
fastify.addSchema({
$id: 'http://example.com/',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
fastify.post('/', {
handler () {},
schema: {
body: {
type: 'array',
items: { $ref: 'http://example.com#/properties/hello' }
}
}
})
$ref
作為根參考
fastify.addSchema({
$id: 'commonSchema',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
fastify.post('/', {
handler () {},
schema: {
body: { $ref: 'commonSchema#' },
headers: { $ref: 'commonSchema#' }
}
})
擷取共用 Schema
如果驗證器和序列化器是自訂的,則 .addSchema
方法將無用,因為參與者不再由 Fastify 控制。若要存取新增到 Fastify 實例的 Schema,您可以簡單地使用 .getSchemas()
fastify.addSchema({
$id: 'schemaId',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
const mySchemas = fastify.getSchemas()
const mySchema = fastify.getSchema('schemaId')
與往常一樣,函式 getSchemas
是封裝的,並傳回在選定範圍內可用的共用 Schema
fastify.addSchema({ $id: 'one', my: 'hello' })
// will return only `one` schema
fastify.get('/', (request, reply) => { reply.send(fastify.getSchemas()) })
fastify.register((instance, opts, done) => {
instance.addSchema({ $id: 'two', my: 'ciao' })
// will return `one` and `two` schemas
instance.get('/sub', (request, reply) => { reply.send(instance.getSchemas()) })
instance.register((subinstance, opts, done) => {
subinstance.addSchema({ $id: 'three', my: 'hola' })
// will return `one`, `two` and `three`
subinstance.get('/deep', (request, reply) => { reply.send(subinstance.getSchemas()) })
done()
})
done()
})
驗證
路由驗證在內部依賴 Ajv v8,這是一個高效能的 JSON Schema 驗證器。驗證輸入非常容易:只需將您需要的欄位新增到路由 Schema 內,就完成了!
支援的驗證如下
body
:驗證 POST、PUT 或 PATCH 方法的請求主體。querystring
或query
:驗證查詢字串。params
:驗證路由參數。headers
:驗證請求標頭。
所有驗證都可以是完整的 JSON Schema 物件 (具有 type
屬性為 'object'
和包含參數的 'properties'
物件),或是較簡單的變體,其中 type
和 properties
屬性會被省略,而參數會列在最上層 (請參閱以下範例)。
ℹ 如果您需要使用最新版本的 Ajv (v8),您應該閱讀如何在
schemaController
區段中執行此操作。
範例
const bodyJsonSchema = {
type: 'object',
required: ['requiredKey'],
properties: {
someKey: { type: 'string' },
someOtherKey: { type: 'number' },
requiredKey: {
type: 'array',
maxItems: 3,
items: { type: 'integer' }
},
nullableKey: { type: ['number', 'null'] }, // or { type: 'number', nullable: true }
multipleTypesKey: { type: ['boolean', 'number'] },
multipleRestrictedTypesKey: {
oneOf: [
{ type: 'string', maxLength: 5 },
{ type: 'number', minimum: 10 }
]
},
enumKey: {
type: 'string',
enum: ['John', 'Foo']
},
notTypeKey: {
not: { type: 'array' }
}
}
}
const queryStringJsonSchema = {
type: 'object',
properties: {
name: { type: 'string' },
excitement: { type: 'integer' }
}
}
const paramsJsonSchema = {
type: 'object',
properties: {
par1: { type: 'string' },
par2: { type: 'number' }
}
}
const headersJsonSchema = {
type: 'object',
properties: {
'x-foo': { type: 'string' }
},
required: ['x-foo']
}
const schema = {
body: bodyJsonSchema,
querystring: queryStringJsonSchema,
params: paramsJsonSchema,
headers: headersJsonSchema
}
fastify.post('/the/url', { schema }, handler)
對於 body
Schema,還可以透過在 content
屬性內巢狀 Schema 來區分每個內容類型的 Schema。Schema 驗證將根據請求中的 Content-Type
標頭套用。
fastify.post('/the/url', {
schema: {
body: {
content: {
'application/json': {
schema: { type: 'object' }
},
'text/plain': {
schema: { type: 'string' }
}
// Other content types will not be validated
}
}
}
}, handler)
請注意,Ajv 會嘗試將值 強制轉換 為您的 Schema type
關鍵字中指定的類型,以便通過驗證並在之後使用正確類型化的資料。
Fastify 中的 Ajv 預設配置支援強制轉換 querystring
中的陣列參數。範例
const opts = {
schema: {
querystring: {
type: 'object',
properties: {
ids: {
type: 'array',
default: []
},
},
}
}
}
fastify.get('/', opts, (request, reply) => {
reply.send({ params: request.query }) // echo the querystring
})
fastify.listen({ port: 3000 }, (err) => {
if (err) throw err
})
curl -X GET "http://localhost:3000/?ids=1
{"params":{"ids":["1"]}}
您還可以為每個參數類型 (body、querystring、params、headers) 指定自訂 Schema 驗證器。
例如,以下程式碼只會停用 body
參數的類型強制轉換,變更 ajv 預設選項
const schemaCompilers = {
body: new Ajv({
removeAdditional: false,
coerceTypes: false,
allErrors: true
}),
params: new Ajv({
removeAdditional: false,
coerceTypes: true,
allErrors: true
}),
querystring: new Ajv({
removeAdditional: false,
coerceTypes: true,
allErrors: true
}),
headers: new Ajv({
removeAdditional: false,
coerceTypes: true,
allErrors: true
})
}
server.setValidatorCompiler(req => {
if (!req.httpPart) {
throw new Error('Missing httpPart')
}
const compiler = schemaCompilers[req.httpPart]
if (!compiler) {
throw new Error(`Missing compiler for ${req.httpPart}`)
}
return compiler.compile(req.schema)
})
有關更多資訊,請參閱這裡
Ajv 外掛
您可以提供您想要與預設 ajv
實例一起使用的外掛清單。請注意,外掛必須與 Fastify 隨附的 Ajv 版本相容。
請參閱
ajv options
以檢查外掛格式
const fastify = require('fastify')({
ajv: {
plugins: [
require('ajv-merge-patch')
]
}
})
fastify.post('/', {
handler (req, reply) { reply.send({ ok: 1 }) },
schema: {
body: {
$patch: {
source: {
type: 'object',
properties: {
q: {
type: 'string'
}
}
},
with: [
{
op: 'add',
path: '/properties/q',
value: { type: 'number' }
}
]
}
}
}
})
fastify.post('/foo', {
handler (req, reply) { reply.send({ ok: 1 }) },
schema: {
body: {
$merge: {
source: {
type: 'object',
properties: {
q: {
type: 'string'
}
}
},
with: {
required: ['q']
}
}
}
}
})
驗證器編譯器
validatorCompiler
是一個函式,會傳回一個函式來驗證主體、URL 參數、標頭和查詢字串。預設的 validatorCompiler
會傳回一個實作 ajv 驗證介面的函式。Fastify 在內部使用它來加速驗證。
Fastify 的 基準 ajv 配置如下
{
coerceTypes: 'array', // change data type of data to match type keyword
useDefaults: true, // replace missing properties and items with the values from corresponding default keyword
removeAdditional: true, // remove additional properties if additionalProperties is set to false, see: https://ajv.js.org/guide/modifying-data.html#removing-additional-properties
uriResolver: require('fast-uri'),
addUsedSchema: false,
// Explicitly set allErrors to `false`.
// When set to `true`, a DoS attack is possible.
allErrors: false
}
可以透過向 Fastify 工廠提供 ajv.customOptions
來修改此基準配置。
如果您想要變更或設定其他配置選項,您需要建立自己的實例並覆寫現有的實例,如下所示
const fastify = require('fastify')()
const Ajv = require('ajv')
const ajv = new Ajv({
removeAdditional: 'all',
useDefaults: true,
coerceTypes: 'array',
// any other options
// ...
})
fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => {
return ajv.compile(schema)
})
注意:如果您使用任何驗證器 (甚至是 Ajv) 的自訂實例,您必須將 Schema 新增到驗證器,而不是 Fastify,因為 Fastify 的預設驗證器不再使用,而且 Fastify 的 addSchema
方法不知道您使用的是哪個驗證器。
使用其他驗證程式庫
setValidatorCompiler
函式可讓您輕鬆地將 ajv
替換為幾乎任何 JavaScript 驗證程式庫 (joi、yup...) 或自訂的程式庫
const Joi = require('joi')
fastify.post('/the/url', {
schema: {
body: Joi.object().keys({
hello: Joi.string().required()
}).required()
},
validatorCompiler: ({ schema, method, url, httpPart }) => {
return data => schema.validate(data)
}
}, handler)
const yup = require('yup')
// Validation options to match ajv's baseline options used in Fastify
const yupOptions = {
strict: false,
abortEarly: false, // return all errors
stripUnknown: true, // remove additional properties
recursive: true
}
fastify.post('/the/url', {
schema: {
body: yup.object({
age: yup.number().integer().required(),
sub: yup.object().shape({
name: yup.string().required()
}).required()
})
},
validatorCompiler: ({ schema, method, url, httpPart }) => {
return function (data) {
// with option strict = false, yup `validateSync` function returns the
// coerced value if validation was successful, or throws if validation failed
try {
const result = schema.validateSync(data, yupOptions)
return { value: result }
} catch (e) {
return { error: e }
}
}
}
}, handler)
.statusCode
屬性
所有驗證錯誤都會新增一個 .statusCode
屬性,設定為 400
。這可確保預設的錯誤處理常式會將回應的狀態碼設定為 400
。
fastify.setErrorHandler(function (error, request, reply) {
request.log.error(error, `This error has status code ${error.statusCode}`)
reply.status(error.statusCode).send(error)
})
使用其他驗證程式庫的驗證訊息
Fastify 的驗證錯誤訊息與預設的驗證引擎緊密結合:從 ajv
傳回的錯誤最終會通過 schemaErrorFormatter
函式執行,該函式負責建立人類易讀的錯誤訊息。但是,schemaErrorFormatter
函式是以 ajv
為基礎編寫的。因此,當您使用其他驗證程式庫時,可能會遇到奇怪或不完整的錯誤訊息。
若要規避此問題,您有 2 個主要選項
- 確保您的驗證函式 (由您的自訂
schemaCompiler
傳回) 傳回與ajv
相同的結構和格式的錯誤 (雖然由於驗證引擎之間的差異,這可能被證明是困難且棘手的) - 或者使用自訂的
errorHandler
來攔截並格式化您的「自訂」驗證錯誤。
為了協助您撰寫自訂的 errorHandler
,Fastify 會在所有驗證錯誤中新增 2 個屬性:
validation
:驗證函式所返回物件的error
屬性內容(由您的自訂schemaCompiler
返回)。validationContext
:發生驗證錯誤的「上下文」(body、params、query、headers)。
以下顯示一個非常牽強的自訂 errorHandler
處理驗證錯誤的範例。
const errorHandler = (error, request, reply) => {
const statusCode = error.statusCode
let response
const { validation, validationContext } = error
// check if we have a validation error
if (validation) {
response = {
// validationContext will be 'body' or 'params' or 'headers' or 'query'
message: `A validation error occurred when validating the ${validationContext}...`,
// this is the result of your validation library...
errors: validation
}
} else {
response = {
message: 'An error occurred...'
}
}
// any additional work here, eg. log error
// ...
reply.status(statusCode).send(response)
}
序列化
通常,您會以 JSON 格式將資料傳送給客戶端,而 Fastify 有一個強大的工具可以幫助您,fast-json-stringify,如果您在路由選項中提供了輸出 schema,就會使用它。我們鼓勵您使用輸出 schema,因為它可以大幅提高吞吐量,並有助於防止意外洩露敏感資訊。
範例
const schema = {
response: {
200: {
type: 'object',
properties: {
value: { type: 'string' },
otherValue: { type: 'boolean' }
}
}
}
}
fastify.post('/the/url', { schema }, handler)
如您所見,回應 schema 是基於狀態碼。如果您想為多個狀態碼使用相同的 schema,您可以使用 '2xx'
或 default
,例如:
const schema = {
response: {
default: {
type: 'object',
properties: {
error: {
type: 'boolean',
default: true
}
}
},
'2xx': {
type: 'object',
properties: {
value: { type: 'string' },
otherValue: { type: 'boolean' }
}
},
201: {
// the contract syntax
value: { type: 'string' }
}
}
}
fastify.post('/the/url', { schema }, handler)
您甚至可以為不同的內容類型使用特定的回應 schema。例如:
const schema = {
response: {
200: {
description: 'Response schema that support different content types'
content: {
'application/json': {
schema: {
name: { type: 'string' },
image: { type: 'string' },
address: { type: 'string' }
}
},
'application/vnd.v1+json': {
schema: {
type: 'array',
items: { $ref: 'test' }
}
}
}
},
'3xx': {
content: {
'application/vnd.v2+json': {
schema: {
fullName: { type: 'string' },
phone: { type: 'string' }
}
}
}
},
default: {
content: {
// */* is match-all content-type
'*/*': {
schema: {
desc: { type: 'string' }
}
}
}
}
}
}
fastify.post('/url', { schema }, handler)
序列化器編譯器
serializerCompiler
是一個函式,它會返回一個函式,該函式必須從輸入物件返回一個字串。當您定義回應 JSON Schema 時,您可以透過提供一個函式來序列化每個路由來變更預設的序列化方法。
fastify.setSerializerCompiler(({ schema, method, url, httpStatus, contentType }) => {
return data => JSON.stringify(data)
})
fastify.get('/user', {
handler (req, reply) {
reply.send({ id: 1, name: 'Foo', image: 'BIG IMAGE' })
},
schema: {
response: {
'2xx': {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' }
}
}
}
}
})
如果您需要在程式碼的非常特定部分中使用自訂的序列化器,您可以使用 reply.serializer(...)
進行設定。
錯誤處理
當請求的 schema 驗證失敗時,Fastify 會自動返回一個狀態碼 400 的回應,其中包含驗證器在 payload 中的結果。例如,如果您的路由有以下 schema:
const schema = {
body: {
type: 'object',
properties: {
name: { type: 'string' }
},
required: ['name']
}
}
並且未能滿足它,該路由將立即返回一個包含以下 payload 的回應:
{
"statusCode": 400,
"error": "Bad Request",
"message": "body should have required property 'name'"
}
如果您想在路由內處理錯誤,您可以為您的路由指定 attachValidation
選項。如果發生驗證錯誤,請求的 validationError
屬性將包含 Error
物件,其中包含原始 validation
結果,如下所示:
const fastify = Fastify()
fastify.post('/', { schema, attachValidation: true }, function (req, reply) {
if (req.validationError) {
// `req.validationError.validation` contains the raw validation error
reply.code(400).send(req.validationError)
}
})
schemaErrorFormatter
如果您想自行格式化錯誤,您可以在實例化 Fastify 時,提供一個必須返回錯誤的同步函式作為 schemaErrorFormatter
選項。上下文函式將是 Fastify 伺服器實例。
errors
是一個 Fastify schema 錯誤 FastifySchemaValidationError
的陣列。dataVar
是目前驗證的 schema 部分。(params | body | querystring | headers)。
const fastify = Fastify({
schemaErrorFormatter: (errors, dataVar) => {
// ... my formatting logic
return new Error(myErrorMessage)
}
})
// or
fastify.setSchemaErrorFormatter(function (errors, dataVar) {
this.log.error({ err: errors }, 'Validation failed')
// ... my formatting logic
return new Error(myErrorMessage)
})
您也可以使用 setErrorHandler 來定義驗證錯誤的自訂回應,例如:
fastify.setErrorHandler(function (error, request, reply) {
if (error.validation) {
reply.status(422).send(new Error('validation failed'))
}
})
如果您想要在 schema 中快速且無痛地獲得自訂錯誤回應,請查看 ajv-errors
。請查看 範例 用法。
請確保安裝
ajv-errors
的 1.0.1 版本,因為它較新的版本與 AJV v6(Fastify v3 附帶的版本)不相容。
以下是一個範例,說明如何透過提供自訂的 AJV 選項來為 schema 的每個屬性新增自訂錯誤訊息。以下 schema 中的內嵌註解描述了如何配置它以針對每種情況顯示不同的錯誤訊息
const fastify = Fastify({
ajv: {
customOptions: {
jsonPointers: true,
// Warning: Enabling this option may lead to this security issue https://www.cvedetails.com/cve/CVE-2020-8192/
allErrors: true
},
plugins: [
require('ajv-errors')
]
}
})
const schema = {
body: {
type: 'object',
properties: {
name: {
type: 'string',
errorMessage: {
type: 'Bad name'
}
},
age: {
type: 'number',
errorMessage: {
type: 'Bad age', // specify custom message for
min: 'Too young' // all constraints except required
}
}
},
required: ['name', 'age'],
errorMessage: {
required: {
name: 'Why no name!', // specify error message for when the
age: 'Why no age!' // property is missing from input
}
}
}
}
fastify.post('/', { schema, }, (request, reply) => {
reply.send({
hello: 'world'
})
})
如果您想返回本地化的錯誤訊息,請查看 ajv-i18n
const localize = require('ajv-i18n')
const fastify = Fastify()
const schema = {
body: {
type: 'object',
properties: {
name: {
type: 'string',
},
age: {
type: 'number',
}
},
required: ['name', 'age'],
}
}
fastify.setErrorHandler(function (error, request, reply) {
if (error.validation) {
localize.ru(error.validation)
reply.status(400).send(error.validation)
return
}
reply.send(error)
})
JSON Schema 支援
JSON Schema 提供了優化 schema 的實用工具,結合 Fastify 的共享 schema,可讓您輕鬆地重複使用所有 schema。
使用案例 | 驗證器 | 序列化器 |
---|---|---|
$ref 到 $id | ✔️ | ✔️ |
$ref 到 /definitions | ✔️ | ✔️ |
$ref 到共享 schema $id | ✔️ | ✔️ |
$ref 到共享 schema /definitions | ✔️ | ✔️ |
範例
在同一個 JSON Schema 中使用 $ref
到 $id
const refToId = {
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { type: 'string' }
}
}
},
properties: {
home: { $ref: '#address' },
work: { $ref: '#address' }
}
}
在同一個 JSON Schema 中使用 $ref
到 /definitions
const refToDefinitions = {
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { type: 'string' }
}
}
},
properties: {
home: { $ref: '#/definitions/foo' },
work: { $ref: '#/definitions/foo' }
}
}
使用 $ref
到作為外部 schema 的共享 schema $id
fastify.addSchema({
$id: 'http://foo/common.json',
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { type: 'string' }
}
}
}
})
const refToSharedSchemaId = {
type: 'object',
properties: {
home: { $ref: 'http://foo/common.json#address' },
work: { $ref: 'http://foo/common.json#address' }
}
}
使用 $ref
到作為外部 schema 的共享 schema /definitions
fastify.addSchema({
$id: 'http://foo/shared.json',
type: 'object',
definitions: {
foo: {
type: 'object',
properties: {
city: { type: 'string' }
}
}
}
})
const refToSharedSchemaDefinitions = {
type: 'object',
properties: {
home: { $ref: 'http://foo/shared.json#/definitions/foo' },
work: { $ref: 'http://foo/shared.json#/definitions/foo' }
}
}
資源
- JSON Schema
- 理解 JSON Schema
- fast-json-stringify 文件
- Ajv 文件
- Ajv i18n
- Ajv 自訂錯誤
- 使用核心方法和錯誤檔案轉儲的自訂錯誤處理 範例