Memphis' Talk

防御性编程之道

一份好代码并不取仅仅决于脑子是否灵活, 工作是否努力. 还有经验和对待自己工作的态度, 重要的事情强调三遍, 态度, 态度, 态度. 码如其人, 从字里行间可是很能够考察人的品性的, 今天在湾区日报上看到了这篇文章, 忙中抽空, 在半夜完成了翻译, 分享出来

原文地址: The Art of Defensive Programming (科学上网)

引言

为什么有些程序员写不出安全的代码? 我们在这里不是要再一次讨论代码的 “整洁之道”, 而是从实战角度出发, 更进一步地讨论代码的安全性和健壮性. 因为不安全的软件可以说是相当地废柴. 我们先看看什么是我所说的不安全的软件.

  • 由于箭载导航的软件 Bug, 欧洲空间局价值十亿美刀阿丽亚娜5型火箭 在 501 号任务起飞 40 秒后发生了解体 (June 4, 1996).

  • 20 世纪 80 年代, Therac-25 放射性治疗仪 控制代码中的一个 bug 释放出了超量的 X-射线, 直接导致了至少 5 名患者的死亡.

  • MIM-104 爱国者导弹 的一个软件错误, 使得系统时钟在工作了 100 个小时之后, 产生了 1/3 秒的时间误差, 导致无法成功定位并拦截飞弹. 伊拉克的导弹打击了美军在沙特阿拉伯达兰的一处军营, 造成了 28 名美军士兵丧生. (February 25, 1991).

这些教训应该足够让我们明白写出安全的代码有多么重要, 尤其是在一些特定的领域. 同时也让我们意识到我们的代码将会带给我们怎么样的惨剧.

防御性编程初窥

为什么我觉得防御性编程是一个解决这些问题的好办法?

抵御不可能的情况, 因为不可能的情况总是会发生

关于防御性编程有很多定义, 相对于关注的不同安全级别和软件项目中用到的资源而有所不同.

防御性编程防御性设计 的一种表现形式, 旨在确保 软件 在未预见到的环境下的持续工作能力. 防御性编程常常被用来在对可用性, 安全性, 健壮性有高要求的环境中使用 – 维基百科

我个人认为这种编程方式在处理规模大, 周期长, 参与人数众多的项目时可以适用. 比如一个需要大范围广泛维护的开源项目.

不妨一起过一遍我总结的几个关键点, 来向防御性编程之道出发.

永远不要信任用户的输入

总是假设你将接收到一些你没有预期到的东西, 这将使你走向防御性编程程序员的康庄大道上. 对抗你的用户输入内容, 或其他广义上输入进你的系统中的信息. 因为我们已经说过了, 我们需要预料到出乎意料的东西. 尝试变得严苛一些, 根据你的预期对你的输入信息做 断言检查)

最好的防守就是进攻

使用白名单替代黑名单, 例如在检查图片文件后缀名时, 不要关注非法的类型, 而专注于合法类型, 然后把剩余的都干掉. 如果你用 PHP 的话, 你还有茫茫多的开源验证库来让你的工作更加轻松.

最好的防守就是进攻 请变得更严格一点

使用数据抽象

OWASP 十大最容易受攻击的安全弱点 榜首就是注入攻击. 这意味着一些人(也许是茫茫多的人)并没有使用一些安全工具来对数据库发起请求. 请使用数据抽象模块或者数据抽象库. 在 PHP 中你可以使用 PDO实现基本的注入保护.

不要重复造轮子

你从不使用框架(哪怕是个微型框架)? 你喜欢毫无理由地自己做一些额外的工作? 好吧, 随你的便, 先提前祝贺一下你浪费的这么多时间. 我要说的并不只是事关那些框架, 还包括了一些新特性, 你可以轻松地 使用那些早就存在, 经过良好测试, 稳定并且被成千上万开发者信任的东西, 而不是只是因为觉得比较屌就自己去手工建造. 自己造轮子的唯一理由应该是因为你的需求无法被现有的轮子所满足(糟糕的性能, 或者缺失关键特性等等).

