为 PHP REST API 实现简单身份验证 [已关闭]

2022-08-31 00:44:51

我正在努力将 REST API 添加到旧版 PHP 站点。这是为了给内部应用提供一个端点,所以我在如何设计东西以及什么支持和不支持方面非常自由。

我现在需要添加到此 API 中的是一种登录方式,然后以特定用户身份执行操作。该网站是几年前建立的,不一定符合当时的最佳实践,所以不幸的是,我在如何做到这一点方面受到了一些限制。所有这些都需要在PHP 5.4和MySQL 5.6中运行。

我一直在阅读这方面的常见设计,OAuth1 /2看起来像最常见的标准。但是,对于我的目的来说,这似乎是巨大的过度杀戮,因为它具有我不需要的各种功能,并且实现起来似乎非常复杂。

相反,我打算做这样的事情:

  • 客户端调用 API 终结点,该终结点生成一个随机会话 ID,将其保存到数据库中的表中,并将其返回给客户端。get_session
  • 客户端保存此会话 ID。
  • 然后,客户端通过向端点发送请求,发送用户名,密码和会话ID(显然通过HTTPS)进行身份验证。login
  • 服务器将数据与用户表进行比较,如果登录名正确,则更新会话表以将会话 ID 与相应的用户 ID 相关联。这需要以某种方式限制速率,以防止暴力破解。
  • 现在,客户端可以调用任何其他终结点,仅提供其会话 ID 以进行授权。
  • 在每个请求上,服务器都会查找会话 ID,查看它与哪个用户关联并执行正确的操作。
  • 客户端可以记住会话 ID 以供将来使用,直到它被手动删除或在一段时间后过期。
  • 若要注销,客户端向终结点发送请求,服务器将删除与用户帐户的关联。logout

这是一个合理的设计吗?它显然不是很复杂,但我正在寻找一些可以实现的东西,而无需巨大的麻烦或需要第三方库。


答案 1

REST 作为一个概念的要点之一是避免使用会话状态,以便更轻松地水平扩展 REST 终结点的资源。如果您计划使用PHP,如问题中所述,您将发现自己处于一个困难的境地,即在您想要扩展的情况下必须实现共享会话存储。$_SESSION

虽然OAuth是您想要执行的操作的首选方法,但完整的实现可能比您想要投入的工作量更多。但是,您可以制定一些折衷措施,并且仍然保持无会话状态。您以前甚至可能见过类似的解决方案。

  1. 预配 API 帐户后,生成 2 个随机值:令牌和机密。
  2. 当客户端发出请求时,他们会提供:
    • 令牌,以明文形式显示。
    • 根据唯一但已知的值和机密计算的值。例如:HMAC或加密签名
  3. 然后,REST 终结点可以维护令牌和机密的简单、集中的键值存储,并通过计算值来验证请求。

通过这种方式,您可以保持“无会话”REST理想,并且在交换的任何部分都不会实际传输秘密。

客户端示例:

$token  = "Bmn0c8rQDJoGTibk";                 // base64_encode(random_bytes(12));
$secret = "yXWczx0LwgKInpMFfgh0gCYCA8EKbOnw"; // base64_encode(random_bytes(24));
$stamp  = "2017-10-12T23:54:50+00:00";        // date("c");
$sig    = hash_hmac('SHA256', $stamp, base64_decode($secret));
// Result: "1f3ff7b1165b36a18dd9d4c32a733b15c22f63f34283df7bd7de65a690cc6f21"

$request->addHeader("X-Auth-Token: $token");
$request->addHeader("X-Auth-Signature: $sig");
$request->addHeader("X-Auth-Timestamp: $stamp");

服务器示例:

$token  = $request->getToken();
$secret = $auth->getSecret($token);
$sig    = $request->getSignature();

$success = $auth->validateSignature($sig, $secret);

值得注意的是,如果决定使用时间戳作为随机数,您应该只接受过去几分钟内生成的时间戳,以防止重放攻击。大多数其他身份验证方案将在签名数据中包含其他组件,例如资源路径、标头数据子集等,以进一步锁定签名以仅应用于单个请求。

2020年编辑:这基本上就是JSON Web Tokens [JWT]是什么。

当这个答案最初写于2013年时,JWT是相当新的,[我没有听说过它们],但截至2020年,它们已经牢固地建立了它们的有用性。下面是一个手动实现的示例,以说明它们的简单性,但是那里有大量的库可以为您进行编码/解码/验证,可能已经融入了您选择的框架中。

function base64url_encode($data) {
  $b64 = base64_encode($data);
  if ($b64 === false) {
    return false;
  }
  $url = strtr($b64, '+/', '-_');
  return rtrim($url, '=');
}

$token  = "Bmn0c8rQDJoGTibk";                 // base64_encode(random_bytes(12));
$secret = "yXWczx0LwgKInpMFfgh0gCYCA8EKbOnw"; // base64_encode(random_bytes(24));

// RFC-defined structure
$header = [
    "alg" => "HS256",
    "typ" => "JWT"
];

// whatever you want
$payload = [
    "token" => $token,
    "stamp" => "2020-01-02T22:00:00+00:00"    // date("c")
];

$jwt = sprintf(
    "%s.%s",
    base64url_encode(json_encode($header)),
    base64url_encode(json_encode($payload))
);

$jwt = sprintf(
    "%s.%s",
    $jwt,
    base64url_encode(hash_hmac('SHA256', $jwt, base64_decode($secret), true))
);

var_dump($jwt);

收益 率:

string(167) "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbiI6IkJtbjBjOHJRREpvR1RpYmsiLCJzdGFtcCI6IjIwMjAtMDEtMDJUMjI6MDA6MDArMDA6MDAifQ.8kvuFR5xgvaTlOAzsshymHsJ9eRBVe-RE5qk1an_M_w"

并且可以由任何遵守该标准的人进行验证,这是非常受欢迎的atm。

无论如何,大多数 API 将它们附加到标头中,如下所示:

$request->addHeader("Authorization: Bearer $jwt");

答案 2

我会说你应该生成一个唯一的令牌,并将其用于通信。基本上:

  • 客户端将用户名/密码发送到资源。login
  • 服务器验证用户名/密码组合。如果正确,它将生成一个唯一的 toquen,将其保存在表中,然后将其发送回用户,同时进行状态更新,如 .sessionslogged_in = TRUE
  • 现在,用户发送的所有其他请求都应包含一个字段(作为字段或参数)。在这一点上,我会重新考虑使用REST,只使用所有内容的请求,并带有作为POST字段。这不会将令牌添加到URL中,因此,可以让它在浏览历史,路由器和东西的网页上注册。tokenPOSTGETPOSToperation
  • 在每个请求中,服务器应检查令牌是否存在以及它是否有效。如果没有,只需返回一条错误消息,如 和 。403 Forbiddenlogged_in = FALSE

系统还可能需要发送另一个数据以使其更安全,例如客户端生成的数据之类的东西,这些数据应该与令牌一起发送并检查服务器端。unique id


推荐