四、审查设计缺陷和安全威胁

在本章中,我们将回顾我们的工作,我们实现的端点,并将研究我们当前工作可以改进和应该改进的两个不同方面。我们还将研究:

  • 我们的代码结构和设计缺陷
  • 安全威胁以及我们如何减轻这些威胁

然后,我们将通过前面两节中讨论的改进来研究实现 RESTful API 的方法。

在当前代码中查找问题

到目前为止,我们已经编写了博客文章中与端点相关的代码,我让您对与注释相关的端点进行同样的处理。如果您还没有这样做,那么我坚持您先这样做,或者至少尝试这样做,因为没有实践,它不会持续太长时间,所以至少在提供一些代码示例或有一些任务要做时,请继续实践。

无论如何,在上一章中,我们已经编写了实现 RESTfulWeb 服务端点的代码,我们将深入研究这一点,并确定缺失的内容以及需要改进的类型。

结构和设计缺陷

现在在我们的代码中,有一些我们可以非常清楚地识别的缺陷。

缺少查询生成器层

尽管我们正在使用 PDO,但我们仍然需要编写查询并执行许多低级操作,例如了解 SQL 注入(因此我们必须使用 prepare 语句,然后绑定值),以执行与数据库相关的操作。我们应该使用某种查询生成器层,它可以为我们进行查询。因此,一旦我们有了这个层,就不需要一次又一次地编写 SQL 查询。

虽然 PDO 可以轻松地将一个数据库连接交换到另一个数据库连接,但仍有一些 SQL 查询需要针对不同的数据库进行更改。事实上,它不仅有利于更改 DBMS,而且拥有一种查询生成器也可以节省时间,因为使用查询生成器时,我们并不总是处理字符串来构建查询,因为使用查询生成器时,我们也可以使用数组或关联数组来构建查询。

不完全路由器

我们实现的路由器只是路由不同的文件,比如通过posts.php服务的/posts。我们的路由器没有指定posts.php的哪个功能将服务于该请求。我们是从posts.php内部根据 URL 模式指定的。只是提醒您,以下是posts.php的条件部分:

if($url == '/posts' && $_SERVER['REQUEST_METHOD'] == 'GET') {
    $posts = getAllPosts($dbConn);
    echo json_encode($posts);
}

这样做并不困难。我们可以简单地将这些条件放在router.php中,并在posts.php中调用适当的函数。但是,如果您还记得我们的routes.php文件,它是一个非常简单的文件,具有键值对。为了方便起见,再次将其放在此处:

<?php

$routes = [
    'posts' => 'posts.php',
    'comments' => 'comments.php'
];

如您所见,我们没有在routes.php中的任何地方指定请求方法,因此我们也需要在routes.php中指定该方法。除此之外,我们还需要在routes.php中使用正则表达式,而不是普通 URL。在routes.php中这样做很容易,但我们需要添加实现的实际位置将是core/router.php。这是可以做到的,但我们不会这样做。我们不会从头开始制造路由器之类的组件,因为这并不是世界上第一次这样做。那我们怎么做呢?我们可以使用已经可用的开源组件或包中的路由器。稍后,我们将看到如何重用现有的开源软件包或组件。

OOP 的使用

我们应该使用面向对象的范例,因为它不仅有助于使代码更好、更干净,而且随着时间的推移,它还可以使开发更快,因为干净的代码减少了我们编写更多功能代码或修改代码时遇到的摩擦。

将配置与实现分开

配置应该更好。我们有一个包含数据库连接信息的config文件是很好的,但是还有很多其他的东西需要配置,比如,是否显示错误应该通过config文件来控制。

因此,经验法则是,我们应该将配置与实现分开。这一点很重要,这样我们就可以随时更改配置,而不必担心负责逻辑等的代码的实现。

应该编写测试

无论您是在编写 RESTful web 服务还是制作网站,编写测试用例都是非常重要的。为此,代码也必须是可测试的。因此,测试(单元测试)不仅根据需求测试代码,还检查代码是否足够灵活和松散耦合。与松散耦合的代码相比,紧密耦合的代码不可能有那么多的可测试性。

用代码编写测试用例还可以使代码更干净、更灵活,并且易于修改。对于 web 服务,API 测试也很方便。

输入验证

如上一章所述,虽然我们避免了 SQL 注入,但我们没有验证来自输入源的数据,因为我们使用的是 PDOprepare()bindValue()方法。这是因为我们只在上一章中编写代码来理解和学习。否则,没有输入验证不仅不方便,而且对应用程序来说也是不安全的。

要应用验证,我们可以使用手动检查,也可以编写一个验证器,在这里我们可以简单地传递输入参数并根据特定规则进行检查。这种类型的验证器非常方便,但是编写一个好的验证器也需要时间,所以最好使用已经存在的开源验证器。如您所见,我们试图编写一个路由器,然后发现了问题。这些问题可以解决,但我们需要编写更多的代码,编写更多的代码需要时间。

