输入数据处理
对于一个 web 项目,使用最多并且漏洞最多的就是数据的输入处理了,正所谓病从口入,web 项目亦是如此,如果客户端传递了恶意的数据进来你又没有做相关检查,轻则500错误,重则数据泄漏,服务器提权。 所以我们宁愿多麻烦一点做多一点数据检查,也不能有任何偷懒不检查而直接使用的想法!
所以本章内容关键点在于使用各种 Helper 助手功能,代码里面取得原始数据的例子只是为了方便参照原始数据是什么。
Query 查询参数
Query
参数指的是请求地址 ?
后的数据
import { mapping, Context, QueryHelper, $query } from 'zenweb';
export class DemoController {
// 取得原始解析对象
// demo1?age=10&name=Bob
@mapping()
demo1(ctx: Context) {
return ctx.query; // 取得已被解析的数据对象 { age: "10", name: "Bob" }
}
// 使用 QueryHelper 进行参数类型转换与校验
@mapping()
demo2(query: QueryHelper) {
return query.get({
age: 'int',
name: 'string',
}); // 解析并转换类型 { age: 10, name: "Bob" }
}
// 如果启用全局应用实例,可以使用 $query 对象进行参数类型转换与校验
// $query 对象可以在任意方法内直接调用
@mapping()
demo2() {
return $query.get({
age: 'int',
name: 'string',
}); // 解析并转换类型 { age: 10, name: "Bob" }
}
}
内置的通用分页处理
分页处理是最常见的数据处理功能,所以专门做了一个标准的处理方法。
分页处理支持如下功能:
- 条数限制检查
- 起始位置检查
- 设置默认条数
- 排序字段检查
import { mapping, QueryHelper } from 'zenweb';
export class DemoController {
// 内置的分页校验功能
// 请求 demo?limit=5&offset=10&order=-id
@mapping()
demo(query: QueryHelper) {
return query.page({
// 这里可以设置参数
}); // 校验分页参数并处理
}
}
服务端配置参数
参数名 | 默认值 | 功能描述 |
---|---|---|
input | ctx.query | 输入数据,默认取 Query 参数 |
minLimit | 1 | 最少限制条数 |
maxLimit | 100 | 最大限制条数 |
defaultLimit | 20 | 不指定条数的情况下的默认条数 |
allowOrder | 无 | 可排序的字段名称列表 |
maxOrder | 1 | 最多允许的同时排序字段数 |
defaultOrder | 无 | 不指定排序字段时默认排序字段 |
客户端输入参数
参数名 | 默认值 | 功能描述 |
---|---|---|
limit | defaultLimit | 取出条数限制(每页多少条) |
offset | 0 | 起始位置,在不指定 page 参数时有效 |
page | 1 | 第几页 |
order | defaultOrder | 指定排序字段。例如:正序 id 倒序 -id ,多个用逗号分隔 |
Params 路由参数
params
参数指的是在路由地址中所包含的变量参数,例如 /user/:id
中的 :id
参数
import { mapping, Context, ParamHelper } from 'zenweb';
export class DemoController {
// 请求地址: /user/99
@mapping({ path: '/user/:id' })
userDetail(ctx: Context, ph: ParamHelper) {
const orig = ctx.params; // 取得原始数据对象 { id: "99" }
const data = ph.get({ id: 'int' }); // 使用 ParamHelper 解析并转换类型 { id: 99 }
return {
orig,
data,
};
}
}
Body 请求主体数据
类 POST 请求往往会提交一些数据内容在 Body 中,例如 form, json, xml, 文件上传等
关于为什么采用依赖注入替代主动解析的原因:
- 解析这些内容需要一定的服务器开销,从 zenweb 3.10 开始就不再主动解析提交内容,而是由依赖注入来初始化并解析内容。
- 如遇到特殊内容格式,在之前的版本往往由于无法正确识别其格式而拒绝请求。
- 多种格式的解析配置不灵活,无法按需配置
@zenweb/body
模块支持格式有:form-urlencoded
、json
以及其他文本格式:xml
、text/*
这里的 xml 格式并不是会解析成 xml 对象,而是支持读取 xml 原始文本,如需要解析 xml 原始文本为 js 对象可以使用 @zenweb/xml-body
模块让 body
模块支持 xml 文本解析。
BodyHelper 对象
BodyHelper
最常用的数据获取方法,支持数据类型的转换与数据校验。
BodyHelper
只支持能被解析为对象的数据结构。也就是 BodyHelper
读取的是 ObjectBody
产生的结果,例如 json、form表单。
import { mapping, BodyHelper } from 'zenweb';
export class DemoController {
@mapping({ method: 'POST' })
demo(bh: BodyHelper) {
return bh.get({
age: '!int', // 必须为整数
name: {
type: '!trim', // 必须有字符串,去除两端空格
minLength: 2, // 最小长度 2 个字
maxLength: 10, // 最大长度 10 个字
},
});
}
}
ObjectBody 对象
可被解析为对象的数据,直接返回已解析数据对象。
import { mapping, ObjectBody } from 'zenweb';
export class DemoController {
@mapping({ method: 'POST' })
// 例如form提交的数据: age=10&name=Bob
demo(body: ObjectBody) {
return body; // 返回格式: {age: "10", name: "Bob"}
}
}
Body 对象
Body
对象通常用来取得更详细的数据类型,以及数据是经过何种解析器处理过,如果数据不能被配置的解析器所处理则会尝试是否为已配置的文本类型。否则不会返回任何类型。
import { mapping, Body } from 'zenweb';
export class DemoController {
@mapping({ method: 'POST' })
demo(body: Body) {
console.log(body.parser); // 匹配的解析器对象
console.log(body.type); // 匹配的数据类型
console.log(body.data); // 如果有数据 body.data 已经处理完成的结果
return '查看命令行输出';
}
}
取得原始数据 RawBody
RawBody
对象支持任意数据类型,这个对象并不常用,如果原始数据使用了标准的 http 压缩协议会被自动解压缩,但是不会经过任何编码转换或任何数据解析处理。
import { mapping, RawBody } from 'zenweb';
export class DemoController {
@mapping({ method: 'POST' })
demo(raw: RawBody) {
// 如果有数据 raw.data 返回 Buffer 对象
console.log(raw.data);
return '查看命令行输出';
}
}
使用 Helper 参数校验转换助手
对于数据的校验与转换,在传统方案中总是处理,十分繁琐,例如下面的代码
import { Context, mapping, QueryHelper } from "zenweb";
export class IndexController {
// 传统方案
@mapping()
get(ctx: Context) {
// 取得 id 参数
let id = ctx.query.id;
// 判断有没有
if (!id) {
fail('id-error');
}
// 尝试转换类型
id = parseInt(id);
// 判断是否转换成功
if (id === NaN) {
fail('id-type-error');
}
return { id };
}
// 使用 helper
@mapping()
get2(query: QueryHelper) {
const { id } = query.get({
id: '!int', // { 参数名: 类型 } 前面的 ! 感叹号代表必填项不能为空
});
return { id };
}
}
此外,helper 助手还支持列表格式、值规则校验。
如果一个输入格式为 ids=1,2,3,4,5
,我们可以使用 { ids: '!int[]' }
helper.get({
ids: '!int[]',
}); // { ids: [1, 2, 3, 4, 5] }
如果希望值在某一个范围内,例如年龄 >16
<30
,可以写成:
helper.get({
age: {
type: 'int',
validate: {
gt: 16,
lt: 30,
},
}
});
参数说明
参数名 | 说明 | 默认值 |
---|---|---|
type | 目标类型,详见:支持的转换类型 | |
default | 默认值 | |
splitter | 是否切割为数组,定义切割字符 | ',' |
minItems | 切割数组后最少需要的元素数量 | |
maxItems | 切割数组后最多不能超过的元素数量 | |
required | 是否为必须 | false |
notNull | 是否允许 null 值 | false |
validate | 数据验证规则,详见:支持的数据验证 |
支持的转换类型
类型名 | 目标类型 | 功能描述 |
---|---|---|
number | number | 任何数值类型,整数、浮点 |
int | number | 转换为整数类型 |
float | number | 转换为浮点数类型 |
bool | boolean | 转换为布尔类型,只有值为 y, 1, yes, on, true (不区分类型)才会被判定为 true,其他都为 false |
trim | string | 转换为字符串并去除两端的空字符 |
string | string | 转换为字符串 |
any | any | 不进行类型转换,直接输出原始类型 |
date | Date | 转换为日期类型 |
支持的数据验证
验证名 | 功能描述 | 参数 |
---|---|---|
lt | 小于 < | any |
lte | 小于等于 <= | any |
gt | 大于 > | any |
gte | 大于等于 >= | any |
eq | 等于 == | any |
neq | 不等于 != | any |
maxLength | 最大长度,调用值的 .length 属性 | number |
minLength | 最小长度,调用值的 .length 属性 | number |
in | 只能出现的值 | any[] |
notIn | 不能出现的值 | any[] |
regexp | 正则表达式匹配 | string \| RegExp |
是否满足Email地址规则 | boolean | |
slug | 是否满足URL路径规则 | boolean |
url | 是否满足 URL 规则 | string[] \| boolean |
此助手的核心代码已经作为独立项目剥离出,详情可以查看 typecasts