一个能够编码、解码和验证签名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
(简称 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
实例上。