在后面的章节中,我们将看到如何使用其他人编写的验证器,并将使用它来创建 RESTfulWeb 服务端点。我们不仅在努力节省编写代码的时间,而且在努力避免编写更多的代码,这些代码的维护将成为我们的责任。

处理 404 和其他错误

现在,如果博客文章或评论的 URL 或 ID 错误,我们还没有处理 404,因此我们需要处理这一问题,不仅要发送 not found 错误,还要发送 HTTP 状态代码 404。因此,对于不同的响应,我们需要发送不同的 HTTP 状态代码。

元信息缺失

现在,没有记录计数,也没有分页。所有记录都显示在那里。所以如果有很多记录,比如说几百万条记录,那么返回所有记录就没有意义了。在这种情况下,我们应该应用分页,并且应该在适当的位置显示元信息。

数据库字段抽象

现在,来自数据库的所有数据都将按原样显示给用户。如果字段名将更改,并且客户端开发人员正在使用该 DB 字段,该怎么办?它也将在客户端开始给出一个错误。

如果您还记得,REST 的一个重要约束是服务器返回给客户机的内容与服务器实际存储数据的方式之间的抽象。所以,我们需要保持这种抽象。在接下来的章节中,我们将看到如何保持这种抽象。

安全

正如您所看到的,我们根本没有应用任何类型的安全性。事实上,我们并没有使所有端点都受到登录保护。但是,在现实世界中不可能没有登录或身份验证。因此,我们需要对一些端点进行登录保护。

在本章中,我们只将看到如何为端点实现安全性,但我们还不会实现,将在后面的章节中实现。现在我们正在研究如何保护一些资源登录,因为基于此,我们还可以识别其他安全风险。因此,在本章的下一节中,我们将看到身份验证将如何工作。

保护 API 端点

首先,我们需要了解身份验证和登录是如何工作的。客户端应用程序第一次发送登录凭据(主要是电子邮件地址和密码)。基于这些凭据,服务器端登录端点进行用户登录,并针对已验证的用户返回令牌。该令牌存储在客户端。在每个请求中,客户端在请求主体或请求头中都有该令牌。在下图中可以更清楚地看到。

第一个客户端将使用登录凭据访问服务器上的登录端点:

一旦客户端获得令牌,客户端将存储它以供以后使用。然后,在每次请求时,客户端将发送相同的令牌,以便服务器可以将客户端视为已认证的:

当服务器发现客户机经过身份验证时,它将基于经过身份验证的用户返回数据。

若并没有令牌和只允许经过身份验证的用户的请求一起发送,那个么服务器应该返回 401HTTP 状态码,该状态码是未经身份验证或未经授权的。

例如,考虑 PoT T0。有创建帖子、修改帖子、删除帖子等端点;这些需要被保护,所以应该有一个身份验证中间件来保护这些端点,而其他端点,如 show post 和 list post 以及一些其他基于GET的端点不应该受到登录保护,所以身份验证中间件应该用于受保护的端点。详情如下:

如图所示,服务器将根据提供的身份验证令牌进行响应,身份验证中间件正好在那里从身份验证令牌解析用户。但是,如果身份验证中间件无法从身份验证令牌解析用户,它只会返回 401 未经授权的错误。

什么是 Auth 中间件?

身份验证中间件只不过是验证身份验证令牌并尝试根据该身份验证令牌解析用户的一段代码。它只是一段代码,将附加到路由中的某些端点或从中返回端点数据的位置。在任何情况下,它都将在端点的实际代码之前执行,并将从请求中的auth令牌验证和解析用户。

第 6 章用 Lumen照亮 RESTful Web 服务,我们将研究中间件,在第 7 章改进 RESTful Web 服务中,我们将为认证中间件编写代码。

RESTful web 服务中常见的安全威胁

因为我们已经研究了当前代码中的问题以及我们将如何在一些端点中实现安全性并使用认证中间件,现在是时候看到在构建 REST Web 服务时需要考虑的常见安全威胁。

HTTPS 的使用

HTTPS 是带有 SSL 的 HTTP。由于我们的数据通过互联网传输,我们需要确保我们的连接安全;因此,我们应该使用 HTTPS。HTTPS 的目的是确保服务器就是它声称的服务器,并且数据通过加密形式的安全连接在客户端和服务器之间传输。

如果您不想购买 SSL 证书,因为它对您来说很昂贵,那么您只需购买https://letsencrypt.org/. 我们加密是一个免费的证书颁发机构。因此,您无需支付 SSL 证书即可使用它。

保护 API 密钥/令牌

由于我们的会话将基于令牌,因此我们需要保护该身份验证令牌。为此,需要做不同的事情:

  1. 未在 URL 中传递访问令牌。
  2. 访问令牌过期。

