规划你的WebAPI – 安全
译自“Professional Web APIs with PHP: eBay,
Google, PayPal, Amazon, FedEx, Plus Web Feeds” (chapter 12)
你一定听过这样一句话“失败的计划是必定失败(Fail to plan, and you plan to fail.”对WebAPI来说,在开始一切工作之前的规划尤为重要,因为他不仅会影响到实现难度,同时会让那些使用API的开发者痛苦不堪。
通常来说,引入额外的安全层次会更好的保护你的API,但是同时需要很好的平衡这种设计对易用性的影响。时刻记得,安全即是保护你的数据,也是确保开发者的调用过程完整(通常是用“token”)。
在一个完全没有安全验证机制的开放API里,首先会有一个来自外界的请求,然后系统会尝试完成并响应这个请求。
优点:
- 最小的使用障碍 – 既没有加密页没有验证机制,任何人都可以访问你的API。
- 更容易的创建分布式应用 – 登录帐户或者程序员,只要他们使用了你的API,那么程序可以分布到任何地方,而你根本无需考虑他们在哪里调用。
- 省心 – 如果你没有管理用户账户和开发密钥,那就可以花更多的时间在开发API本身。
缺点:
- 缺少控制 – 任何人在任何地方都可以调用API,尽管这是web服务的目标,但可能会在潮水般的请求涌来时失去控制。如果这些请求只是来自一部机器,还可以借助防火墙来搞定,但是如果分布很广,处理起来就会很痛苦。
- 没有加密 – 所有请求端和服务端的请求和响应都是对任何人都可见的。
- 无法接触到开发者 – 因为API的调用不存在注册过程,也就无法联络到相应的开发者。而你可以通过注册机制建立一个与开发者的一个很好的联系。比如,告知他的应用正在被误用,API有新的改动,征求改进建议等等。
- 误用 – 很不幸,总有一些人会利用这一点去做一些不好的事,即便你觉得这个可能性很小。
因为这些问题,完全开放的API只适用于用来请求信息,而不是发布信息 – 也就是请求的信息资源产生过程不会占用太多CPU资源。一个很恰当的例子是国家天气服务API,它只接受信息请求,并且这些请求可以全天候的缓存在服务器上。如果是需要发布信息,那么相应的验证机制要被用来识别请求者,当请求需要消耗大量CPU时,远端程序需要被识别出来,从而对发来的请求进行过滤和控制。
通过HTTP头包含中验证信息,基于Base64编码,实际上并没有加密,没有信息安全可言。
优点:
- 简单 — 因为验证信息是在HTTP头里,所以可以被路由器和网关处理。从而可以用硬件过滤和筛查客户端请求。从应用的角度来看,验证实际上是发生在服务器端,因此设计服务器时应该考虑到高性能和高并发的开发和测试。
- 对应用来说透明 - 因为是web服务器来处理验证,你可以完全不需要考虑用户登录问题。当然这只适用于请求那些于特定用户无关的信息(每个用户使用相同的请求得到相同的信息)。
- 易于编码 — 添加一个额外的HTTP头信息对大多数编程语言来说都不在话下。It is also pretty universally available even in shared hosting situations (which may prevent things like SSL requests or external libraries).
缺点:
- 验证信息是明文传输的 — Base 64是可逆算法,任何人都可以从传输的信息中得到用户名和密码,但实际都不需要这样做,只需要修改HTTP头即可。
- 用户名限制 — 当使用HTTP验证时,冒号(:) 不可以作为用户名的一部分。
- 没有加密 — 所有请求和响应都是可见的。
这种基本的验证方式对大多数API应用已经足够了,基本的验证允许API既可以是客户相关也可以是客户无关的,取决于是否需要。同时允许过滤那些有问题的客户端。更好的办法是将用户名和密码组合分开,这样验证信息可以有点保护,合法用户可以使用其他信息去修改API的使用权限。
大多数工作都是由web服务器来完成的。Apache可以使用一个文本文件查找用户帐号,但如果API允许用户信息修改,那么这不是一个明智的选择。Apache可以使用Berkeley数据库(如果你设置了mode_db或者mode_dbm模块),BerkeleyDB在大多数linux版本里都是标准组件,如果没有安装,可以从www.sleepvcat.com下载。要使用BerkeleyDB,PHP需要配置“-with –db4”选项,相应Apache必须要使用“—enable-module=auth_db”选项编译。
Httpdconf要配置为((.htaccess也要类似配置):
<Directory /www/domains/api.example.com >
AuthName "API Requires Registration"
AuthType Basic
AuthDBUserFile /www/basicAuth/api.example.com/passwords.dat
require valid-user
</Directory>
Directory参数指定被保护的文件夹
AuthName指定当浏览器访问该目录时显示给用户的消息
AuthType 设置为 basic,即基本HTTP验证方式
AuthDBUserFile Berkeley数据库的文件路径,它应该是web文档根目录以外的地方 – 你不想让攻击者可以下载它吧
require 指明想要访问目录的用户必须存在于数据库中
上面这些的前提是用户可以被添加到数据库中,那么用户如何添加呢?请看下面这个函数:
function createUser($username, $password)
{
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
$r1 = rand(1, strlen($chars) - 1);
$r2 = rand(1, strlen($chars) - 1);
$salt = substr($chars, $r1, 1) . substr($chars, $r2, 1);
$saltedPassword = crypt($password, $salt);
$resource = dba_open("/www/basicAuth/api.example.com/passwords.dat", "c", "db4");
if (dba_insert($username, $saltedPassword, $resource))
{
dba_close($resource);
return true;
}else
{
dba_close($resource);
return false;
}
}
该函数最重要的部分是用dba_open()函数打开数据库(第二个参数“c”指定如果文件不存在则创建他, “db4”是数据库类型),插入用户名和密码,然后关闭数据库连接。
|
注意 |
随机生成的salt会随同密码发送到crypt函数得到一个加密的密码从而可以提高字典攻击的难度。密码仍然可以被字典攻破,因为crypt函数会把salt添加到加密后的密码前面。 |
下面这个函数是删除用户:
function deleteUser($user)
{
$resource = dba_open("/www/basicAuth/api.example.com/passwords.dat", "c", "db4");
dba_delete($user, $resource);
dba_close($resource);
}
最后,你应该想要通过API脚本访问用户信息:
$username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
这两个变量都存储与$_SERVER这个全局数组里,password是明文存储的,需要记住,用户这时已经通过了验证,因此这些信息只是用来存放基本数据罢了。
要访问API,客户端程序需要发送一个包含了用户名和密码(Base64加密过的)的HTTP头。
$authorization = base64_encode("username:password");
首先用户名和密码必须要用内置函数进行编码,“username:password”则是HTTP 规范制定的格式。
fputs($fp, "Authorization: Basic $authorization\r\n");
在socket连接过程中,验证头始终都和HTTP头部一起发送。
客户端验证信息也可以和一般的消息一起发送。因为增加一个验证信息对客户端代码来说并不是多大的负担。请记住,即便使用了SSL,URL仍然是以明文传输的,也就是说如果验证信息是通过URL传递的(REST请求),那么它对传输途中的任何人都是可见的。
优点:
- 处理简单 – 每次处理都进行验证,如果一个普通页面。
- 容易编码 – 程序员只需要增加一个额外的参数(验证信息)
- 方便跟踪 – 可以容易地配置统计某一段时间里有多少次调用,根据情况进行限流。
- 验证信息是明文 – REST APIs会将验证信息以明文发送无论是否使用了安全的接入点。不安全的接入点也会把验证信息以明文格式发送,无论是REST还是SOAP。
- 没有加密 – 所有请求和响应都是对可见的。
基于消息的验证和HTTP验证很相似,最主要的不同是由谁来处理验证过程,HTTP是通过服务器,而基于消息的验证则是通过API本身。
切记验证信息应该和站内的其他验证信息分开。下面是一个基本的验证函数。
function checkUser($username, $password)
{
$query = "SELECT `user_level` FROM `users` WHERE `username` = ‘$username' AND
`password` = '$password'";
$results = getAssoc($query, 1);
return $results[‘level'];
}
取决于服务器端使用的是SOAP还是REST,这部分在后面的“REST?还是SOAP”中有讨论。
配置web服务器支持SSL连接可以保护请求和响应内容,同时又不需要额外编码。服务器端证书认证只能用来确认服务器而不能用来识别验证客户端。所以仍然需要前面两种验证机制。
优点:
- 加密 – 请求和响应内容都被加密,从而不会存在被监听的问题。
- 服务器验证 – 客户端可以根据SSL证书来确认服务端没有改变。使用签名机构的证书同样可以达到这样的目的。
- 配置简单 – 没有额外编码,只需要配置web服务器。
- 增加了服务器负担 – 加密和解密的过程本身就需要占用较多的CPU资源,每个请求都会需要一个额外的握手过程来建立安全套接字。
- 没有客户端验证 – 使用了SSL。
- 增加了客户端负担 – 处理SSL对客户端来说通常都会比较繁琐,可能需要安装相应的扩展包才能实现(在共享主机环境下可能会比较麻烦)。
SSL对任何API都是很重要的安全机制。它提供了对请求和响应内容的保护,以及服务器的识别。并且很容易和HTTP认证或基于消息的认证结合。但是也要根据特定情况决定是否使用SSL,因为它毕竟会增加一些负担给服务器和客户端。
不需要代码,只需要配置web服务器。
要连接SSL接入端,PHP需要配置“--with-openssl”选项。同时要看服务器是否支持SSL,从phpinfo()输出中看是否在Registerd PHP Streams有“https”。
API服务器可以为每个客户端生成一个证书来建立安全的信息传输,然后这个证书会被用在验证过程,确认服务器和客户端身份。尽管这种方法提供了最高级别的安全,但它也是要求对服务器端和客户端最严格的:并非所有的工具(比如NuSOAP)都支持客户端证书。
优点:
- 身份确认- 服务器和客户端的身份都得到了确认。
- 加密 – 请求和响应内容都被保护,不会被第三方监听。
缺点:
- 增加了处理 – 因为需要建立安全套件字,额外的工作不可避免。
- 服务端额外负担 – 服务器需要为每一个客户端都生成证书,并且要确保这些API必须要存放安全的地方,通过安全的传输通道。
客户端证书确保了客户端和服务端的身份,可以一定程度上保证API的安全,但是它的代价也是很明显的 - 客户端和服务器端的CPU的负担,并且无法使用一些方便的客户端工具与API交互。
设置客户端证书需要一定量的工作,你很难说服所有试图使用API的客户去花钱申请自己的证书。你也可以提供授权给客户,但这样也有一些不安全的问题 – 你必须要像一个真正的授权机构一样小心。但这仍然是一个相对轻松的解决办法,接下来我们详细介绍一下:
- 设置证书授权。最好是使用一个独立的服务器来保存CA的私钥,而不是用web服务器,理想情况下,这台服务器最好不要接入互联网,而只可以连接请求密钥的机器,因为API需要不停请求它生成密钥。
- 为你的用户生成密钥,同时使用安全的连接分发。如果使用不安全的方式去分发,比如HTTP,甚至email,那么还不如不生成。因为完全没有必要去生成。
- 配置web服务器只接受生成的证书,用户必须提供由你签名的证书。
- 配置服务器使用SSL认证。
OpenSSL可以很好的完成这项工作,它包含两个很重要的脚本,CA.pl和CA.sh,可以自动完成这个过程。其他类似OpenCA或者TinyCA提供了界面更友好的方案。 考虑到你只需要做一次,那么太过友好的界面并不是很必要。
首先需要修改CA脚本,其中有一行代码:
$DAYS="-days 365"
定义了证书的有效期为一年以内(365天),如果觉得太短,可以设置一个大一点的值,比如10年。.
其次,运行脚本时需要回答有关于公司的问题, common name这一项很重要- 它应该被配置为API的主机名,记得使用一个健壮的密码。脚本执行时会生成一个demoCA的文件夹,里面放置了所有生成好的文件。
最后,生成服务器端证书。分两步,第一步,生成认证签名请求(Certificate Signing Request, CSR);第二部,使用CA对CSR进行签名。下面是用openssl生成CSR的命令:
openssl req -new –key server.key –out server.csr
openSSL在生成密钥之前会问一系列问题,这些需要和你创建CA时的信息匹配,以免增加不必要的额外配置。要为生成的CSR签名,首先需要把CSR文件(server.csr)重命名为newreq.pem,然后使用下面这条命令:
CA.sh –signreq
它会为请求签名,最后必须要配置Apache服务器使用证书,需要修改虚拟主机的httpd.conf文件,增加如下信息:
SSLEngine On
SSLCertificateFile /etc/http/conf/ssl/server.crt
SSLCertificateKeyFile /etc/http/conf/ssl/server.key
SSLProtocol All –SSLv2
SSLCipherSuite ALL:!EXP:!NULL:!ADH:!LOW
前三行用来打开SSL引擎、设置证书路径和服务器私钥路径。最后两行用来阻止SSL使用不安全的协议。
第四,生成客户端证书:
CA.sh –newreq
CA.sh –signreq
记得通过安全的渠道分发给用户。
第五,配置web服务器要求客户在试图建立连接时提供证书,同时确保证书是由你签名和创建的。
SSLCACertificateFile /etc/http/conf/ssl/demoCA.crt
SSLCARevocationFile /etc/http/conf/ssl/demoCA.crl
SSLVerifyClient require
SSLVerifyDepth
SSLCACertificateFile 要指向你放置CA证书文件的位置, SSLRevocationFile 要指向.crl文件(该文件是和*.crt文件同时生成的),没有这个文件,你就无法取消非法用户(或者不在使用API的用户)。SSLVerifyClient 指明所有用户必须提供证书,否则将拒绝连接。最后, SSLVerifyDepth 1 指定所有客户端证书必须是由你的CA直接生成的,这避免了其他用户创建合法的客户端证书。最后要重启Apache来使这些配置生效。
|
注意 |
建议阅读Ivan Ristic的《Apache Security》了解更多的关于SSL和Apache相关的安全事宜。 |
要连接有安全的接入端,PHP需要配置“--with-openssl” 选项,同时要确定phpinfo()的输出中有SSL支持(Registered PHP Stream列表),并且在Registered Stream Socket Transports 中也存在SSL项。
因为必须要提供客户端证书,所以还要使用cURL库。它适合用来调用SOAP和REST API,处理请求和响应都很类似:
function callAPI($endpoint, $requestBody)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($ch, CURLOPT_SSLCERT, "../certs/cert_key_pem-1.txt");
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $requestBody);
ob_start();
curl_exec($ch);
$response = ob_get_clean();
if (curl_error($ch))
{
file_put_contents("/tmp/curl_error_log.txt", curl_errno($ch) . ": ".
curl_error($ch), "a+");
curl_close($ch);
return null;
}else
{
curl_close($ch);
return $response;
}
}
这个函数使用客户端证书来建立连接,然后使用输出缓冲来获取响应内容(cURL把输出直接发送给浏览器)。如果出现错误,响应出错信息会记录到文件;如果成功,响应结果会返回给调用函数。客户端证书切记要存放到安全的地方(web文档根目录之外的位置)。


