一个能够编码、解码和验证签名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(简称 Key ID)声明/属性来包含密钥 ID。这个密钥 ID 应该映射到单个唯一的签名密钥。Proof 通过构造函数中的 keyMap 属性支持多个签名密钥:您提供字符串到 SignerInterface 实例的键/值对数组。

对于需要解码的 JWT,Proof 将检查头部是否有 kid 属性,如果有,将从 keyMap 中拉取匹配的 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实例的Token属性。该Token实例可以在添加更多上下文到请求的进一步中间件中使用,例如一个包含发起请求用户的User实例。

如果JWT无效,将抛出一个异常。您需要根据需要处理此异常。可能抛出的异常有

  • 如果 JWT 由于格式错误或包含无效 JSON 而无法解码,将抛出 InvalidTokenException
  • 如果签名不匹配,将抛出 SignatureMismatchException
  • 如果令牌的 exp 声明已过期,将抛出 ExpiredTokenException
  • 如果令牌的 nbf 声明尚未准备好(即时间戳仍然是将来的),将抛出 TokenNotReadyException

中间件默认在Authorization HTTP头中查找JWT,使用Bearer方案。例如

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实例上。