本文讲的是
如何创建一个安全的API(一),
毫无疑问,我们生活的世界正在随着时间的前行联系越来越紧密。无数新服务的出现都在帮助我们去使生活变得更简单,更愉快,而在这些所有变化的背后,服务们在进行组合交流所使用的是一项技术—API驱动架构。这些API能够允许服务在程序级别进行交流,并且可以调用这个服务中心的各种服务(每一种服务就是一个函数)帮助应用程序达到开启视窗、描绘图形和使用周边设备等目的。
如果你在过去的七八年里一直在进行着应用程序的开发,那么你可能会遇到各种各样新颖的API。或者说你可能需要为你们公司构建一个,又或者说你正在创业,而API正是你的产品。到现在来看,俨然如果你没有API,那么你的服务已经落后于这个时代了。
然而,并不是所有人都会对API的开发抱持开放的态度。前端开发人员可能会在开发的过程中发现使用API会更加的舒服,但在其进行面向客户的开发和功能设计方面时深入了解它然后感受到其深度所在之后,他们会发现虽然其基础知识仍然是那些((如HTTP请求/响应和输入处理),但是在实现的过程,却难免有些担忧——虽然其从外观上来看没什么问题,但内部却需要共享数据。
对于一个刚接触到API的开发人员而言,他们会倾向于做出纯粹去进行工作的东西—在特定的端点上接受数据请求,并对请求进行匹配的回复。这毫无疑问是我们的最终的目的,但其实这在创建API的过程中还有一件事非常重要:做出选择并做好保护工作。事实上,很多人都会很直接的认为,因为它是一个API,所以它只有跟程序才会交流,所以对它的安全性要求可能比其他面向用户的程序更为宽松。
首先让我们来谈谈有关保护API的一些基础知识。在开始之前我必须要说一件事—我在接下来所要展示的方法并不是唯一的去保护API的方式,而我其实也是希望通过这一方法的讲解来说明一些基础的API安全概念。我们将使用的方法是使用“共享密钥”系统,在系统中API和客户端都具有用于认证和发送消息的令牌。我找到了一些实现类似功能的系统,并利用它对每个请求使用一个静态标记。但是,如果攻击者发现了这个令牌,这可能会导致严重的问题—攻击者可以模拟用户的消息并潜在地绕过验证机制。
因此我选择了一个多重令牌,可撤销的系统而不是一个单一的令牌系统,它可以让用户创建他们想要的令牌,描述他们的使用,除此之外另一个主要的好处是可以随时撤销它们。并且如果你是GitHub用户,那么你一定熟悉它的令牌系统。而我们选择的这一系统与它有着类似的工作方式,只是需要添加更多元数据。
除此之外,如果你熟悉OAuth v2处理,那么这个设置对你来说肯定也很熟悉。该系统与OAuth处理在秘密令牌和前后越区切换上。另外,请记住OAuth的设计不是主要用于身份验证的,其主要目的是授权。例如,在网站上看到的所有“使用Twitter登录”功能,当你点击该按钮时,应用程序会将你返回到Twitter站点并要求批准,当你点击“允许”按钮之后,流程继续。之后就是由OAuth来继续去处理这一请求了。通过使用该流程,你会授权应用程序使用你的Twitter信息来识别你。但是与此同时你也会通过Twitter 验证身份来登录你的帐户。而另一个应用程序不知道你是否真的这么做了,但他们已经接受了这一风险,并将这一决定移交给了Twitter。
在这里我必须做出一个警告:在使用像这样的基于令牌的系统时,请确保所请求的URL中没有使令牌结束的内容。默认情况下,URL会从Web服务器日志中请求记录。如果你的令牌(特别是长寿命的)在URL中,那么所有的攻击者必须要做的是攻击你的日志存储位置,来访问他们想要的所有令牌。
在我们将要创建的系统中,还将有另一种方式来帮助提高他们的安全性 – 限制他们有效的时间。我在这里谈论的令牌是认证后的请求中使用的令牌,而不是用于身份验证的令牌。通过限制这些令牌“活”的时间,我们能够减少攻击者被拦截和重新使用的可能性。在我们的系统中,超时将限制为1小时。这通常足以让用户能够使用API成功运行,并为请求提供足够的保护。
当令牌过期时,客户端将被发送一个关于认证失败的消息,这时就需要请求一个新的令牌。在OAuth的世界里,他们发送一个“刷新令牌”,这是专门用于来“refetch”请求的,但是由于我们在这里要做的是一个更简单,更轻便的版本,我们只需坚持要求手动获取新的令牌即可。
基本流程
我其实已经暗示了这个API是如何运作的,但是我还是打算花一些时间来遍历正常会话的流程,包括身份验证以及剩余的请求。
1.令牌制作
事实上,当用户进入应用程序的管理部分后这一进程就开始了,然后就会生成用于身份验证过程的新令牌。该令牌将是一个随机的字符串,数字和符号,在认证请求期间(并且仅用于)使用。
2.认证
一旦我们拥有令牌,用户将向API上的端点发出一个请求POST及其凭证:用户名和密码的令牌。但正如前所述,我相信你也不会希望将此请求GET作为凭据信息显示在服务器日志中,因为这可能会造成非常不好的结果。
对令牌和所提供的用户名进行查找,并确保匹配。如果一切都很好,响应将包含我们随机生成的时间限制令牌,用于当前会话。该令牌用作以下请求中的标识符,并确保正在发送的消息未被篡改(稍后更多)。如果用户在该令牌仍然有效的时间内执行另一个认证请求,则每次都会给予一个新令牌,以进一步降低潜在攻击者的令牌拦截和重用的风险。
3.请求
有了这个令牌,客户端就可以向API中的其余端点发出请求。我们的系统目前不会执行任何权限,只有认证的网关。在一个真正的API中,需要一个更复杂的系统来帮助保护各个端点,并确保只有使用它们的用户才能使用。
要发出请求,客户端必须执行几个任务:
发送认证所产生的令牌在头文件中调用 X-Token
创建要发送的消息的内容的HMAC散列,并将其包括在X-Token-Hash头中
使用PHP的话,第二步只要使用hash_hmac方法就可以很容易的办到:
<?php
$body = json_encode(['foo' => 'bar']);
$messageHash = hash_hmac('SHA512', $body, $hash.time());
?>
在上面的代码中,我们正在创建自身内容的SHA512 HMAC散列,并使用$hash身份验证请求以及当前时间(以秒为单位)作为关键字。当消息回到服务器时,这个哈希值是根据相同的信息重新创建的,并且与X-Token-Hash标题中发送的值进行比较。如果系统不匹配,系统会知道消息的内容已被更改,并可以完全拒绝。
聪明的读者可能已经注意到这个请求处理的一个小问题 – 当前时间是以秒为单位的。在这之前,我已经发现对于API来说这已经成为了一个问题,就因为他们的用户在全世界广泛传播,而有时他们的系统上的时钟却不能正确同步。因此,无论发送什么,请求均会出现“不良请求”消息。为了帮助解决这种情况,你可以使用分钟代替秒来修改被使用的时间值使其“稍长一点”。包含时间的要点是提供一些随机性来生成HMAC哈希值,因此每个请求都不一样。如果把它归结到第二个问题里可能会造成太多的问题,因此需要稍等一下:
<?php
$body = json_encode(['foo' => 'bar']);
$time = date('ymdHi');
$messageHash = hash_hmac('SHA512', $body, $hash.$time);
?>
你基本上只需要在请求的生命内添加59秒,虽然这不太安全,但如果这意味着更多用户的请求正在通过,这是一个公平的决策。只要记住将其标记为你身边的被接受的风险,这样就不会被遗忘。
工具
虽然这里提出的概念可以适用于任何语言和任何框架,完全由你来选择,但我却不得不选择其中之一来开始。由于我重点关注本系列中的PHP示例,所以我确定了一个简单的PHP框架——Slim框架。这个框架在技术上是一个“微框架”,旨在为开发人员提供最少的功能来制作网络应用程序。在其基础上,它只是一个简单的前端控制器,并附加路由器和请求/响应处理。它没有太多的其他东西。有几个优点捆绑在一起,但如果你正在寻找一个框架,你可以完全放松,并为你提供一切,那么Slim不是这样的。
然而,这种看起来简约的方法却会使我们的例子变得非常完美,特别是当API比起前端的Web应用程序更重要的是请求和响应周期时。Slim提供了我们需要设置一些基本路线并将其链接到我们的API功能的一切。
只要给出一个我们所说的简单的一个想法,一旦通过Composer安装,所有需要使其响应索引页面请求的应用程序是:
require_once 'vendor/autoload.php';
$app = new SlimApp();
$app->get('/', function() {
echo 'Hello world!';
});
$app->run();
在这个简化的结构中,我们将构建我们的API,并集成其他几个部分,以帮助保护它及其保护的数据。谈到其他部分,让我们来看看下一个在我们的请求/响应周期中给我们提供一些可重复使用的逻辑:中间件。
如果你不熟悉中间件的概念,这是一个很简单的概念来处理。我更多的是一个视觉学习者,所以我一直发现这个形象(从Slim框架借来的)有用:
如图所示,中间件的基本思想是围绕你的应用程序的主要部分的“包装”。它旨在提供以请求和响应处理为中心的附加功能。当然,它也可以做其他的事情,但大多数中间件在处理跨HTTP请求的数据流方面表现出色。该请求进入应用程序,通过中间件层,一旦内部处理完成,它将以相反的顺序返回出相同的中间件层。
这个中间件层是我们在示例应用程序中执行某些授权逻辑的地方。由于需要在每个请求上检查API访问级别,所以将其包装在中间件中是有意义的,并检查所需数据的传入请求。这种方法还允许我们在控制器和内部逻辑出现许多问题之前保护客户端。
接下来是我们将在API示例中使用数据库的两个软件包:Laravel Enloquent数据库层和Phinx迁移工具。如果你是PHP开发人员,那么你可能会听说过Laravel框架。这个框架在过去几年中人气大涨,究其原因就在于其易用性和“简单”的感觉,使得其已经获得了相当的追捧。虽然框架本身具有很多功能,远 比我们需要的这些示例更多,但是Eloquent包在这里仅仅只是用于处理数据库。
幸运的是,这个包可以在主Laravel框架之外使用,因为它是一个“胶囊”。有了这个,我们可以将Eloquent及其功能引入我们基于Slim的应用程序,并像Laravel应用程序一样使用它。你可以查看“Eloquent” 手册了解更多信息,以下是使用它的示例:
$links = Link::all();
$users = User::where(['active' => 1])->get();
$userLinks = User::find(1)->links;
它不仅允许直接提取记录,还可以搜索数据库信息并在模型之间建立关系,从而使你更容易在PHP中交叉引用数据,而不会对SQL造成困扰。
Phinx工具使我们能够进行可重复使用的数据库迁移。迁移基本上是一种自动执行SQL命令的方法。当你考虑开发人员面临的一些常见数据库问题时,迁移的真正好处是非常明显的:初始数据库设置的一致性,并保持整个团队同步。Phinx是一个基于PHP的工具,可以让你创建迁移,也可以用PHP编写来让你定义迁移的“up”和“down”逻辑:当事情发生变化时,自动删除。Phinx工具还将跟踪哪些迁移已经运行,如果更改有意想不到的后果,则允许回滚。
以下是创建表的Phinx迁移的示例:
<?php
use PhinxMigrationAbstractMigration;
class CreateSources extends AbstractMigration
{
public function change()
{
$table = $this->table('sources');
$table->addColumn('name', 'string')
->addColumn('type', 'string')
->addColumn('user_id', 'string')
->addColumn('source', 'string')
->addColumn('last_update', 'datetime', ['default' => 'CURRENT_TIMESTAMP'])
->addColumn('created_at', 'datetime')
->addcolumn('updated_at', 'datetime')
->create();
}
}
在这种情况下,我们创建一个sources表,其中包含用户引用的列,类型,名称和源值。你会注意到,在这个例子中没有一个具体up或者down的方法。随着更新版本的Phinx出现,该工具在背后利用magic已经实现了change方法。当你在编写一个change方法时,Phinx将尽力找出基于正在执行的操作(应用或回滚)与迁移有关的操作。在这个例子的情况中它比较简单:在up表上创建,在down表上删除。
还有一些其他随机的功能将被包括在随机数生成功能和自定义异常处理的方式中,但不要担心,这些都将被及时覆盖。
以上就是这个系列的第一部分的内容。我在本文中主要概述了API生态系统的当前状态、应用程序的基本流程,并列出了我们将在系列中使用的一些工具,以使magic发生。在系列的下一部分中,我们将花费一些时间通过规划我们的一些基本API功能来完成设置和对话。
原文发布时间为:2017年6月21日
本文作者:Change
本文来自云栖社区合作伙伴嘶吼,了解相关信息可以关注嘶吼网站。