nimbly/proof

一个简单的库,能够对签名的JWT进行编码、解码和验证。

1.1 2023-05-11 22:08 UTC

This package is auto-updated.

Last update: 2024-09-08 16:16:47 UTC


README

Latest Stable Version GitHub Workflow Status Codecov branch License

一个简单的库,能够对签名的JWT进行编码、解码和验证。

要求

  • PHP 8.x
  • OpenSSL PHP 扩展

安装

composer require nimbly/proof

使用概述

实例化

使用您的 SignerInterface 实例创建一个新的 Proof 实例。签名者负责对您的JWT进行签名以防止对令牌的篡改。必须向 Proof 实例提供您的签名首选项:HmacSignerKeypairSigner(有关更多信息,请参阅 签名者 部分)。

$proof = new Proof(
    new KeypairSigner(
        Proof::ALGO_SHA256,
        \openssl_get_publickey($public_key_contents),
        \openssl_get_privatekey($private_key_contents)
    )
);

创建令牌

使用声明创建一个新的 Token

$token = new Token([
    "iss" => "customer-data-service",
    "sub" => $user->id,
    "iat" => \time(),
    "exp" => \strtotime("+1 hour")
]);

将令牌编码为JWT字符串

Token 实例编码为JWT字符串。

$jwt = $proof->encode($token);

将JWT字符串解码为令牌

将JWT字符串解码为 Token 实例。

$token = $proof->decode($jwt);

令牌

Token 实例表示JWT的有效负载,其中存储了有意义的应用程序级别数据。例如,令牌的 subject、过期时间戳等。这些数据称为 claim。有关预定义的公共声明的完整列表,请参阅https://www.iana.org/assignments/jwt/jwt.xhtml#claims。您也可以使用自己的自定义声明以满足您的需求。

创建令牌

创建 Token 实例时,可以通过构造函数作为简单的键 => 值对传递声明。

$token = new Token([
    "iss" => "customer-data-service",
    "sub" => $user->id,
    "custom_claim_foo" => "bar",
    "exp" => \strtotime("+1 hour")
])

或者,您可以通过调用 setClaim 方法在 Token 上设置声明。

$token->setClaim("nbf", \strtotime("+1 week"));

将令牌编码为JWT

使用 Token 实例,您可以通过将其传递给 encode 方法来将其编码为签名JWT。您将返回一个签名的JWT字符串。

$jwt = $proof->encode($token);

编码时的异常

当编码 Token 时,有多个失败点会抛出异常

  • 如果标题或有效负载无法正确进行JSON编码,则抛出 TokenEncodingException
  • 如果使用给定的 SignerInterface 实例签名JWT时出现问题,则抛出 SigningException

将JWT解码为令牌

当您解码JWT字符串时,它也会验证签名并检查过期(exp)和“不早于”(nbf)声明(如果存在)。如果成功,您将收到一个带有JWT有效负载声明的 Token 实例。

$token = $proof->decode($jwt);

您可以通过调用 getClaim 方法获取 Token 上的声明。

$subject = $token->getClaim("sub");

您可以通过调用 hasClaim 方法检查声明是否存在。

if( $token->hasClaim("sub") ){
    // Load User from DB
}

您可以通过调用 toArray 方法获取 Token 上的所有声明。

$claims = $token->toArray();

解码时的异常

当解码JWT时,有多个失败点会抛出异常

  • 如果JWT无法解码,因为格式不正确或包含无效的JSON,则抛出 InvalidTokenException
  • 如果签名不匹配,则抛出 SignatureMismatchException
  • 如果令牌的 exp 声明已过期,则抛出 ExpiredTokenException
  • 如果令牌的 nbf 声明尚未准备好(即时间戳仍在将来),则抛出 TokenNotReadyException

签名者

您需要一个 SignerInterface 实例来对JWT进行签名和验证。

HMAC

HmacSigner 使用共享密钥对消息进行签名并验证签名。与使用密钥对相比,这是一个不太安全的替代方案,因为用于签名的相同密钥值必须用于任何需要验证该签名的其他系统或服务。

$hmacSigner = new HmacSigner(
    Proof::ALGO_SHA256,
    $secretsManager->getSecret("jwt_signing_key")
);

在使用共享密钥时,请记住它应被视为高度敏感数据,因此不应将其保存在代码仓库(公开或私有)或部署在您的应用程序中。如果未经授权的第三方能够获取到您的共享密钥,他们将能够创建自己的令牌,这可能导致您的用户和系统的敏感数据泄露。如果您怀疑您的共享密钥已泄露,请立即生成新的共享密钥。

密钥对

KeypairSigner 是首选的签名方法,因为它比使用 HmacSigner 更安全。密钥对签名者依赖于使用一对私钥和公钥。私钥用于签名 JWT,而公钥只能用于验证签名。

KeypairSigner 依赖于 PHP 4.0 版本中通过 openssl 扩展/模块提供的 OpenSSLAsymmetricKey 的私钥和/或公钥实例。您可以使用 openssl_get_privatekeyopenssl_get_publickey PHP 函数来加载密钥。

私钥是可选的,只有在您需要签名新的令牌时才需要。公钥是可选的,只有在您需要验证令牌签名时才需要。

例如

