为 PHP REST API 实现简单的身份验证

新手上路,请多包涵

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

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

我一直在阅读这方面的常见设计,OAuth1/2 看起来像是最常见的标准。然而,这对我来说似乎有点矫枉过正,因为它有各种我不需要的功能,而且实施起来似乎非常复杂。

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

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

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

原文由 Nils 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 519
1 个回答

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

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

  1. 当提供 API 帐户时,会生成 2 个随机值:一个令牌和一个秘密。
  2. 当客户提出请求时,他们提供:
    • 令牌,明文形式。
    • 根据唯一但已知的值和 Secret 计算得出的值。例如: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");

原文由 Sammitch 发布,翻译遵循 CC BY-SA 4.0 许可协议

推荐问题
logo
Stack Overflow 翻译
子站问答
访问
宣传栏