>백엔드 개발 >PHP 튜토리얼 >构建安全的PHP应用

构建安全的PHP应用

WBOY
WBOY원래의
2016-06-20 12:39:161935검색

译者序

2015年7月的一天,我正在Feedly上悠然的阅读,突然出现一篇博客有介绍这本书,因为说的是我当时正在关注的PHP安全,所以认真看了那篇博客,顺着博客里的链接找到了这本书的官网,看起来很靠谱,而恰好我也需要,网站上有购买链接,一直到leanpub上我都在寻找是否有中文版可以购买,但没有找到,而后在Twitter上联系了作者Ben,问他关于中文版的事,遗憾的是还没人翻译过,当时就想这本书也不长,为什么我不来翻译一下,为中国的开发者做点贡献呢,当然也会收获小小的成就感,随后我们用邮件沟通了翻译的详细事项,只用了来回两封邮件,不过几百个单词,交流真简单明了,我爱上了这种高效简单的沟通方式!当然以后如果还有这样的机会,我想我还会继续行好事,做想做该做的,不问前程!如果有翻译不好的地方,可以随时邮件我,我会非常积极的修改,这本书在相当长的一段时间都会以电子版的形式存在,所以当我修改了你提出的问题后,一定会最快的同步线上,这样购买的所有人都会收到更新了,一起行好事吧!

写在前面

几年前我用PHP的CodeIgniter框架写了一个网页程序,但是这个框架并没有内置任何类型的身份验证系统。当然,这并不会难倒像我这样的一个好(懒惰)的开发者,我到处寻找一个靠谱的库来让我的应用拥有健壮的身份验证能力。然而令人失望的是我发现在CodeIgniter上并没有一个简洁、可靠并能满足身份验证需求的库。这让我走向了开发Ion Auth(可以从Github找到)之路,它是为CodeIgniter开发的一款轻量级的身份验证库,并在为网页应用的安全上做了一个长时间的改革迭代,同时也帮助其他开发人员这样做。

多年之后,我们都已经换了很多的框架和语言,但是我仍然对被忽视的基础安全方面保持持续关注。让我们一起改变这个现状吧。我希望能够帮助大家再也不用生活在密码泄露的恐惧中,再不会为恶心的SQL注入而担心,能够轻松的避免那些“黑客”的临幸。让我们都能确保可以每天按时下班回家,并能高枕无忧的做闭目佳人!

这本书将会是一本可以在具体项目中进行参考的快速阅读手册。意思是你可以在数个小时内快速看完并在你需要的时候随时查阅。我也会尽量使阅读本书变得更加有趣。

格式说明

若无特殊说明,在本书中缩进内的示例代码均为PHP。

以$符号开头的行

$ ls -al 

是普通用户下在命令行下的命令示例

以#符号开头的行

# ls -al 

是root用户命令行下的命令示例

服务器命令行示例会呈现*nix(centos, redhat, ubuntu, osx, etc)操作系统风格的样式

我会努力让示例代码有合适的换行,这样方法参数会在不同的行。这本书的代码风格虽然看起来比较奇怪但是比那些封装的代码更容易阅读。

勘误表

如果您发现任何问题都可以毫不犹豫的联系我的邮箱。

如果您有翻译问题也可以联系译者邮箱

示例代码

若无特殊说明,例子中的代码均为PHP。我会尽可能使用原生的PHP,除非它造成了太多的冗余。当使用原生PHP来阐述问题太啰嗦时,我也会使用Laravel框架更优雅的风格让大家更容易理解。

一些代码过时或者不清楚,你可以在Github repository找到更全的代码。

来吧!

关于作者

Ben Edmunds 带领开发团队做最前沿的网页和手机应用。他是一个活跃积极的领导者、开发者,也是在多个开发社区的演讲者。他已经在软件研发领域专业研究十余载并做过从机器人技术到政府项目的各种工作。

PHP Town Hall 播客的联合主持。波特兰PHP Usergroup联合组织者。开源项目倡导者。

关于译者

张庆龙 技术上致力于web开发,主要使用PHP和Ruby on Rails,热衷使用Mac进行开发和日常工作。热爱生活,喜钓鱼,爱表演,好搞小幽默。主要活动在京津冀地区。

第一章 - 不相信任何用户,格式化所有的输入!

我们从一个故事说起,麦克是俄克拉何马州一个私立学校的系统管理员。他的主要工作是保持学校的网络畅通和服务器稳定。最近他开始为学校开发一个可以自动完成各种任务的网页应用以供内部使用。他没有参加过正规的培训而且是刚从一年前开始编程,但是他对自己的工作自我感觉良好。他了解一些PHP基础而且已经为学校开发了一个有够稳定的客户关系管理系统。还有大量的功能等着添加,但是基本功能已经完备。麦克甚至由于精简业务和节省学校开支获得了来自院长的称赞。

一切都很好,但是有一天一个特别的学生带来了噩梦。这个学生的名字是Little Bobby Tables。这天,大乔在管理员办公室打电话给麦克问为什么系统崩溃了。经过排查,麦克发现那个存有所有学生信息的数据库表完全消失了。好家伙,Little Bobby的全名竟然是Robert'); DROP TABLE students;--,太尴尬了。这时候数据库还没有备份,这是麦克即将要做的一件事,但是还没开始。这回麦克摊上大事儿了。

SQL 注入

真实的故事

虽然在真实的世界里名字中包括危险SQL语句的可能非常小,但是像这种SQL注入漏洞却每天都在发生:

  • 2012年,LinkedIn由于一个隐藏的SQL注入漏洞泄露了600万用户数据

  • 2012年,雅虎!泄露了450,000的用户密码

  • 2012年,Nvidia的400,000个密码被盗

  • 2012年,Adobe的150,000个密码被盗

  • 2013年,eHarmony泄露了大概150万的用户密码

SQL注入的原理

如果你可以不加修饰的接受用户直接输入的内容,那么一个歹毒的用户就可以传入一个奇葩的数据,直接改变你的SQL语句。
假设你的代码像下面这样:

mysql_query('UPDATE users  SET first_name="' . $_POST['first_name'] . '"  WHERE id=1001'); 

你可能希望生成的SQL语句为:

UPDATE users set first_name="Liz" WHERE id=1001; 

但是如果有个歹毒的用户把他的名字写成:

Liz", last_name="Lemon"; -- 

那生成的SQL语句就变成了下面这样:

UPDATE usersSET first_name="Liz", last_name="Lemon"; --"WHERE id=1001; 

现在你所有的用户的名字都被改成了Liz Lemon, 这岂不是很悲催。

如何防止上面的事发生

一个有效预防SQL注入的方法是格式化输入(也被称为转义)。你可以为每个特别的输入转义。或者更好的方法被称作参数约束,这是我强烈推荐的方法,它拥有更高的安全性。使用PHP的PDO类,你的代码现在变成下面这样:

$db = new PDO(...);$query = $db->prepare('UPDATE users  SET first_name = :first_name  WHERE id = :id');$query->execute([  ':id'         => 1001,  ':first_name' => $_POST['first_name']]); 

使用约束的参数就意味着每个值都能被恰当的引用,转义,且只匹配一个值。需要牢记的是,参数约束虽然可以保护你的SQL语句,但是却不会在插入数据库之后保护输入的数据。记住,任何数据都有可能是具有破坏性的。你仍然需要剔除and/or这些容易被遗忘且稍后会展示在用户页面上的内容。你可以在存入数据库的时候就进行处理,或者在展示的时候处理,但是不要省略这重要的一部。我们会在接下来的章节详细探讨。