$keypairSigner = new KeypairSigner(
    Proof::ALGO_SHA256,
    \openssl_get_publickey($secretsManager->getSecret("public_key")),
    \openssl_get_privatekey($secretsManager->getSecret("private_key"))
);

生成密钥对

如果您还没有,您可以使用大多数 Linux 系统上找到的 openssl 创建密钥对。

openssl genrsa -out private.pem 2048

使用刚刚创建的私钥文件(private.pem),输出一个公钥。

openssl rsa -in private.pem -outform PEM -pubout -out public.pem

您现在应该有两个名为 private.pempublic.pem 的文件。private.pem 文件是您的私钥,可以用来签名您的 JWT。public.pem 文件是您的公钥,只能用来验证您已签名 JWT 的签名。

在分布式或微服务架构中分离私钥和公钥特别有用,因为大多数服务只需要验证 JWT 而不生成自己的令牌。对于这些服务,您只需要公钥。

在创建密钥对时,请记住您的 私钥 应被视为高度敏感数据,因此不应将其保存在代码仓库(公开或私有)或部署在您的应用程序中。如果未经授权的第三方能够获取到您的私钥,他们将能够创建自己的令牌,这可能导致您的用户和系统的敏感数据泄露。如果您怀疑您的私钥已泄露,请立即生成新的密钥对。

支持多个密钥

JWT 允许在头部包含一个 kid(键 ID 的缩写)声明/属性以包含一个密钥 ID。此密钥 ID 应映射到单个唯一签名密钥。Proof 通过构造函数中的 keyMap 属性支持多个签名密钥:您提供一个字符串到 SignerInterface 实例的键/值对数组。

对于需要解码的 JWT,Proof 将检查头部是否存在 kid 属性,如果存在,将从中拉取匹配的 SignerInterface 实例。如果没有找到匹配项,将抛出 SignerNotFoundException。如果头部不包含 kid 属性,则使用默认签名者进行解码。

对于编码新的 JWT,您可以将可选的 kid 参数传递到 encode 方法。kid 的值 必须 在密钥映射中存在,并将用于签名令牌。如果没有找到匹配项,将抛出 SignerNotFoundException

如果您不传递 kid 参数,则编码将使用默认签名者完成。

$proof = new Proof(
    signer: $signer,
    keyMap: [
        "1234" => $signer,
        "5678" => $signer2
    ]
);

$proof->encode($token, "5678");

自定义签名者

如果您想要实现自己的自定义签名解决方案,提供了一个 Nimbly\Proof\SignerInterface。只需使用您自己的解决方案实现此接口,并将其传递到 Proof 构造函数。

PSR-15 中间件

Proof 搭载了一个 PSR-15 中间件,您可以在 HTTP 应用程序中使用它来验证来自 ServerRequestInterface 实例的 JWT。如果 JWT 有效,则会向 ServerRequestInterface 实例添加一个包含 Nimbly\Proof\Token 实例的 Nimbly\Proof\Token 属性。Token 实例可用于添加更多上下文到请求的进一步中间件,例如添加一个 User 实例。

如果 JWT 无效,将抛出异常。您需要根据您的应用需求处理这个异常。可能抛出的异常包括

  • 如果JWT无法解码,因为格式不正确或包含无效的JSON,则抛出 InvalidTokenException
  • 如果签名不匹配,则抛出 SignatureMismatchException
  • 如果令牌的 exp 声明已过期,则抛出 ExpiredTokenException
  • 如果令牌的 nbf 声明尚未准备好(即时间戳仍在将来),则抛出 TokenNotReadyException

默认情况下,中间件会在带有 Bearer 方案的 Authorization HTTP 头中查找 JWT。例如

Authorization: Bearer eyJhbGdvIjoiSFMyNTYiLCJ0eXAiOiJKV1QifQ.eyJtc2ciOiJJIHNlZSB5b3UgbG9va2luZyBpbnRvIHRoaXMgdG9rZW4hIn0.BnWZZhTs3ikgfxI7izf-2XVULbotriCPNJKxf9AYEKU

您可以在构造函数中通过提供头名称(不区分大小写)和方案(区分大小写)来覆盖此行为。如果没有方案,您可以使用 null 或空字符串值。

new Nimbly\Proof\Middleware\ValidateJwtMiddleware(
    proof: $proof,
    header: "X-Custom-Header",
    scheme: null
);

装饰 ServerRequestInterface 实例

一种常见的做法是为请求添加额外的属性,以便为请求处理器添加更多上下文,例如包含发出请求的用户的 User 实体。使用 Nimbly\Proof\Middleware\ValidateJwtMiddleware 和您自己的中间件,这变成了一件相对简单的事情。

class AuthorizeUserMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $token = $request->getAttribute(Nimbly\Proof\Token::class);

        if( empty($token) || $token->hasClaim("sub") === false ){
            throw new UnauthorizedHttpException("Bearer", "Please login to continue.");
        }

        $user = App\Models\User::find($token->getClaim("sub"));

        if( empty($user) ){
            throw new UnauthorizedHttpException("Bearer", "Please login to continue.");
        }

        $request = $request->withAttribute(App\Models\User::class, $user);

        return $handler->handle($request);
    }
}

在这个示例中,每个需要用户账户的请求都已将 User 实例附加到 ServerRequestInteface 实例上。