nimbly / proof
一个简单的库,能够对签名的JWT进行编码、解码和验证。
Requires
- php: ^8.0
- ext-openssl: *
- paragonie/hidden-string: ^2.0
- psr/http-message: ^1.0
- psr/http-server-middleware: ^1.0
Requires (Dev)
- nimbly/capsule: ^2.0
- phpunit/phpunit: ^9.0
- symfony/var-dumper: ^5.1
- vimeo/psalm: ^5.0
This package is auto-updated.
Last update: 2024-09-08 16:16:47 UTC
README
一个简单的库,能够对签名的JWT进行编码、解码和验证。
要求
- PHP 8.x
- OpenSSL PHP 扩展
安装
composer require nimbly/proof
使用概述
实例化
使用您的 SignerInterface
实例创建一个新的 Proof
实例。签名者负责对您的JWT进行签名以防止对令牌的篡改。必须向 Proof
实例提供您的签名首选项:HmacSigner
或 KeypairSigner
(有关更多信息,请参阅 签名者 部分)。
$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_privatekey
和 openssl_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.pem
和 public.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
实例上。