现在你的代码稍微多了一点,但是更安全了。你不用担心再被另一个Little Bobby Tables弄糟你的日子。参数约束已经很帅了是吧,知道还有什么很帅么?是我!哈哈。

最佳做法和其他解决方案

存储过程是另一个防范SQL注入的方法,它是在数据库上编译的。用上存储过程以后就意味着你不太可能被SQL注入,因为你的数据一开始就没有通过SQL语句的形式传输。一般来说,存储过程是让人苦恼的,有以下几个主要原因:

  • 难以测试

  • 把业务逻辑放到了应用外(即数据库层)

  • 不易版本控制,因为他没在你的代码而是在数据库

  • 在需要修改这部分逻辑的时候直接限制了能胜任之人的数量(为什么存储过程在我们日常开发中不多见?)

  • 客户端 Javascript并不是验证数据的好办法。它很容易被篡改或者被一个只具备初级互联网知识水平的歹毒用户回避。跟我重复:我永远不会只依靠Javascript验证;我永远不会只依靠Javascript验证。你当然可以使用Javascript来提供实时交互并做更好的用户体验,但是为了表达对神的真爱,还是需要在后端增加处理逻辑来保证一切都是合法的。

    Mass Assignment(批量赋值)

    Mass assignment是一个非常能提高开发速度的实用工具,但若使用不当也会带来严重问题。

    假如你有一个User的模型,你需要对它进行一些更改。可以依次更新每个字段,或者可以把所有需要修改的值一次性通过表单传过去。

    比如下面是你的表单:

    <form action="...">        <input name="first_name" />        <input name="last_name" />        <input name="email" /></form> 

    然后使用后端的PHP代码处理提交的表单。如果使用Laravel框架,代码是下面这样:

    $user = User::find(1);$user->update(Input::all()); 

    真是又快又好是吧?但是如果有个歹毒的用户修改了表单,给了她自己管理员权限呢?

    <form action="...">  <input type="text" name="first_name" />  <input type="text" name="last_name" />  <input type="text" name="email" />  <input type="hidden" name="permissions" value="{'admin':'true'}" /></form> 

    我们的后端代码就会错误的改变了用户的权限。

    听起来又有一个愚蠢的问题需要解决了,但这却是目前大多数开发者和网站可能深受其害的。最近,大家应该都听说了一个开发者扬言Ruby on Rails很容易被这个的漏洞利用。Egor Homakov最初向Rails团队提交问题说Rails在刚被初始化之后是不够安全的,他这个bug很快就被关掉了。核心团队认为要避免这个问题的方法对新手来说都是常识(attr_accessible),这个问题属于开发者而不是Rails应该做的(大家一般都会这样想)。Homakov觉得好心被当成了驴的某些器官,非常生气,就黑了Github上的Rails账号(Github是用Ra...

    你是如何应对类似的攻击呢?具体实现方法还要看你使用的框架和语言,但是你有几个通用的选择:

    • 彻底关掉mass assignment

    • 给可以安全被批量赋值的字段加白名单

    • 给那些危险的加黑名单

    根据你的实现方法,这些也可以被同时使用。

    在Laravel框架你可以在models中添加一个$fillable变量做可被批量赋值的白名单字段:

    <?phpclass User extends Eloquent {      protected $table = 'users';      protected $fillable = ['first_name', 'last_name', 'email']; 

    这样就会在批量赋值的时候把"permissions"字段过滤。另一种方法就是增加一个$guarded变量做黑名单:

    <?phpclass User extends Eloquent {      protected $table = 'users';  protected $guarded = ['permissions']; 

    哪种对你的程序方便就用哪种。

    如果你不是用Laravel,你的框架可能已经集成了类似黑/白名单的方法。如果你使用的定制框架,干嘛不自己实现他呢!

    类型转换

    我一般还喜欢做另外的一步,不光为安全还考虑到数据完整性,那就是对已知格式进行类型转换。由于PHP是一门动态类型的语言, 一个值有可能是很多的类型:字符型,整形,浮点型等。通过类型转换,我们可以确认数据都会匹配我们的所需。上面的例子中,如果我们知道变量ID一定会是整形的数字,那么它被类型转换将变得很有意义:

    <?php    $id = (int) 1001;    $db    = new PDO(...);    $query = $db->prepare('UPDATE users  SET first_name = :first_name  WHERE id = :id');    $query->execute([  ':id'         => $id, //我们知道它是个整形                         ':first_name' => $_POST['first_name']]); 

    上面这样并没什么意义,因为ID是我们自己定义的,我们本来就知道他是整形,但是如果ID是在别的表单传递过来的,它就有了让我们心如止水的意义。

    PHP提供了一系列可以转换的类型:

    <?php    $var = (array) $var;    $var = (binary) $var;    $var = (bool) $var;    $var = (boolean) $var;    $var = (double) $var;    $var = (float) $var;    $var = (int) $var;    $var = (integer) $var;    $var = (object) $var;    $var = (real) $var;    $var = (string) $var; 

    这不仅在处理数据库方面很有帮助,而且在你整个应用程序上都有用。因为即使PHP是动态类型但并不意味着你不能在某些特别的地方尝试强制类型。科学开发!

    净化输出

    输出到浏览器

    你不仅要关注数据的插入,还应该净化/转义任何由用户生成最终会被输出到浏览器的内容。

    你可以在存入数据库之前修改或转义数据内容,或者在需要展示到浏览器的时候做这件事。这通常要看你的数据是如何编辑以及它的用处。比如,如果用户可能再次编辑,原样存储,净化输出将更有意义。

    什么时候转义会输出的用户内容有大的安全意义呢?假如用户提交了下面这样的Javascript片段到你的应用,等下还可能被显示到浏览器:

    <script>alert('I am not sanitized!');</script> 

    如果在输出的时候你什么都不做,这个恶毒的Javascript通常都会像你自己写的一样被执行。这只是一个无所谓的alert(), 但是黑客一般都不是这么友善。任何可能被显示且从外面插入到你应用的数据,你都要对其处理。

    还有一个这方面的应用是图片的XIFF数据。如果你的应用会展示一个用户上传的图片的XIFF数据,这也应该被格式化。

    如果你正使用模板引擎或者你使用的框架提供了模板机制,它可能已经自动完成了转义,或者它会提供一个相关的方法给你。详细的使用和实现你可以查看相关的文档。

    这完全取决于你,PHP内置了很多函数,当你要在浏览器显示数据时都会成为你最好的朋友: htmlentities()和htmlspecialchars(). 他们都可以转义使得在展示之前就变得更加安全。

    htmlspecialchars(); 可以完成90%你想要的结果。他会自动转义 (如, &) 这些特殊的字符至HTML实体。

    htmlentities()和htmlspecialchars()性质相差无几. 如果可能他会转义所有的字符到HTML实体。这在很多时候就不太实用。确认这些方法使用之后的结果,之后再评估哪种是你想要的。

    命令行的响应

    别忘了对命令行脚本的运行结果进行格式化。相关的函数有escapeshellcmd()和escapeshellarg()

    顾名思义。使用escapeshellcmd()转义所有命令。可以防止所有命令被执行。escapeshellarg()用来搞封装的参数来确保他们被正确的转义,确保不会用你的应用程序来处理命令一样处理参数。

    第二章 - HTTPS/SSL/BCA/JWH/SHA 等其他的随机序列及他们的实际问题。

    又到了小故事时间。2010年10月Eric Butler发布了一个叫做Firesheep的火狐浏览器扩展,是为了展示一个在网页上人们还没有注意到的大问题。Firesheep 可以让一个普通的线上用户在本地网络轻易的看到未加密的通信信息,进而劫持到其他用户的会话。这个扩展功能允许人们以Cookie的方式查看在公共网络上交换的信息,在中间攻击,劫持。很害怕吧,这确实是真的。可能你在想,这只是猜想吧。那么实际点。让我们一起来用实例证明它。

    在2010年12月,Jane因为工作出差住在希尔顿酒店,这时候John也在。他们都在为了人生目标而奋斗。Jane之前再新闻上听说了Firesheep觉得好玩。淘气的他连上酒店的wifi并打开了Firesheep。恰好,John正在使用wifi,Jane看到John正在使用一个不安全的连接访问公司的网页邮箱。一次点击John的邮箱账号就被拿到了。想想这可以造成多大的麻烦,私密的邮箱账户,邮箱的常用功能都打开了大门。

    像这种利用未加密的网络通信的会话劫持(又名sidejacking),是一直有可能被知道其他人在做什么的。随着Firesheep的发布大家都知道了下个插件点个鼠标就能干很危险的事。

    你下载了Firesheep,(我知道你这个坏小子在干嘛)你可能在想可怕地事即将发生。实际上大相径庭,这些互联网公司早已动身完成了这次保护并且严肃的使用上了HTTPS。Gmail, Facebook, 以及 Twitter均已整站采用HTTPS。之前只是用在了登录页面的情况通过上面所说的那个例子已经证明可以暴露用户会话了。

    什么是 HTTPS

    普通的网页流量基于HTTP协议传输,当你在浏览器地址栏敲下 "http://www.google.com" 的时候你就在使用HTTP,协议不是摆在最前面么。HTTP协议通过80端口传输,HTTPS是443端口。HTTP一点也不安全,他可以把任何消息清澈透明的发送给正在窃听的任何人。HTTP的意思是"HTTP Secure" 或者 "HTTP on SSL”,到底是哪个还有争议,但是他们都在说明一件事。HTTP使用SSL进行安全传输。

    我们这里只讲HTTPS的宏观工作原理,因为细节对大多数人来说并不用关心。如果你想了解更多,谷歌是个好地方,当然如果你访问不了用百度也行,呵呵。

    能说明SSL工作原理的真实故事是外交邮袋。它的内容是安全的,因为它只有在最终被传递到手握有效证书的人那里才能被打开。这个袋子被国际法律保护着,同理,SSL的加密信息被健壮的代码和密钥保护。

    证书权威局会注册你网站的证书并证实有效。用户浏览器能够识别主要认证机构并且会拿网站的证书和证书权威局提供的根证书校验。通信在双方都会用这个密钥加密,所以所有的通信都被加密。如果你曾使用过通过公钥无密码SSH登录你应该比较熟悉这个过程了。你有一个公钥和一个私钥用来使远程机器完成身份验证。

    如果你整站采用了HTTPS这就有效预防了中间攻击,还包括我们上面提到的会话劫持。

    局限性

    使用HTTPS也会在有些情况有局限。

    虚拟主机

    普通的虚拟主机配置不能使用在SSL上。如果你使用托管虚拟主机或者在一个服务器跑了多个站点都会造成问题。因为服务器只有在连接建立之后才能确定带有SSL身份验证的请求头。因为证书只能有一个host,这就是说他不是那么简单生效。最简单的办法就是使用配置多IP地址并使用IP绑定hosts来代替你可能正在使用的域名绑定host。我们通常推荐为安全站点配置单独的服务器,如果你想使用HTTPS那么你可能还需要一台专用服务器。

    当然还有一些主机服务商把一个共享证书用在他们服务的一系列网站中。这能让你更快速便宜的用上HTTPS。问题是你域名必须使用人家提供给你的,例如

    https://yourApp.com/login 

    可能会变成下面这样

    https://yourHost.com/yourApp/login 

    你是不是关注这个问题取决于你的应用和品牌需要。

    速度

    HTTPS连接包含SSL握手来建立连接,因此使速度变得慢了些。一次初始握手过程附加的执行连接包含了内容的加密和解密,意味着一旦初始连接完成,随后的连接就不会太慢了。性能影响非常不值得去思考,这并不是放弃HTTPS的正当理由。

    缓存

    切达干酪。脂肪堆积。已故的总统。现金。算了,其实我们在聊缓存。可以加快加载时间的秘密武器。你必须用英国口音说说。现代浏览器会像HTTP一样缓存HTTPS内容。为了让老浏览器支持只需要使用Cache-Control 首部, 比如

    header('Cache-Control: max-age=31536000'); 

    就会告诉浏览器我要缓存一年!

    真正的问题在代理缓存。代理缓存可能来源于网络提供商或者一个打算提高连接速度的服务。这主要用在网络速度比较慢的农村。使用HTTPS,这种缓存是不可能的因为在代理看起来所有的通信都是加密的。对大多数站点来说这不是一个主要问题除非你有一个相当广阔的用户群,或者一个把目标用户定个偏远地区的应用,是应该深思熟虑的。

    还有一件事,好运的是网站的大多数地方并无需缓存。这意味着你不需要让浏览器缓存任何内容,坐下来拟定哪些应该被缓存和缓存多久。例如,CSS和JavaScript文件应该被缓存比较长的时间,而用户的信息流应该频繁更新。

    证书类型

    目前存在有两种SSL证书.

    域名验证证书验证并没那么严格但足够便宜。50刀起,这是小站最好的选择了。对用户来说通常二者的区别是域名验证证书只会显示一个锁的图标,而扩展验证证书就会显示整个绿色的地址条。

    扩展验证证书是SSL证书的高端标准。他不仅会验证域名的主人还会验证域名主人的是否合法。因为这通常包含了证书权威局认证的个人成就的一部分,这些证书也是大大的昂贵的。通常价格是500刀起。这将是那些卓越大公司的证书首选。浏览器在使用这种证书的时候会显示出一整个绿色的地址条,让用户看到就会心如止水,信任感极强。

    什么时候用HTTPS

    通常看到的情况是整站使用HTTPS或者只在敏感数据的地方使用。多年来我们经常看到登录页和购物车页会被加密。他们仍然在必须的地方加密但是会在中间攻击时把用户会话暴露给黑客。最近比较流行整站使用HTTPS。用来说明你站点的任何地方都已被加密。这在很多时候都不错,HTTPS的局限应该被仔细想好,不要盲目的在没评估过流量的时候整站使用HTTPS。如果你已经确定想好你的应用在使用HTTPS后他的局限性小于加强安全性,那么全站使用HTTPS是非常推荐的。

    此时此刻你是不是觉得很容易就会忘掉整个HTTPS事件呢?呵呵,好吧,让我们慢慢来。不管你还有什么限制你都有责任为你的用户实现一切你能实现的所有安全策略。如果你正在跑的程序比如是购物车或者收集信用卡信息,那么毋庸置疑,一定要采用HTTPS。即使有些还不能确定是绝对的敏感数据,若有账号相关也应该被加密保护起来了,别再搁置了,只要需要就把HTTPS用起来吧。

    实现 HTTPS

    我需要哪种SSL证书?

    主要问题是你是否需要用在子域名上。如果你需要用在很多子域名,如

    api.yourApp.comdocs.yourApp.comyourMom.yourApp.comcart.yourApp.com 

    你就需要通配SSL证书了。如果你只是用在主域名,如

    yourApp.com 

    那么非通配的标准版就足以应付了,用通配那个版本的好处就是当你之后需要的他的时候直接就有,节省成本。

    生成服务器证书

    为了让证书颁发机构签署并生成你的证书你需要在服务器上生成密钥然后上传这些到证书颁发机构。

    对了,还需要OpenSSL,如果你服务器上还没有,你应该安装上它。在各种操作系统上安装软件的教程不在本书的讲述范围,网上有大把的教程,希望你可以完成在你服务器上的配置HTTPS的任务。如果你不会那找个人来帮你是个不错的选择。

    第一步先建个目录来存储密钥,可以随意找你喜欢的目录比如

    /usr/bin/ssl/ 

    我们来生成私钥

    $ openssl genrsa -out yourApp.key 1024 

    然后使用私钥生成签名

    $ openssl req -new -key yourApp.key -out yourApp.csr 

    过程中一般都默认即可,一个需要注意的地方是"Common Name",这里写你的域名,如"yourApp.com"。

    这时候我们得到两个文件

    '' /usr/bin/ssl/yourApp.key "

    '' /usr/bin/ssl/yourApp.csr "

    做其他事之前,我们先备份一下key文件,然后再备份一次到其他地方。因为如果丢了你就要重新购买新的证书了,当然这时候服务器也会崩溃。

    获取SSL证书

    开始用起HTTPS的第一步是获取证书。一些证书颁发机构提供了一些便宜甚至免费的证书,但是他们不会预装在流行的浏览器使得对于面向公众的站点失效。如果是内部应用就可以使用那些免费的然后自签名使用,如果是其他的还是购买为妥。

    首先我推荐你看下你的DNS提供商是否有一些折扣的或者易配置的证书,比如我用的DNSimple,他们就有大的折扣。

    如果你的DNS提供商没有证书服务,Symantec/VeriSign是不错的证书颁发机构。

    现在支付吧。

    然后就根据你选择的证书颁发机构提供的步骤来吧,通常你只要上传你的签名(yourApp.csr)就哦了,他们就会给你发邮件来注册证书。

    你的证书颁发机构会给你一个签名过的证书比如yourAppSigned.crt。上传到你的服务器,比如

    /usr/bin/ssl/yourAppSigned.crt 

    Apache 配置

    如果你使用Apache可以参考下面的步骤,如果是用其他的请跳过本小节。打开httpd.conf文件。注意,有些发行版为https使用了分离的配置文件。比如我现在使用的OSX版本就是用的httpd-ssl.conf文件。

    添加一个如下的虚拟主机,它可能和你HTTP站点的长的类似

    <VirtualHost *:443>      DocumentRoot "/path/to/your/app/htdocs"      ServerName yourApp.com      SSLEngine on      SSLCertificateFile /usr/bin/ssl/yourAppSigned.crt      SSLCertificateKeyFile /usr/bin/ssl/yourApp.key</VirtualHost> 

    重启 Apache

    $ apachectrl restart 

    or

    $ service apache restart 

    通常这时候访问"https://yourApp.com”,你发现已经搞定了!

    Nginx 配置

    如果你的服务器是NGINX可以参考下面的步骤,如果你再使用其他的,不好意思了,篇幅有限……自己搜搜吧。呵呵!

    打开Nginx的虚拟主机配置文件,一样添加一个类似的配置

    server {      listen   443;      server_name yourApp.com;      location / {            root   /path/to/your/app/htdocs;            index  index.php;          }      ssl             on;      ssl_certificate     /usr/bin/ssl/yourAppSigned.crt      ssl_certificate_key   /usr/bin/ssl/yourApp.key}     

    重启 Nginx

    $ sudo /etc/init.d/nginx restart 

    or

    $ service nginx restart 

    访问吧"https://yourApp.com”,哦了!

    小附加内容

    Apache的最好资源就是他的官方文档

    http://httpd.apache.org/docs/current/ssl/ssl_howto.html 

    NGINX 的 WIKI 也是一个不错的开始

    http://wiki.nginx.org/HttpSslModule 

    其他的服务器?把下面网址中的yourWebServerName换成你的服务器名称,敲在浏览器的地址栏,奇迹即将发生。

    http://lmgtfy.com/?q=yourWebServerName+SSL+certificate+setup 

    路径

    绝对路径

    你既然已经搭好了HTTPS环境,那么用户访问的时候你应该在需要的时候让他们访问到HTTPS版本。Apache/Nginx 的重定向都能做到。另一个简单的做法是直接把绝对路径配到你的HTTPS网址上,比如"https://yourApp.com",然后强制把HTTP的链接重定向过来。

    很多时候你希望某些页面使用HTTP而其他的使用HTTPS,这时候正确的服务器配置及合适的代码路由设置就成了关键。

    相对路径

    另一个需要说的不是我们这节安全相关的重点但可以在你同时使用HTTPS和HTTP的时候让你的生活变简单。资源的网址,如CSS,JS可以用两个反斜杠来替代http:// 或 https:// 来正确的适配协议。比如你可能在主页有下面的代码

    <link type="text/css" rel="stylesheet" href="//assets/main.css" /> 

    访问 https://yourApp.com 的时候会加载

    如https://yourApp.com/assets/main.css

    如果访问 http://yourApp.com 则会聪明的加载

    如http://yourApp.com/assets/main.css

    这虽然是一个小技巧,但是非常实用,其实,我们都关心,不是么?

    搞定

    好样的,我们都搞定了。摸摸头,拍拍背,喝点喜欢的饮料,小憩一会,我们再看下一章。

    第三章 - 为每个人存储加密的密码

    你现在应该已经知道了我们要说的内容了。克里斯是Marvel Comics web开发组的一个初级开发者。又是一个变态的伯班克午后的酷暑。在他们团队最近的漫画门户项目中克里斯负责开发登录模块。他的"团队"的意思是说他和其他开发者,克里斯忘了抹清凉油,今儿可真热啊。

    克里斯已经构思好了登录系统的工作原理。大家期望的标准流程都有,包括登录,登出,忘记密码等一系列……他还需要存储密码以便在登录时验证,并在找回密码的时候以便发送给用户。一会儿的时间他想过了所有的登录流程并开始思考用户在数据库访问密码时的安全问题。他知道他应该对密码加密但是如何等登录的时候解密?在找回密码的时候又该如何?

    经过漫长的枯燥45分钟的研究克里斯决定使用PHP的两个函数mcrypt_encrypt()和mcrypt_decrypt(),克里斯振奋起来,密码的加密解密的安全问题已经都使用PHP搞定。项目马上就要做完了,他还有一半的时间。

    但是朋友(我们是朋友了对吧?),当你再Marvel.com重置密码的时候你的邮箱收到了你之前的明文密码。在一封邮件里!有你的明文密码!你现在看起来需要改一个你在任何地方都没有用过的密码了。然后我默默的等待。默默的在街角哭泣,想着这会被如何利用,想着人们的苦难,何时才能后天下之乐而乐。

    这个故事的中心思想是,不要存储密码。只存储密码的不可逆哈希串。让我们来看看如何正确的做这件事吧。

    小说明

    我并不是密码专家。这些都是我从经验得到的个人建议。这些都是主观的最佳做法但并不意味着可以为安全存储*发射代码作为说明书。你最重要的推特消息都会保持安全的。

    哈希是什么?

    我们第一把覆盖到概念了。散列法并不是加密,密码的散列是不可逆的。这意味着不能被解密。其实并没有像用户/管理员/老妈/任何人展示密码的需求。一旦密码被输入它就变成了一个唯一哈希序列,而只能由输入的原始密码散列计算出。

    流行的攻击

    在进一步讨论之前,我们先来深入探讨下哈希算法的流行攻击。

    查找表

    查找表就是根据已知密码查找散列序列的对应关系表。简单表述如下

    password | hash------------------------pass1    | bidfb2enkjnfpass2    | psdfnojn3nodetc... 

    这就是等下要和你数据库中的密码散列值对比的数据用来确定是否恰好“碰到”。这种攻击在你使用加盐的随机散列的时候是几乎没用的但是如果散列不加盐当然就要比抢劫火车容易多了。

    彩虹表

    彩虹表在技术复杂度上和查找表很像。他基于数学方法用较小内存实现了查找表。我们在这里提到的你都可以互换理解。彩虹表在面对加盐的随机序列时也不好用所以也与现代哈希算法不太相关。

    彩虹表非常复杂,超出了本书的范畴。如果你想了解的更多,请参考 [original paper published by Martin Hellman] 介绍了它的概念. [Wikipedia article on Rainbow Tables] 也是一种比较容易消化的学习资源。

    碰撞攻击

    碰撞攻击是大多数哈希算法的主要安全漏洞。有两种碰撞攻击类型。

    经典的碰撞攻击是在两个不同的字符串生成了一样的哈希,例如

    string1 = 'abc123'string2 = 'bcd234'hash(string1) === hash(string2) 

    前缀选择碰撞攻击是两个不同的前缀组成的两个字符串生成了同样的哈希,例如

    prefix1 = 'zxy'prefix2 = 'abc'string1 = 'abc123'string2 = 'bcd234'hash(prefix1 + string1) === hash(prefix2 + string2) 

    来点盐

    当然,这还远远不够。下面的问题是查找表以及彩虹表的漏洞利用。这些漏洞都是基于一个保存了流行密码哈希序列的一张表。彩虹表稍微复杂一些但是还是可以搞定的。我们怎么预防呢?盐,随机盐。

    盐是为了使哈希唯一而附加在其上的东西。盐也可以被添加到玛格丽塔的边缘,使之更加美味。你就可以用一个随机字符串(盐)和文本密码拼在一起来得到一个唯一的值。这意味着即使有了密码哈希表,攻击者也不能正确的匹配上因为你使用的随机盐。只有两个完全相同的密码才会生成一样的哈希。随机盐是保障密码安全非常重要的一部分。

    随机的也不一定总是随机

    你加的盐只有随机才会生效。尽管随机!==随机。你不一定需要真正意义的随机数,rand()函数也不会使之竹篮打水。

    使用PHP的内置函数rand()或mt_rand()的问题在于可以被种子数据操作和决定,而不够"随机"。生成一个真正随机数的重要组成部分是在源头中填充足够的熵。熵是由生成随机数的系统集合的真正随机信息的总量。大多数非加密随机数生成器,像rand()和mt_rand(),是使用算法生成数字而没有足够的外部数据使之真正唯一。这意味着rand()产生的数据可以被攻击者猜测并操作。通过认真观察rand()的输出攻击者可以敏锐的判断出规律并预测。实际上只需要rand()的624个值就足以预判后面的所有值了。

    你已经被警告过了,不应该使用rand()作为盐。当你购买这本书的时候我们就已经在此约定,你不会因为你使用rand()作为盐的时候我用第一代iPad扇你的脸。比板砖还带劲。

    问你服务器的/dev/random在大多数系统中是真随机的最好办法。缺陷是当把它用在登录模块时会在收集系统熵的时候阻塞。收集熵时也会收集系统的环境数据;像各种硬件数据,键盘输入,鼠标移动,硬盘的访问,等。为了生成一个随机字节的缓冲区。这会消耗较长的时间来返回结果,尤其在系统不忙的时候。我最近正好碰到了这个问题,我们团队的一个小哥正使用/dev/random生成一个激活码。一切看起来都那么美好,过了测试。但当我们把它部署到生产机器时,响应需要60秒。服务器压力并不大因为只有这个项目在跑,导致/dev/random阻塞的时候正在收集熵。咋解决咧?/dev/urandom;

    /dev/urandom在真正随机上并不健壮但它却是密码安全的。也就是说它并不是真正随机数但被认为在用作盐商足够安全。/dev/urandom能不阻塞的马上返回一个非常不错的伪随机数。/dev/urandom使用已有的熵池来生成伪马上返回一个非常不错的伪随机数。/dev/urandom使用已有的熵池来生成伪随机数,在大多数身份验证系统中都足够安全。如果你正在为一个*发射系统写登录代码最好还是让用户在/dev/random旁边等待,但如果用在社交照片分享站点我想/dev/urandom就已经很不错了。

    哈希算法

    让我们一起讨论下流行的哈希算法。

    MD5

    没什么比不恰当的使用MD5算法被我们熟知了。通常是因为大多数数据库都缺省支持。MD5早已被数学方法证明其并不安全。它的缺陷是在现代硬件上非常容易产生冲突。

    有一个不容忽视的例子,在2005年研究员们能够使用笔记本电脑在MD5校验和产生冲突。这说明并不需要$200k的性能怪兽服务器就能破解MD5,只需要2005年代的一台笔记本。对互联网来说就像100年以前。别再使用MD5做密码的哈希了。这是在用不安全的哈希来验证数据内容,当然,这也会让攻击者更有兴趣。

    MD5也不是糟糕的一点不能用因为多数情况使用合适的盐还是足够安全的。但这也不是告诉你不应该为未来寻找更好的解决方案。

    SHA-1

    古老的SHA-1加密,多年来一直被认为是安全和可信任的。在互联网也存在了数十年。在2005年(2005在安全上真是悲催的一年)山东大学的科学家发布了一个研究报告,SHA-1算法可以通过不到2^69^次哈希就产生了冲突。2^80^次哈希操作时产生的冲突被认为是密码安全的。参考下这个,2^80^差不多是1.20892x10^24^,因此"密码安全"看起来是还算安全的。但后来摩尔定律又证明了SHA-1并不安全而且不应该使用在需要保证绝对安全的应用中使用。

    就像上面说到的MD5,当我们使用随机盐的时候SHA-1在算法上也是安全的。

    SHA-256 / SHA-512

    在2001年SHA-2被当做SHA-1的迭代版本。在2005年SHA-1被证明不安全之后它被慢慢接受。SHA-256和SHA-512一个道理;SHA-256使用32位字符,而SHA-512是64位。他们的循环次数也不同。但核心算法几乎是完全一样的。

    SHA-2目前被认为是密码安全的,在使用足够的循环之后(>64)仍没有发现漏洞。

    由于没有被Blowfish大量的审查,BCrypt只在内部使用。Blowfish加密自1991年以来一直被认为是安全的,但使用它弱键是一个已知的缺陷。它是基于一个加密算法给BCrypt带来了一层额外的开销,使它优于标准的哈希算法,换句话说BCrypt设计的较慢。但慢是一件好事!

    在这我推荐BCrypt, SHA-256 或者 SHA-512是可以作为目前有效的安全备选的。可以用来作为衍生算法的一部分,比如PBKDF2或用PHP的crypt()函数实现。

    BCrypt

    大多数人觉得BCrypt是一个新丁而没有被广泛的使用。其实它是在1999年发布的,这明显是老头儿了。BCrypt是基于Blowfish密码的衍生方法,它是迭代的所以由于生成哈希的开销关系他可以防止暴力破解。

    尽管事实上很多密码学家都在大量关注BCrypt但目前仍还有公布的漏洞。一直到写这篇文章的这么长时间以来(2014),BCrypt被认为是密码安全的。

    BCrypt在加密纯文本密码的时候有72字符的限制。所以经常有切掉多余字符的做法或者只支持最大72个字符的验证。

    BCrypt也是我们后面例子中选择的密码加密方案。

    SCrypt

    SCrypt是个新成员。在2012年发布,它是一个在内存方面加强的衍生算法。理论上来说SCrypt在高内存消耗条件下是个更安全的算法。但是它太新了我个人不太推荐在现在使用。

    在密码界新的一般都不太受欢迎,因为它并没有接受足够的像那些老算法那样相同等级的攻击和审查。虽然最近爆了一些漏洞但这并不意味着SCrypt不够安全,但还是要远离理论上的安全优势。

    值得注意的是SCrypt已经被很多流行的加密货币使用到他们的采矿业务,显著的有莱特币和狗狗币。这意味着它也会像它的前辈一样受到大量注意。

    存储

    这节会比较短,嗨起来!不管你使用什么系统存储你的密码哈希,不论使用关系数据库,密钥存储器,锁箱,袜子抽屉,或者文件系统;使用不限长度的文本字段或者我推荐使用varchar(255)。你的哈希算法会生成最大长度的字符串所以你并不用为攻击者脱你的数据库而担心。不同的哈希算法会生成不同长度的字符串所以你要根据你的算法来设置字段长度。我比较喜欢设置较长的字段以防之后用别的哈希算法,而不抠门在那多出的存储空间。

    使用BCrypt你的哈希最大是60个字符。所以理论上来说你可以设置一个varchar(60)的字段但并不兼容之后的修改。为了你的密码还是别在乎那几个字节了,搞个text字段或者用varchar(255)吧,你值得拥有。

    验证

    唯一需要验证的是密码的最小长度。你应该允许使用字符,空格,短语等,你的用户可以设置他们想要的任何密码。好的短语是比较推荐的,"correct horse battery staple”总比"myNewPassword”要强。你只需要担心密码不够复杂,不够长。

    为了给所有人你的爱,别做像通过Javascript限制复制粘贴的傻事。如果人家用户使用密码管理工具你应该让其用的更爽。如果你这么做了,你的用户会觉得很不爽,骂声当然就来了。

    有个需要注意的是BCrypt只能使密码的前72位有效所以你可以从技术上进行约束。这虽然会稍微有些限制你的用户但是对你未来选择其他哈希算法带来了方便。如果你的用户对一个74个字符的密码记得特熟练,你可以只使用它的前72位,总好过让他想个新的出来。有些代码会推荐先使用标准哈希算法(SHA-256, SHA-512, 等)然后再使用BCrypt对其哈希结果再做一个来解决这个长度问题。这真是一个有够完美的解决方案,但我不太推荐,因为72个字符加上一个有效的盐足以让你的密码在现代科学上有够安全了。

    放在一起说说

    我们刚说了一些基础知识,现在我们一起敲点代码吧。

    开始我会带你用传统的/弃用的做法随后我也会带你使用PHP 5.5或更高版本的新方法来搞这些。原因有下:

  • 我希望你能够了解底层原理而不是只看到一个封装的方法

  • 你可能正在使用一个低版本的PHP,但是我还不想放弃你

  • 下面的场景是注册新用户。我们需要生成一个随机盐,做他们的密码哈希,然后把他们都存在一个虚拟的数据库中。作为未来的身份验证使用。

    如果你还在用低于5.3.7版本的PHP那么你至少要为了保证最起码的安全升级到5.3.7来。如果你实在不升我不知道还能推荐给你什么。升级之后你会发现很多老版本的bug都被修复了。在我们的场景中,BCrypt在5.3.7之前就有缺陷。我们会在下面的例子中使用$2y$前缀,他会一直作为前缀参数。意思就是说他已经更新了漏洞修复和新的逻辑。

    第一步我们要先生成一个随机盐。根据你系统安装的扩展使用PHP你有很多方法去做这件事。比如:

    <?php//获取二进制随机盐$saltLength = 22;$binarySalt = mcrypt_create_iv(      $saltLength,      MCRYPT_DEV_URANDOM);//把上面的数据变成安全字符串$salt = substr(      strtr(            base64_encode($binarySalt),                '+',                '.'              ),              0,              $saltLength        ); 

    我们使用了MCRYPT_DEV_URANDOM参数的 mcrypt_create_iv()函数,这样就可以告诉它从/dev/urandom缓冲区取内容。封装在里面的bin2hex()让我们可以得到一个十六进制字符串来替代二进制数据。

    我们使用了MCRYPT_DEV_URANDOM参数的 mcrypt_create_iv()函数,这样就可以告诉它从/dev/urandom缓冲区取内容。封装在里面的bin2hex()让我们可以得到一个十六进制字符串来替代二进制数据。

    <?php//获取二进制随机盐$saltLength = 22;$binarySalt = file_get_contents(  '/dev/urandom',  false,  null,  0,  $saltLength);//把上面的数据变成安全字符串$salt = substr(  strtr(    base64_encode($binarySalt),    '+', '.'  ),  0,  $saltLength); 

    现在我们已经得到了盐,下面我们对密码做哈希

    <?php//获取二进制随机盐$saltLength = 22;$binarySalt = mcrypt_create_iv(  $saltLength,  MCRYPT_DEV_URANDOM);//把上面的数据变成安全字符串$salt = substr(  strtr(    base64_encode($binarySalt),    '+',    '.'  ),  0,  $saltLength);//设置bcrypt哈希的cost参数//多做实验得出服务器的正确值$cost = 10;//现在我们要连接算法代码 ($2y$) 和 cost 以及盐$bcryptSalt = '$2y$' . $cost . '$' . $salt;//好好的哈希一把$passwordHash = crypt(  $_POST['password'],  $bcryptSalt);//返回了一个安全的哈希//后面也可能出来一个错误代码或者一个不安全的哈希哦if (strlen($passwordHash) === 60) {      //下面只是示范      $db = new ImaginaryDatabase;      $db->user()->create(array(    'password' => $passwordHash  ));}else {  //错误提示} 

    现在我们已经给密码做了哈希并存入了数据库。注意我们为什么没有存盐呢?是因为crypt()会保存返回的哈希中我们选择的算法,密码的哈希值,以及盐。

    我们已经有注册用户了,比如他要登出站点(好大的胆子!)然后一会又回来了,他们需要登录。登录是简单美好的,我们只需要验证他的文本密码和我们存在数据库中的哈希。

    <?php    //我们先定义一个默认值    //失败,一定要总是在开始定义默认的失败    $valid = FALSE;    //从我们的数据库中获得哈希    $user = $db->user()-where(array(  'email' => $_POST['email']))->row();    //验证他们是否相等    $pass = $_POST['password'];    if (crypt($pass, $user->pass) === TRUE) {        $valid = TRUE;    }//其他的验证,错误处理等等...    if ($valid === TRUE) {  //验证有效} 

    如果我们把之前生成的哈希放在crypt()的第二个参数他将自动选择算法并使用相应的盐来生成哈希与存在数据库中的密码哈希进行对比。

    >= PHP 5.5

    在PHP5.5.后引进了新的密码哈希函数使得很大程度简化了密码操作流程。目的在于隐藏那些复杂的逻辑并使PHP开发者使用默认的函数就能保证安全而不依赖于所有的开发者都需要掌握这些密码知识,换句话说他们正在努力使我的书越来越不好卖,尴尬。

    严肃的说,应该尽可能的使用PHP的密码函数。它提供了内置的最新版本哈希并加带附加的安全检查而我们这里并没有包含,比如保护定时攻击(Timing Attacks).

    我们下面就讲一下PHP 5.5语法的步骤。我们将用到password_hash函数来生成密码的哈希,这个函数会自动创建随机盐所以我们会省一些步骤。我们看下代码:

    <?php//仅供参考 - 默认的cost为10, 它可以自定义//做哈希$passwordHash = password_hash($_POST['password']);//下面只是示范$db = new ImaginaryDatabase;$db->user()->create(array(  'password' => $passwordHash)); 

    下面是在登录的时候验证密码

    <?php    $valid = FALSE;    $user = $db->user()-where([  'email' => $_POST['email']])->row();    $pass = $_POST['password'];    if (password_verify($pass, $user->pass) === TRUE) {          $valid = TRUE;    }if ($valid === TRUE) {  //附加逻辑} 

    像上面你看到的那样,PHP 5.5中的新密码函数可以让你的生活简单清爽。这个函数的另一个优点是它内置于PHP所以你在未来可以一直依赖它,而不是自己持续的追最新密码算法的版本。这很吸引人对吧,把它的利弊交给PHP核心团队,留更多的时间让自己去做其他更精彩的事情。

    当你想要实现身份验证模块的时候确保参考PHP文档来保证代码的时效性。当然,也应该知道其他有效密码函数的优势比如password_get_info()和password_needs_rehash().

    你还在用PHP 5.3么?错过了新版5.5的所有乐趣?别急,Anthony Ferrara帮你做了一些事情。他的password_compat库在PHP 5.3.7实现了新的密码函数,你可以从这里下载[Anthony's Github]

    我极力推荐你使用这个库而不是自己写,因为它已经被充分测试过了我们完全可以信赖它。

    残忍的暴力攻击防护

    你可以拥有最完美的密码哈希,用了所有的安全措施,但是当残忍的攻击者不断的攻击你的登陆页直到他获得正确密码的做法是没有用的。暴力攻击就是攻击者使用软件不断的尝试不同的密码直到他登录为止。

    防护这种攻击很简单,遗憾的是我发现很多站点并没有做的很好。所以我们该如何做好呢?那就是当有人以这种方法获取密码的时候就让他靠边等着去吧。

    一个人试着登录,失败了。然后又重试,又失败了。再来,呵呵。这时候你就让他靠边等一分钟再来。好了,这就是最简单的解决办法,也是最有效的办法。如果一个攻击者每分钟才能试三次密码那么他将等到天荒地老,还攻击什么呢。你的用户安全了。不一定是限制三次,三、五、十都不错;我个人推荐每分钟不超过10次。

    你还可以扩展一下,限制IP。只允许某一IP登录所有用户X次而不是只限制登录一个用户X次。还要确保包含恰当的错误提示以及合理的重试时间,企业团体用户一般都会抛出一个公共IP地址所以你还不能因为一个坏人而惩罚数千个合法的用户。

    当然,把相同的逻辑用在API验证上,让攻击者认识到不同的地方也会是一样的结果。不管你在前端用了什么安全措施,也要把他合理的移植到API上去。

    升级遗留的老系统

    现在房间里有个大象。抓住他?大象哎。一晚上也抓不到吧。那么我们如何升级在使用MD5密码并没加盐的原有系统呢?

    我可以给你两个选择:

    1 - 在每个用户登录的时候默默的帮他用BCrypt升级他的密码。他们在什么都不知道的时候你的数据库就已经更新到了安全的密码。

    2 - 使用BCrypt在原有的MD5串上再做一次哈希。新密码就是MD5和BCrypt的二重加密了。

    第一种升级办法

    第一种方法是迁移新身份验证方案的传统建议。而且在大多数情况下是最好的办法。你可以像下面这样做来实现:

    <?php    $valid = FALSE;    $user = $db->user()-where([  'email' => $_POST['email']])->row();    if ($user->pass_hash_algo === 'bcrypt') {          //他们已经登录过并已经完成了升级,我们可以验证登录        }else {              //验证一下老密码              $oldHash = md5($_POST['password']);              if ($oldHash === $user->pass) {                    //用原密码生成一个新的哈希值                    $newHash = password_hash($_POST['password']);                    //保存到数据库并标记完成                    $user->pass = $newHash;                    $user->pass_hash_upgraded = 'bcrypt';                    $user->save();                    $valid = TRUE;              }        //...} 

    这样改一下登录逻辑你的用户将在登录的时候自动升级他密码的新哈希。积年之后这次升级将轻松的用上了我们推荐的哈希算法。之后SCrypt可能会变成被接受的标准那时候你的升级将轻松易得。

    第二种升级办法

    第二种办法虽然解决的不清澈但是一经使用立刻生效。上面的第一种方法在一段时间还会保留一些不安全的哈希密码直到所有的用户都登录过但这种办法一旦上线立刻能使你的数据安全,你不会丢失任何一夜的睡眠。这节你会直接在所有的密码哈希上使用BCrypt再做一次哈希。你的最新哈希是在MD5基础上使用了BCrypt之后的强大哈希。这增加了复杂度因为你每次都需要同时使用BCrypt和MD5直到永远,会笨重很多年。

    一共包括两步,第一步写个类似下面的脚本扫一遍你的现有数据库。告诉我你会用导出的数据库而不是在生产服务器上干这件事。对吧?因为那会让我们都伤心…你还要在每条记录上加一个状态。下面是个例子,别复制粘贴啊,咳咳...

    <?php    $users = $db->users()->result();    foreach ($users as $user) {          $newHash = password_hash($user->pass);          $user->pass = $newHash;          $user->save();    } 

    现在数据库已经升级了我们还要改下注册方法

    <?php    //先用MD5再用BCrypt    $passHash = password_hash(md5($_POST['password']));    //下面只是个示例    $db = new ImaginaryDatabase;    $db->user()->create([  'password' => $passHash]); 

    当然登录方法也要同样的将校验文本密码改成验证MD5密码

    <?php    $user = $db->user()-where([  'email' => $_POST['email']])->row();    //在使用bcrypt之前先使用md5方法    $md5Pass = md5($_POST['password']);    //验证密码的正确性    if (password_verify($md5Pass, $user->pass) === TRUE) {          $valid = TRUE;    }//其他的验证等     

    好了,我们终于安全了

    这就是本节的全部了。虽然还有更多关于密码的知识但你必须知道的已经在这里了,我可能不会写密码的附加章节或者高阶内容了。敬请期待下一章吧,我们会讲URL的限制,安全的文件操作,确保没人能看到你的数据。

    资源

    如果你对本章的密码学感兴趣我强烈建议你看下Bruce Schneider的研究成果。你应该读一下他的书Applied Cryptography,在加密方面很权威。

    我还喜欢关注PHP社区其他成员的工作,Anthony Ferrara倡导从PHP5.5内置密码哈希函数。这使得我们在未来缺省构建安全的PHP应用方向迈了一大步。

    第四章 - 身份验证, 权限控制, 安全的文件操作

    Erica在一个小型的制造业公司做程序员以及IT支持。他最近被分配开发公司的员工系统,带领公司从纸质时代飞到数字世纪。

    第一步是让员工扫描文档,用电子存储替代纸质表格。

    归档系统开发的一切顺利,系统由所有文档组成的数据库清单构建,还能过滤像部门,日期,标签这些。没什么复杂逻辑,Erica就想保持简单。

    文档会以用户的部门和职位为过滤条件呈现给用户(比如,某人,管理部,主管)。几个月以来都相安无事,但有一天还是迎来了不快。一个机密文件出现了漏洞。经过调查发现一个不满的员工得到了一个只有高级管理员能浏览的文件访问权。这个员工把此文件泄露给了竞争公司听说轻而易举的拿到了竞标。

    这是怎么发生的呢?Erica花了一个周末去研究,突然,他发现了,这是多么的明显,但他没有想到要防范。

    在他的系统中,文件上传的时候是以数据库表中的ID命名的。上传了一个PDF文件可能叫"13424.pdf",又上传了一个DOC文件,就被名为"13425.doc",诸如此类。Erica虽然已经使文件不可见,但他没考虑到文件可以直接被浏览器下载。用户可以轻易的通过猜测自增ID下载到敏感的文件。

    这小故事略长,但只要你努力你也能讲出这么长的故事。我曾经告诉一个妇女我是Kevin Costner,他就信了,因为我也信。

    身份验证

    控制敏感数据的第一步就是身份验证,你要确保每个用户并不是只靠自己说说就能证明他是谁。

    这里我们会阐述一下上一章中密码验证的代码实例,然后会讲下用户的有效登录过程。

    <?phpclass UserModel{  //... 其他方法      function login($user, $password)  {            if (password_verify($_POST['password'], $user->pass) === TRUE) {                  $valid = TRUE;                }    //其他操作等...            if ($valid === TRUE) {                  //成功登录                  //存储session                  Session::put('user', $user->id);                  return TRUE;            }                //登录失败            Session::put('user', NULL);            return FALSE;            }} 

    我们记下了用户的成功登录态,只存储了他的用户ID。但他的登录并不是说他可以在你的应用里为所欲为。

    权限控制

    除了判定用户是否登录有效,我们还要判定他是否有某些页面/部分/功能的访问权限。

    我们从基础说起。"麻瓜"和"管理员"。麻瓜是拥有少量访问权限的普通用户。管理员就是可以访问所有地方的人物。管理员可以增删用户,管理文章等。非常重要的是普通用户 - 麻瓜 - 不能操作这些功能

    不同的框架在验证使用权的时机各不相同。在Laravel中,Symfony是首选。在大多数系统中你可以只简单的调用构造函数。下面是个例子;你可以根据情况改变实现方式。

    我们先在UserModel中添加一个获取用户信息的方法

    <?phpclass UserModel{//... 其他方法//... 登录方法    function current()  {        $user = FALSE;        if (isset($_SESSION['user']) === TRUE) {            $user = DB::findById($_SESSION['user']);        }            return $user;    }//假设还有一个获取用户组的方法} 

    现在我们来验证一下该用户是否有访问权。

    <?php    class AdminController{          function __construct()  {            $userModel = new UserModel;            $user = $userModel->current();            //检查用户是否登录是否为管理员            if ($user === FALSE || in_array('admin', $user->groups) === FALSE) {            //做好错误处理            die('Not Authorized');    }  }  //其他的管理员专用操作} 

    你通常会在你的controllers/routes/或其他地方抽象出一个控制层。这一层会梳理你的路由恰当的拥有视图的访问级别。比如,/admin/*只能是"admin"用户组访问。POST和PUT请求只能"编辑"使用。DELETE请求只能管理员使用。

    关键是,每个页面都应该检查用户的访问级别并确定他们是否被授权请求各种方法。你不仅需要去设想一个用户可以看到一个表单并能POST到正确的地址,并授权执行了动作。我们还应该防止恶意用户提交恶意数据改变你的数据库,他在一开始就不应该看到那个页面。

    别在安全的身份验证上掉以轻心,认真起来!你会为你自己和其他开发者节省不必要的开发时间也减少了未来的头疼。

    验证重定向

    在你的应用流程中,经常会通过表单发送一个POST请求到你的控制器,之后验证数据,执行动作,然后重定向到应用的下一步页面。

    如果你重定向的地址包含敏感数据,用户其实可以通过简单的发送一个请求跳过你的期望流程直接到最后的页面,并跳过了你的验证步骤。

    当然有很多办法来控制啦。第一种办法就是不再使用重定向。大多数时候当你想要用重定向的时候,你可以换成直接调用下个方法,比如把下面的替换一下:

    <?phpif ($valid === TRUE && $dataSaved === TRUE) {  URL::redirect('/blog/1/edit');} 

    改成直接调用edit方法而不是使用重定向:

    <?phpif ($valid === TRUE && $dataSaved === TRUE) {  $this->_edit(1);} 

    在这个例子中,_edit()方法应该是一个保护或私有方法,这样就不能跳过前面的步骤而用URL直接进入了。

    如果实际上你需要一个其他的地址,而且你有很多入口都会指向这个目的地,那么你就必须验证那些必须的步骤已经被执行过。

    使用一个有过期时间的session变量(一般叫做flash)通常是不错的办法。

    <?php    if ($valid === TRUE && $dataSaved === TRUE) {          Session::flash('blogEditValid', TRUE);          URL::redirect('/blog/1/edit');    } 

    在你的edit函数处, 你应该验证刚刚的参数

    <?php    public function edit($id){          if (Session::get('blogEditValid') !== TRUE) {                //错误信息等                die('Not Authorized');          }    } 

    模糊处理

    你有没有听说过"security through obscurity(隐式安全)”的术语?虽然不是所有的地方都有效,但有时候真的好用。大多数应用的数据库都会在每个表中存一个ID的字段来做主键。这个ID后面被用在系统的各个地方。比如URL,表单,还有API用来表示我们需要的数据。

    有时候,试想,你并不想对你的用户展示真正的数据表ID。也许你在做一个新产品的时候也不希望用户知道他原来才刚刚是第十三个用户。也许你的数据是公开的但你并不想被机器人蠕虫轻易的爬去。

    这时候,你可以混淆ID到非自增的字符串但可以翻译为你的ID。如果你是这样做的:

    성명:
    본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.