防御性编码的意识与实践

2023年 9月 28日 17.9k 0

汽车防御性驾驶是一种安全驾驶的理念和实践,旨在最大程度地减少交通事故的发生,并保护驾驶员、乘客和其他道路使用者的安全。防御性驾驶核心在于合理怀疑其他交通参与者可能会做出危及安全的行为并提前做出预判,比如碰到路口预判前面可能冲出来小孩。碰到前面缓慢,预料到自己急刹可能导致跟得很近的后车追尾上来。

在编程领域也有防御性编码(Defensive coding),有着与汽车防御性驾驶相似的理念,它也是保护我们的系统的一种实践,减少线上事故的发生。

一个大型系统必然是多个团队的协作成果,团队之间的接口调用通常靠文档规范约定,这些接口文档也便形成了一份份规约。可能有人会说对方团队不遵守文档规范是对方的问题,不关我们团队的事情啊。对于这个问题,我只想说只关心自己不被追责而不关心技术全局是否完善的技术人员注定发展空间是有限的。因为各类规范只能保证下限,在团队与团队的协作中仅仅按契约完成规范还不够,就像仅按交规开车并不能防止事故一样,我们还需要防御性编码的意识与实践。那怎样做到防御性编码呢?下面是最常见的一些手段:

1、日志记录

作为本系统与外部系统接口交互的记录,必须要记录日志。日志无论在事中查错、事后追责提供证据方面都有着不可替代的作用。如果自己的系统没有日志,就只能寄希望于上游或下游记录了日志,这有时不仅受制于人,还可能会为日后互相甩锅埋下隐患。对于重要的(如涉及资金),或者频繁需要查日志的系统间交互,不仅需要打印应用日志,还需要考虑将这些日志记录到数据库或NoSQL中并提供方便的查询界面以提高查询的效率。

2、协议之外

比如某第三方支付系统的接口规定,调用支付接口后返回报文的支付状态字段,1代表支付成功,0代表支付失败,-1代表不确定。结果线上因为对方升级协议的原因,多了一个状态2,而这个业务的程序员又没足够的经验预判这种情况,引起了大面积的支付错误投诉。他的代码是这样写的:

if(status == -1){
    //支付结果不确定处理
}else if(status == 1){
    //支付结果成功处理
}else{
    //支付结果失败处理
}

可以看到他把新出现的这个状态当成支付失败处理了。对于这种出现了协议之外的值的情况,在业务开始可以拒绝的情况下直接拒绝该请求调用。在不能拒绝的情况下(如本例中用户已经输入支付密码完成支付,不能再拒绝请求调用了)则需要在业务上兼容处理,比如这里可以把这种情况视为支付结果不确定,最后再根据对账取得最终的支付结果是成功还是失败。

3、输入限制

常见的输入类型有整数、小数、字符串等,我们必须检查输入字段是否符合接口文档规定的类型,并限制字段的基本长度。同时也从业务角度去检查字符串是否满足格式,如常见的身份证、邮箱、手机号码等是否满足各自规定的业务格式。

4、输入过滤

比如你提供了一个搜索接口,但你不确定上游会输入一句话还是一篇文章,光靠双方自觉地按接口文档去约束显得既苍白又无力,一刀切停止服务似乎又过于简单粗暴。一个可行的方法是无论上游输入多少,只截取前30个字符去ElasticSearch中查询(接口协议文档中明确约定)。有时候为了自己系统的安全,对于明显超出正常业务范围的字符也会进行过滤,如常见的&rn等。

5、边界判断

一个常见的问题是只检查了下限没检查上限,比如商品数量数据,通常会做大于0检查,但却很少会去检查上限。比如在Java中当我们使用内置运算符做数学运算时,如果结果超出变量类型表达范围,结果会并不会抛出异常,而计算结果又是不符合预期的。如两个大的正整数相乘,结果可能溢出后变成了负的。这就要求我们对字段的数值范围,业务取值范围进行进行边界判断。

6、接口限流

我们知道,系统之间交互模式有推送与拉取。拉取虽在及时性上不如推送,但它可以根据系统自身的能力量力而为地处理业务,能较好的保护自身。而推送则有可能出现能力不匹配的情况,如果上游推送很快,自己的系统又处理不过来则需要提前做好限流,并确保上游能兼容限流的情况。

7、重复请求处理

如果指望通过一纸规定就能避免调用方重复发起,就能避免被调用方重复处理问题,那就未免显得有点天真了。因为重复可能发生在应用层,也可能发生在框架或其它层,未必就是调用方主观的行为。对重复请求的处理,有些情况可以用幂等处理,除了部分业务本身就是幂等的,其他多数是通过业务惟一索引进行限制实现。需要指出的是有些系统是通过先查询数据库或缓存再判断是否是重复请求的还需要注意原子操作问题。

8、金额单位

要特别小心涉及金额的接口处理。对金额的处理,不同的系统有不同的标准,同样对于1.23元,某互联网公司的支付系统规定以分为单位的整数(如123);某电信系统则规定以厘为单位的整数(如1230);某金融系统又规定是以元为单位的字符串(如1.23)。尤其需要小心对最后一种情况的数值范围判断,涉及到字符串转数字,要小心精度丢失。在Java中涉及小数的计算,多数情况使用BigDecimal。

防御性编码要求我们跳出责任归属的视角系统性地看待全局问题,对可能发生问题的地方提前预防,对事故高发情况提前预判,保护自己系统的同时也保护上下游系统,取得全局最优解而不仅仅是局部最优解。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论