未在 URL 中传递访问令牌

API 密钥或令牌或任何需要发送到服务器的敏感信息不应在 URL 中传递,因为这可以在 web 服务器日志中捕获。因此,它必须在 POST 正文或请求头中传递。

访问令牌到期

在两种情况下,访问令牌应过期。首先,它应该在注销时过期。第二,访问令牌应该在一段固定的时间后过期,并且这个持续时间不应该太长。使令牌过期的原因是使访问令牌的有效时间更短更安全。如果我们有许多未使用的访问令牌,那么这些令牌被误用的可能性就更大。

有效期约为两小时或更短。尽管这取决于您想要如何实现它,但更短的过期期限更安全。过期并不意味着用户需要再次登录,而是会有一个令牌刷新端点。将使用最后一个过期的令牌对特定用户进行攻击,以获得新的令牌。请注意,最后一个令牌应该在有限的时间内可用于刷新令牌端点,之后,最后一个令牌不应可用于刷新令牌。否则,让代币过期有什么意义呢。记住,这两种方法之间都有权衡。每个请求上的刷新令牌更安全,但会给服务器带来更多开销。因此,在您的场景中,选择哪种方式始终取决于您。

使令牌过期的另一种方法不是按时间使其过期,而是在每次请求时刷新令牌。例如,如果使用一个令牌发送请求,服务器将验证该令牌,刷新令牌,并发送一个新令牌作为响应。因此,旧令牌将不可用。令牌将在每次请求时刷新。这可以通过两种方式实现;你喜欢什么取决于你自己。

有限范围访问令牌

限制访问令牌的范围也是一个好主意,以避免在未经授权的人获得令牌时出现问题。此外,如果向客户端应用程序提供的服务不是特定于某个用户或访问的,那么它仍然应该具有某种 API 密钥,通过该密钥我们可以识别谁在请求信息。因此,如果有可疑的尝试使用某个 API 密钥访问 API 端点,我们可以简单地撤销特定的 API 密钥,这样它将不再对未来的请求有效。只有当存在多个具有限制访问级别的 API 密钥时,才有可能。

公共和私有端点

就像公共 web 页面一样,我们也可以为 RESTful web 服务提供公共端点。身份验证之前用户可用的所有端点都不是公共的。有时,我们创建的端点在登录前或未登录时都是开放的,但它们只能通过我们的应用程序访问。这些端点不是公共的,因此我们不希望通过其他应用程序访问这些端点。为此,我们将使用某种 API 密钥,如前所述。

我们可以使用基于oauth2的访问令牌。使用oauth2访问令牌的一大优势是,如果我们要让不同的应用程序访问相似的端点,那么我们可以为不同的应用程序使用不同的访问令牌。

示例:我们可以将在线书店 API 公开为 RESTful web 服务,我们可以有两个应用程序:

  • 面向客户的图书销售app.
  • 教师选书app.

现在,通过客户的app.,用户可以浏览不同的书籍,并添加到购物车和购买。而在教师app.中,用户可以浏览并选择不同的书籍转发给稍后购买书籍的人。这两个不同的应用程序。将有一些共同的端点和一些彼此不同的端点。但是,我们不希望任何端点向所有人公开。因此,我们可以有两个不同的访问级别,并将使两个不同的移动apps.具有两个不同的 API 密钥,每个具有不同的访问级别。当用户登录时,我们将返回一个访问受限的访问令牌。根据用户角色,不同的令牌可以具有不同的访问级别。

比如说在app.老师中,有的老师只能选书,有的老师,比如HOD系主任)也可以买书。因此,在登录后,这两个用户都可以将不同的访问令牌转换为不同的访问级别。该访问级别将基于访问令牌,该令牌将被转换为登录的用户,我们将从该用户获得一个角色,并根据该角色决定访问级别。

公共 API 端点

因此,即使在登录之前,这些端点也是私有的。如果我们有一些 API 端点是公共的,比如一个天气预报,它向每个人提供预测数据。最好还是有一个 API 密钥来跟踪谁正在向服务器获取数据,但如果不是这样,我们只是在没有任何 API 密钥的情况下提供数据,该怎么办?这是否意味着我们正在公开这些数据,所以我们不需要担心任何事情?事实上,没有。

如果客户机正在向服务器传递任何信息,那么最好使用 TLS 对数据进行加密。除此之外,我们也不能允许任何人继续到达终点;为了公平使用,我们需要应用节流,这意味着在特定的时间段内,一个客户端只能对一个 API 端点进行有限次数的访问。

不安全的直接对象引用

不安全的直接对象引用是指根据来自请求的数据获取或提供敏感信息。这不仅是 RESTful web 服务的问题,也是网站的问题。为了理解这一点,让我们考虑一个例子:

