且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

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

更新时间:2022-06-18 23:46:42

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

One of the major points of REST as a concept is to avoid the use of session state so that it's easier to scale the resources of your REST endpoint horizontally. If you plan on using PHP's $_SESSION as outlined in your question you're going to find yourself in a difficult position of having to implement shared session storage in the case you want to scale out.

虽然 OAuth 将是您想要执行的操作的首选方法,但完整的实现可能比您想投入的工作要多.但是,您可以做出一些半途而废的事情,并且仍然保持会话-较少的.您以前甚至可能见过类似的解决方案.

While OAuth would be the preferred method for what you want to do, a full implementation can be more work than you'd like to put in. However, you can carve out something of a half-measure, and still remain session-less. You've probably even seen similar solutions before.

  1. 在配置 API 帐户时生成 2 个随机值:令牌和秘密.
  2. 当客户提出请求时,他们会提供:
    • 明文形式的令牌.
    • 根据唯一但已知的值和 Secret 计算出的值.例如:HMAC 或加密签名

通过这种方式,您可以保持无会话"REST 的理想状态,而且您在交换的任何部分都不会真正传输 Secret.

In this way you maintain the "sessionless" REST ideal, and also you never actually transmit the Secret during any part of the exchange.

客户端示例:

$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);

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

It's worth noting that if decide to use a timestamp as a nonce you should only accept timestamps generated within the last few minutes to prevent against replay attacks. Most other authentication schemes will include additional components in the signed data such as the resource path, subsets of header data, etc to further lock down the signature to only apply to a single request.

当这个答案最初是在 2013 年编写时,JWT 还很新,[我没有听说过],但到 2020 年,它们已经牢固地确立了自己的实用性.下面是一个手动实现的例子来说明它们的简单性,但是有大量的库可以为您进行编码/解码/验证,可能已经融入您选择的框架中.

When this answer was originally written in 2013 JWTs were quite new, [and I hadn't heard of them] but as of 2020 they've solidly established their usefulness. Below is an example of a manual implementation to illustrate their simplicity, but there are squillions of libs out there that will do the encoding/decoding/validation for you, probably already baked into your framework of choice.

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.

and can be validated by anyone that adheres to the standard, which is pretty popular atm.

无论如何,大多数 API 将它们添加到标头中:

Anyhow, most APIs tack them into the headers as:

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