规划你的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本身。

缺点:

  • 缺少控制 任何人在任何地方都可以调用API,尽管这是web服务的目标,但可能会在潮水般的请求涌来时失去控制。如果这些请求只是来自一部机器,还可以借助防火墙来搞定,但是如果分布很广,处理起来就会很痛苦。
  • 没有加密 所有请求端和服务端的请求和响应都是对任何人都可见的。
  • 无法接触到开发者 因为API的调用不存在注册过程,也就无法联络到相应的开发者。而你可以通过注册机制建立一个与开发者的一个很好的联系。比如,告知他的应用正在被误用,API有新的改动,征求改进建议等等。
  • 误用 很不幸,总有一些人会利用这一点去做一些不好的事,即便你觉得这个可能性很小。

因为这些问题,完全开放的API只适用于用来请求信息,而不是发布信息 也就是请求的信息资源产生过程不会占用太多CPU资源。一个很恰当的例子是国家天气服务API,它只接受信息请求,并且这些请求可以全天候的缓存在服务器上。如果是需要发布信息,那么相应的验证机制要被用来识别请求者,当请求需要消耗大量CPU时,远端程序需要被识别出来,从而对发来的请求进行过滤和控制。

HTTP 验证

通过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下载。要使用BerkeleyDBPHP需要配置“-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头部一起发送。

基于消息的验证

客户端验证信息也可以和一般的消息一起发送。因为增加一个验证信息对客户端代码来说并不是多大的负担。请记住,即便使用了SSLURL仍然是以明文传输的,也就是说如果验证信息是通过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”中有讨论。

SSL 接入端

配置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的客户去花钱申请自己的证书。你也可以提供授权给客户,但这样也有一些不安全的问题 你必须要像一个真正的授权机构一样小心。但这仍然是一个相对轻松的解决办法,接下来我们详细介绍一下:

  1. 设置证书授权。最好是使用一个独立的服务器来保存CA的私钥,而不是用web服务器,理想情况下,这台服务器最好不要接入互联网,而只可以连接请求密钥的机器,因为API需要不停请求它生成密钥。
  2. 为你的用户生成密钥,同时使用安全的连接分发。如果使用不安全的方式去分发,比如HTTP,甚至email,那么还不如不生成。因为完全没有必要去生成。
  3. 配置web服务器只接受生成的证书,用户必须提供由你签名的证书。
  4. 配置服务器使用SSL认证。

建立你的认证(CA

OpenSSL可以很好的完成这项工作,它包含两个很重要的脚本,CA.plCA.sh,可以自动完成这个过程。其他类似OpenCA或者TinyCA提供了界面更友好的方案。 考虑到你只需要做一次,那么太过友好的界面并不是很必要。

首先需要修改CA脚本,其中有一行代码:

$DAYS="-days 365"

定义了证书的有效期为一年以内(365天),如果觉得太短,可以设置一个大一点的值,比如10年。.

其次,运行脚本时需要回答有关于公司的问题, common name这一项很重要- 它应该被配置为API的主机名,记得使用一个健壮的密码。脚本执行时会生成一个demoCA的文件夹,里面放置了所有生成好的文件。

最后,生成服务器端证书。分两步,第一步,生成认证签名请求(Certificate Signing Request, CSR);第二部,使用CACSR进行签名。下面是用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》了解更多的关于SSLApache相关的安全事宜。

客户端代码

要连接有安全的接入端,PHP需要配置“--with-openssl” 选项,同时要确定phpinfo()的输出中有SSL支持(Registered PHP Stream列表),并且在Registered Stream Socket Transports 中也存在SSL项。

因为必须要提供客户端证书,所以还要使用cURL库。它适合用来调用SOAPREST 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文档根目录之外的位置)。

Leave a Reply

Spam Protection by WP-SpamFree