假设我们要更改用户的名字或帐单地址。最好将其引用到端点,例如:PATCH /api/users/me?fist_name=Ali (having token in header),而不是PATCH /api/users/2?fist_name=Ali (having token in header)

为了允许用户修改他/她自己的数据,它将在头中有一个令牌,服务器将通过该令牌确保该用户可以修改记录。但是,哪张唱片?在具有me的端点中,它将根据令牌获取一个用户,并修改其first_name

而在第二种情况下,我们有用户的id=2,因此可以基于用户id=2获取或更新用户,这是不安全的,因为用户可以在 URL 中传递任何用户 ID。因此,问题不在于这种类型的 URL,问题在于直接从用户输入或客户机请求获取或更新基于引用的记录。无论提供什么用户 ID,如果我们打算修改登录用户的名字,那么它应该基于令牌而不是 URL 中的用户 ID 获取或更新用户。

限制容许动词

我们需要限制允许使用的动词。例如,如果一个 web 服务端点仅用于读取目的,而不用于修改,那么在 URL/api/post/3上,我们应该只允许GET method/verb,但不应该允许PATCH. PUTDELETEPOST。如果有人用PATCHPUTDELETEPOST点击/api/post/3,则不应为其提供服务,而应返回“405 方法不允许”错误。

但是,如果他们的客户端有访问令牌,并且基于此,用户只允许使用GET方法(尽管有其他方法可用),而不允许使用其他方法,并且该用户的客户端与其他方法点击相同的 URL,则应出现“403 禁止”错误,因为根据当前用户的角色或权限,存在允许但不允许的方法。

输入验证

似乎输入验证可能与技术关系不大,但验证输入非常重要,因为它不仅有利于在数据库中保存干净的数据,而且有助于防范不同的威胁,如 XSS 和 SQL 注入。

实际上,XSS 预防和不同输入验证是输入验证的一个重要部分,而 SQL 注入主要是在数据库中输入数据时防止的。需要防止的另一种类型的威胁是 CSRF,但 API 密钥或身份验证令牌的使用已经可以防止这种威胁。但是,也可以使用单独的 CSRF 令牌。

可用的可重用代码

我们没有讨论每一个安全威胁,但是我们使用了一些需要注意的事情来避免与安全相关的问题。我们已经讨论了应该如何保护端点,以及如何为 RESTful web 服务实现身份验证。我们还讨论了上一章中编写的当前代码中的缺陷。

然而,我们还没有编写使代码更好、更安全的代码。我们可以做到这一点,但我们应该明白,已经有很多东西可以利用,而不是从头做起。因此,我们将使用可用的代码,而不是自己用普通 PHP 编写所有内容。这不仅是为了节省时间,而且是为了使用社区中可用的、经过社区时间考验的东西。

因此,如果我们已经决定使用第三方代码片段、包或类,那么我们应该了解,在 PHP 中,没有一组开发人员在一个框架中编写代码。有很多 PHP 类作为单独的类提供。有些是为某些框架编写的。有些是为开源 CMS 编写的,比如 WordPress。在PEARPHP 扩展和应用程序存储库中也有一些软件包可用。因此,一个地方可用的代码可能没有用处,或者与其他代码不兼容。

事实上,仅仅将不同的代码段加载在一起也可能是一个问题,特别是当存在大量依赖项时。

因此,PHP 社区迎来了一场革命。它不是框架、CMS 或开源类或扩展。它是 PHP 的依赖项管理器,称为 Composer。我们可以以标准方式安装 Composer 软件包,Composer 已经成为大多数 PHP 流行框架的标准。在这里我们将不详细讨论 Composer,因为 Composer 是下一章的主题,因此我们将详细讨论它,因为我们将大量使用 Composer 进行包安装、依赖项管理、自动加载等。不仅在这本书中,而且如果你打算用 PHP 制作任何合适的应用程序,你需要一个作曲家。因此,我们将使用的可重用代码将主要通过 Composer 包。

总结

我们已经讨论了当前代码中的问题和缺失部分以及安全威胁,并且讨论了如何实现身份验证。我们还讨论了将使用可重用组件或代码来节省时间和精力。此外,由于代码将由我们自己编写,我们将负责其维护和测试,因此使用开放源代码不仅可用,而且在许多情况下由社区进行测试和维护,这更有意义。为此,我们将主要使用 Composer,因为它已成为 PHP 中打包和使用可重用包的标准工具。

在下一章中,您将了解有关作曲家的更多信息。它是什么,如何工作,以及我们如何将其用于不同的目的。

We have talked about security threats in this chapter, but we have not covered them in lots of detail because we only had one chapter to discuss them. But, web application and RESTful Web service security is a wide topic. There is a lot more to learn about it. I would recommend you to go and check https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project as a starting point. There is a lot of stuff you will learn from there and you will learn from a different perspective.