这是人们所说的聪明的代码复用. 请尽情地拥抱它.

不要信任开发者

说到防御性编程有时会提及防御性驾驶的这个概念, 在驾驶中, 我们假设我们周围的其他人都会潜在地犯错. 所以我们对于他人的行为也许要格外小心. 同样的概念应用到编程上就是我们作为开发者, 不能信任其他开发者的代码, 甚至连自己的代码都不能信.

在大项目中, 牵涉到许多开发人员. 就会有许多不同的代码写法和组织方式. 常常让人觉得困惑, 甚至于导致 bug 的产生. 这是我们为什么要强制推行代码风格规范和 mess detector 的原因, 它能让我的生活更加简便.

扎实的代码

这对(防御性编程的)程序员来说是一块硬骨头, 写不操蛋的代码这件事, 许多人心里清楚且挂在嘴边, 却没有正真在意, 或没有投入足够的努力来使自己的代码更加扎实.

举些反例

不要: 使用未初始化的对象属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class BankAccount
{
protected $currency = null;
public function setCurrency($currency) { ... }
public function payTo(Account $to, $amount)
{
// sorry for this silly example
$this->transaction->process($to, $amount, $this->currency);
}
}
// I forgot to call $bankAccount->setCurrency('GBP');
$bankAccount->payTo($joe, 100);

在这个情况下, 我们不得不记得在使用 payment 前先调用 setCurrency. 这真是件糟糕的事情, 像这样引起状态改变的操作 (如付款), 不应该使用到两个公开方法, 分两个步骤来完成. 我们可以有很多不同的方式完成支付, 但是我们一定只能有一个简单公开的方法来完成状态的改变. (对象永远不准处于一种不一致的状态当中).

对于这个例子, 我们可以做到更好: 封装未初始化的属性到 Money 对象中

1
2
3
4
5
6
7
8
<?php
class BankAccount
{
public function payTo(Account $to, Money $money) { ... }
}
$bankAccount->payTo($joe, new Money(100, new Currency('GBP')));

为保万无一失, 不要使用未初始化的对象属性

不要: 在类空间之外泄漏状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
class Message
{
protected $content;
public function setContent($content)
{
$this->content = $content;
}
}
class Mailer
{
protected $message;
public function __construct(Message $message)
{
$this->message = $message;
}
public function sendMessage(
{
var_dump($this->message);
}
}
$message = new Message();
$message->setContent("bob message");
$joeMailer = new Mailer($message);
$message->setContent("joe message");
$bobMailer = new Mailer($message);
$joeMailer->sendMessage();
$bobMailer->sendMessage();

这种情况下 Message 被引用传递, 导致的结果是两个邮件内的信息都是 “joe message”. 解决办法之一可以在邮差类的构造函数中克隆消息对象, 但是我们最应该总是遵守的方式应该是使用(不可变的) 值对象而不是 Message 这样的可变对象, 能用不可变对象就尽量用不可变对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
class Message
{
protected $content;
public function __construct($content)
{
$this->content = $content;
}
}
class Mailer
{
protected $message;
public function __construct(Message $message)
{
$this->message = $message;
}
public function sendMessage()
{
var_dump($this->message);
}
}
$joeMailer = new Mailer(new Message("bob message"));
$bobMailer = new Mailer(new Message("joe message"));
$joeMailer->sendMessage();
$bobMailer->sendMessage();

写测试

这还用强调吗? 写单元测试能帮助你遵守一些通用的原则如 “高内聚, 低耦合”, “单一职责原则”, “良好的对象组成”. 不仅能帮助你测试小的工作单元, 也能测试你构建你的对象的方式. 事实上, 当你测试你的小功能时, 你会清楚地看到你需要多少测试用例, 模拟多少对象才能够 100% 地覆盖你的代码.

总结

希望你能喜欢这篇文章, 记住这些只是建议, 具体在什么时候, 在哪里, 是否要使用防御性编程, 还是要取决于你自己.

谢谢阅读, 祝你有个愉快的一天!