Fastify 使用基于 schema 的途徑,從本質(zhì)上將 schema 編譯成了高性能的函數(shù),來實(shí)現(xiàn)路由的驗(yàn)證與輸出的序列化。我們推薦使用 JSON Schema,雖然這并非必要。
? 安全須知應(yīng)當(dāng)將 schema 的定義寫入代碼。 因?yàn)椴还苁球?yàn)證還是序列化,都會使用 new Function() 來動態(tài)生成代碼并執(zhí)行。 所以,用戶提供的 schema 是不安全的。 更多內(nèi)容,請看 Ajv 與 fast-json-stringify。
路由的驗(yàn)證是依賴 Ajv 實(shí)現(xiàn)的。這是一個高性能的 JSON schema 校驗(yàn)工具。驗(yàn)證輸入十分簡單,只需將字段加入路由的 schema 中即可!支持的驗(yàn)證類型如下:
示例:
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'] }, // 或 { 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 = {
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)
請注意,Ajv 會嘗試將數(shù)據(jù)隱式轉(zhuǎn)換為 schema 中 type 屬性指明的類型。這么做的目的是通過校驗(yàn),并在后續(xù)過程中使用正確類型的數(shù)據(jù)。
感謝 addSchema API,它讓你可以向 Fastify 實(shí)例添加多個 schema,并在你程序的不同部分使用它們。該 API 也是封裝好的。
有兩種方式可以復(fù)用你的共用 shema:
以下展示了你可以 如何 設(shè)置 $id 以及 如何 引用它:
更多例子:
使用$ref 的例子:
fastify.addSchema({
$id: 'http://example.com/common.json',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
fastify.route({
method: 'POST',
url: '/',
schema: {
body: {
type: 'array',
items: { $ref: 'http://example.com/common.json#/properties/hello' }
}
},
handler: () => {}
})
替換方式 的例子:
const fastify = require('fastify')()
fastify.addSchema({
$id: 'greetings',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
fastify.route({
method: 'POST',
url: '/',
schema: {
body: 'greetings#'
},
handler: () => {}
})
fastify.register((instance, opts, done) => {
/**
* 你可以在子作用域中使用在上層作用域里定義的 scheme,比如 'greetings'。
* 父級作用域則無法使用子作用域定義的 schema。
*/
instance.addSchema({
$id: 'framework',
type: 'object',
properties: {
fastest: { type: 'string' },
hi: 'greetings#'
}
})
instance.route({
method: 'POST',
url: '/sub',
schema: {
body: 'framework#'
},
handler: () => {}
})
done()
})
在任意位置你都能使用共用 schema,無論是在應(yīng)用頂層,還是在其他 schema 的內(nèi)部:
const fastify = require('fastify')()
fastify.addSchema({
$id: 'greetings',
type: 'object',
properties: {
hello: { type: 'string' }
}
})
fastify.route({
method: 'POST',
url: '/',
schema: {
body: {
type: 'object',
properties: {
greeting: 'greetings#',
timestamp: { type: 'number' }
}
}
},
handler: () => {}
})
getSchemas 函數(shù)返回指定作用域中的共用 schema:
fastify.addSchema({ $id: 'one', my: 'hello' })
fastify.get('/', (request, reply) => { reply.send(fastify.getSchemas()) })
fastify.register((instance, opts, done) => {
instance.addSchema({ $id: 'two', my: 'ciao' })
instance.get('/sub', (request, reply) => { reply.send(instance.getSchemas()) })
instance.register((subinstance, opts, done) => {
subinstance.addSchema({ $id: 'three', my: 'hola' })
subinstance.get('/deep', (request, reply) => { reply.send(subinstance.getSchemas()) })
done()
})
done()
})
這個例子的輸出如下:
URL | Schemas |
---|---|
/ | one |
/sub | one, two |
/deep | one, two, three |
你可以提供一組用于 Ajv 的插件:
插件格式參見 ajv 選項(xiàng)
const fastify = require('fastify')({
ajv: {
plugins: [
require('ajv-merge-patch')
]
}
})
fastify.route({
method: 'POST',
url: '/',
schema: {
body: {
$patch: {
source: {
type: 'object',
properties: {
q: {
type: 'string'
}
}
},
with: [
{
op: 'add',
path: '/properties/q',
value: { type: 'number' }
}
]
}
}
},
handler (req, reply) {
reply.send({ ok: 1 })
}
})
fastify.route({
method: 'POST',
url: '/',
schema: {
body: {
$merge: {
source: {
type: 'object',
properties: {
q: {
type: 'string'
}
}
},
with: {
required: ['q']
}
}
}
},
handler (req, reply) {
reply.send({ ok: 1 })
}
})
schemaCompiler 返回一個用于驗(yàn)證請求主體、url 參數(shù)、header 以及查詢字符串的函數(shù)。默認(rèn)情況下,它返回一個實(shí)現(xiàn)了 ajv 驗(yàn)證接口的函數(shù)。Fastify 使用它對驗(yàn)證進(jìn)行加速。
Fastify 使用的 ajv 基本配置如下:
{
removeAdditional: true, // 移除額外屬性
useDefaults: true, // 當(dāng)屬性或項(xiàng)目缺失時,使用 schema 中預(yù)先定義好的 default 的值代替
coerceTypes: true, // 根據(jù)定義的 type 的值改變數(shù)據(jù)類型
allErrors: true, // 檢查出所有錯誤(譯注:為 false 時出現(xiàn)首個錯誤后即返回)
nullable: true // 支持 OpenAPI Specification 3.0 版本的 "nullable" 關(guān)鍵字
}
上述配置可通過 ajv.customOptions 修改。
假如你想改變或增加額外的選項(xiàng),你需要創(chuàng)建一個自定義的實(shí)例,并覆蓋已存在的實(shí)例:
const fastify = require('fastify')()
const Ajv = require('ajv')
const ajv = new Ajv({
// fastify 使用的默認(rèn)參數(shù)(如果需要)
removeAdditional: true,
useDefaults: true,
coerceTypes: true,
allErrors: true,
nullable: true,
// 任意其他參數(shù)
// ...
})
fastify.setSchemaCompiler(function (schema) {
return ajv.compile(schema)
})
// -------
// 此外,你還可以通過 setter 方法來設(shè)置 schema 編譯器:
fastify.schemaCompiler = function (schema) { return ajv.compile(schema) })
通過 schemaCompiler 函數(shù),你可以輕松地將 ajv 替換為幾乎任意的 Javascript 驗(yàn)證工具 (如 joi、yup 等)。
然而,為了更好地與 Fastify 的 request/response 相適應(yīng),schemaCompiler 返回的函數(shù)應(yīng)該返回一個包含以下屬性的對象:
因此,下面的例子和使用 ajv 是一致的:
const joi = require('joi')
// 等同于前文 ajv 基本配置的 joi 的配置
const joiOptions = {
abortEarly: false, // 返回所有錯誤 (譯注:為 true 時出現(xiàn)首個錯誤后即返回)
convert: true, // 根據(jù)定義的 type 的值改變數(shù)據(jù)類型
allowUnknown : false, // 移除額外屬性
noDefaults: false
}
const joiBodySchema = joi.object().keys({
age: joi.number().integer().required(),
sub: joi.object().keys({
name: joi.string().required()
}).required()
})
const joiSchemaCompiler = schema => data => {
// joi 的 `validate` 函數(shù)返回一個對象。當(dāng)驗(yàn)證失敗時,該對象具有 error 屬性,并永遠(yuǎn)都有一個 value 屬性,當(dāng)驗(yàn)證成功后,會存有隱式轉(zhuǎn)換后的值。
const { error, value } = joiSchema.validate(data, joiOptions)
if (error) {
return { error }
} else {
return { value }
}
}
// 更簡潔的寫法
const joiSchemaCompiler = schema => data => joiSchema.validate(data, joiOptions)
fastify.post('/the/url', {
schema: {
body: joiBodySchema
},
schemaCompiler: joiSchemaCompiler
}, handler)
const yup = require('yup')
// 等同于前文 ajv 基本配置的 yup 的配置
const yupOptions = {
strict: false,
abortEarly: false, // 返回所有錯誤(譯注:為 true 時出現(xiàn)首個錯誤后即返回)
stripUnknown: true, // 移除額外屬性
recursive: true
}
const yupBodySchema = yup.object({
age: yup.number().integer().required(),
sub: yup.object().shape({
name: yup.string().required()
}).required()
})
const yupSchemaCompiler = schema => data => {
// 當(dāng)設(shè)置 strict = false 時, yup 的 `validateSync` 函數(shù)在驗(yàn)證成功后會返回經(jīng)過轉(zhuǎn)換的值,而失敗時則會拋錯。
try {
const result = schema.validateSync(data, yupOptions)
return { value: result }
} catch (e) {
return { error: e }
}
}
fastify.post('/the/url', {
schema: {
body: yupBodySchema
},
schemaCompiler: yupSchemaCompiler
}, handler)
Fastify 的錯誤驗(yàn)證與其默認(rèn)的驗(yàn)證引擎 ajv 緊密結(jié)合,錯誤最終會經(jīng)由 schemaErrorsText 函數(shù)轉(zhuǎn)化為便于閱讀的信息。然而,也正是由于 schemaErrorsText 與 ajv 的強(qiáng)關(guān)聯(lián)性,當(dāng)你使用其他校驗(yàn)工具時,可能會出現(xiàn)奇怪或不完整的錯誤信息。
要規(guī)避以上問題,主要有兩個途徑:
Fastify 給所有的驗(yàn)證錯誤添加了兩個屬性,來幫助你自定義 errorHandler:
以下是一個自定義 errorHandler 來處理驗(yàn)證錯誤的例子:
const errorHandler = (error, request, reply) => {
const statusCode = error.statusCode
let response
const { validation, validationContext } = error
// 檢驗(yàn)是否發(fā)生了驗(yàn)證錯誤
if (validation) {
response = {
message: `A validation error occured when validating the ${validationContext}...`, // validationContext 的值可能是 'body'、'params'、'headers' 或 'query'
errors: validation // 驗(yàn)證工具返回的結(jié)果
}
} else {
response = {
message: 'An error occurred...'
}
}
// 其余代碼。例如,記錄錯誤日志。
// ...
reply.status(statusCode).send(response)
}
schemaResolver 需要與 schemaCompiler 結(jié)合起來使用,你不能在使用默認(rèn)的 schema 編譯器時使用它。當(dāng)你的路由中有包含 #ref 關(guān)鍵字的復(fù)雜 schema 時,且使用自定義校驗(yàn)器時,它能派上用場。
這是因?yàn)?,對?Fastify 而言,添加到自定義編譯器的 schema 都是未知的,但是 $ref 路徑卻需要被解析。
const fastify = require('fastify')()
const Ajv = require('ajv')
const ajv = new Ajv()
ajv.addSchema({
$id: 'urn:schema:foo',
definitions: {
foo: { type: 'string' }
},
type: 'object',
properties: {
foo: { $ref: '#/definitions/foo' }
}
})
ajv.addSchema({
$id: 'urn:schema:response',
type: 'object',
required: ['foo'],
properties: {
foo: { $ref: 'urn:schema:foo#/definitions/foo' }
}
})
ajv.addSchema({
$id: 'urn:schema:request',
type: 'object',
required: ['foo'],
properties: {
foo: { $ref: 'urn:schema:foo#/definitions/foo' }
}
})
fastify.setSchemaCompiler(schema => ajv.compile(schema))
fastify.setSchemaResolver((ref) => {
return ajv.getSchema(ref).schema
})
fastify.route({
method: 'POST',
url: '/',
schema: {
body: ajv.getSchema('urn:schema:request').schema,
response: {
'2xx': ajv.getSchema('urn:schema:response').schema
}
},
handler (req, reply) {
reply.send({ foo: 'bar' })
}
})
通常,你會通過 JSON 格式將數(shù)據(jù)發(fā)送至客戶端。鑒于此,F(xiàn)astify 提供了一個強(qiáng)大的工具——fast-json-stringify 來幫助你。當(dāng)你提供了輸出的 schema 時,它能派上用場。我們推薦你編寫一個輸出的 schema,因?yàn)檫@能讓應(yīng)用的吞吐量提升 100-400% (根據(jù) payload 的不同而有所變化),也能防止敏感信息的意外泄露。
示例:
const schema = {
response: {
200: {
type: 'object',
properties: {
value: { type: 'string' },
otherValue: { type: 'boolean' }
}
}
}
}
fastify.post('/the/url', { schema }, handler)
如你所見,響應(yīng)的 schema 是建立在狀態(tài)碼的基礎(chǔ)之上的。當(dāng)你想對多個狀態(tài)碼使用同一個 schema 時,你可以使用類似 '2xx' 的表達(dá)方法,例如:
const schema = {
response: {
'2xx': {
type: 'object',
properties: {
value: { type: 'string' },
otherValue: { type: 'boolean' }
}
},
201: {
type: 'object',
properties: {
value: { type: 'string' }
}
}
}
}
fastify.post('/the/url', { schema }, handler)
假如你需要在特定位置使用自定義的序列化工具,你可以使用 reply.serializer(...)。
當(dāng)某個請求 schema 校驗(yàn)失敗時,F(xiàn)astify 會自動返回一個包含校驗(yàn)結(jié)果的 400 響應(yīng)。舉例來說,假如你的路由有一個如下的 schema:
const schema = {
body: {
type: 'object',
properties: {
name: { type: 'string' }
},
required: ['name']
}
}
當(dāng)校驗(yàn)失敗時,路由會立即返回一個包含以下內(nèi)容的響應(yīng):
{
"statusCode": 400,
"error": "Bad Request",
"message": "body should have required property 'name'"
}
如果你想在路由內(nèi)部控制錯誤,可以設(shè)置 attachValidation 選項(xiàng)。當(dāng)出現(xiàn)驗(yàn)證錯誤時,請求的 validationError 屬性將會包含一個 Error 對象,在這對象內(nèi)部有原始的驗(yàn)證結(jié)果 validation,如下所示:
const fastify = Fastify()
fastify.post('/', { schema, attachValidation: true }, function (req, reply) {
if (req.validationError) {
// `req.validationError.validation` 包含了原始的驗(yàn)證錯誤信息
reply.code(400).send(req.validationError)
}
})
你還可以使用 setErrorHandler 方法來自定義一個校驗(yàn)錯誤響應(yīng),如下:
fastify.setErrorHandler(function (error, request, reply) {
if (error.validation) {
// error.validationContext 是 [body, params, querystring, headers] 之中的值
reply.status(422).send(new Error(`validation failed of the ${error.validationContext}`))
}
})
假如你想輕松愉快地自定義錯誤響應(yīng),可以看這里。
為了能更簡單地重用 schema,JSON Schema 提供了一些功能,來結(jié)合 Fastify 的共用 schema。
用例 | 驗(yàn)證器 | 序列化器 |
---|---|---|
共用 schema | ?? | ?? |
引用 ($ref ) $id
|
? | ?? |
引用 ($ref ) /definitions
|
?? | ?? |
引用 ($ref ) 共用 schema $id
|
? | ?? |
引用 ($ref ) 共用 schema /definitions
|
? | ?? |
// 共用 Schema 的用例
fastify.addSchema({
$id: 'sharedAddress',
type: 'object',
properties: {
city: { 'type': 'string' }
}
})
const sharedSchema = {
type: 'object',
properties: {
home: 'sharedAddress#',
work: 'sharedAddress#'
}
}
// 同一 JSON Schema 內(nèi)部對 $id 的引用 ($ref)
const refToId = {
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { 'type': 'string' }
}
}
},
properties: {
home: { $ref: '#address' },
work: { $ref: '#address' }
}
}
// 同一 JSON Schema 內(nèi)部對 /definitions 的引用 ($ref)
const refToDefinitions = {
type: 'object',
definitions: {
foo: {
$id: '#address',
type: 'object',
properties: {
city: { 'type': 'string' }
}
}
},
properties: {
home: { $ref: '#/definitions/foo' },
work: { $ref: '#/definitions/foo' }
}
}
// 對外部共用 schema 的 $id 的引用 ($ref)
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' }
}
}
// 對外部共用 schema 的 /definitions 的引用 ($ref)
fastify.addSchema({
$id: 'http://foo/common.json',
type: 'object',
definitions: {
foo: {
type: 'object',
properties: {
city: { 'type': 'string' }
}
}
}
})
const refToSharedSchemaDefinitions = {
type: 'object',
properties: {
home: { $ref: 'http://foo/common.json#/definitions/foo' },
work: { $ref: 'http://foo/common.json#/definitions/foo' }
}
}
更多建議: