firehed / jwt
JSON Web Token 工具
Requires
- php: ^8.1
- firehed/security: ^1.0
Requires (Dev)
- phpstan/phpstan: ^1.2
- phpstan/phpstan-phpunit: ^1.0
- phpstan/phpstan-strict-rules: ^1.0
- phpunit/phpunit: ^10.5 || ^11.0
- squizlabs/php_codesniffer: ^3.4
This package is auto-updated.
Last update: 2024-09-19 23:17:53 UTC
README
安装
composer require firehed/jwt
使用
基本编码示例
<?php require_once 'vendor/autoload.php'; use Firehed\JWT; use Firehed\Security\Secret; $keys = new JWT\KeyContainer(); $keys->addKey(1, JWT\Algorithm::HMAC_SHA_256, new Secret('some secret key')); $data = [ 'some' => 'data', 'that' => 'you want to encode', ]; $token = new JWT($data); $token->setKeys($keys); $jwt_string = $token->getEncoded();
基本解码示例
<?php require_once 'vendor/autoload.php'; use Firehed\JWT; use Firehed\Security\Secret; $keys = new JWT\KeyContainer(); $keys->addKey(1, JWT\Algorithm::HMAC_SHA_256, new Secret('some secret key')); $jwt_string = 'some.jwt.string'; $token = JWT::fromEncoded($jwt_string, $keys); $data = $token->getClaims();
安全:签名密钥
生成
HMAC-SHA 算法族支持的最大密钥长度为 512 位(64 字节)。建议使用最长的密钥。
如果 PHP 版本大于等于 7,使用 random_bytes()
: $secret = random_bytes(64)
;
由于这些可能包含二进制数据,最好以 base64 编码的形式存储它们
$secret = random_bytes(64);
$encoded = base64_encode($secret);
下面的配置文件应该先对编码的字符串进行 base64_decode
,然后再返回
标识符和轮换
强烈建议 定期轮换签名密钥,JWT 规范通过 kid
标头使这一过程变得简单。编码输出将始终包括用于签名令牌的密钥标识符,该值将在解码时自动使用。
在您的应用程序配置中,定义多个密钥及其标识符
$keys = new KeyContainer(); $keys->addKey('20160101', Algorithm::HMAC_SHA_256, new Secret(base64_decode('string+generated/earlier'))) ->addKey('20160201', Algorithm::HMAC_SHA_256, new Secret(base64_decode('other+string/generated')));
简单地向容器中添加额外的密钥可以大致自动处理所有新令牌的密钥轮换,但您的应用程序可能以不同的方式运行,无法保证这一点。
默认情况下,KeyContainer
将使用最近添加的密钥,如果没有明确请求。您可以显式设置默认密钥来覆盖此行为
$keys->setDefaultKey('20160101');
注意:密钥 ID 可以采用任何标量格式。上面的示例使用日期戳,但顺序整数也行。建议使用对应用程序有语义意义但对最终用户没有意义的值。
安全:异常处理
在调用 getClaims()
时,如果签名无法验证或标准 nbf
或 exp
声明中的时间有效性超限,可能会抛出异常。
在调用这些方法时,准备好捕获 InvalidSignatureException
、TokenExpiredException
和 TokenNotYetValidException
。
如果将无效的令牌传递给 JWT::fromEncoded()
,将抛出 InvalidFormatException
。
异常树
Exception
|--Firehed\JWT\JWTException
|--Firehed\JWT\InvalidFormatException
|--Firehed\JWT\InvalidSignatureException
|--Firehed\JWT\TokenExpiredException
|--Firehed\JWT\TokenNotYetValidException
算法支持
从 v2.0.0 版本开始,以下算法得到支持
none
HS256
(HMAC-SHA256)HS384
(HMAC-SHA384)HS512
(HMAC-SHA512)
由于 none
算法本身是不安全的,编码数据只能通过 getUnverifiedClaims()
API 调用访问。这是为了引起对数据不可信的明确关注。强烈建议永远不要使用 none
算法。
在验证过程中故意忽略头部中的算法,以避免 算法交换攻击。这个库相反使用 kid
(密钥标识符)头部,将值与 KeyContainer
中的密钥匹配。目前不支持非对称密钥。
会话
由于 JWT 是经过加密签名的,现在既可能又实用地将基本的会话处理完全放在客户端,从而消除对数据库连接或文件系统的依赖(以及将其扩展到多个服务器的复杂性)。包含一个实现 PHP 的 SessionHandlerInterface
的类,以便更容易地进行。
一般来说,除了标识符以外的会话数据存储在客户端是 一个糟糕的决定。它在快速原型设计或资源极度受限的环境中很有用。
有一些非常重要的考虑事项
-
JWT会话cookie将使用
session_get_cookie_params
中的值。PHP为这些值提供了不安全默认值,您必须使用session_set_cookie_params
重新配置它们,以确保设置secure
和httponly
cookie标志。这不是自动完成的,也没有强制执行,以便于本地测试。 -
会话不得包含敏感信息。JWT未加密,只是编码和签名。您必须接受会话中的任何数据对用户可见。
-
由于这仅使用cookie进行存储,可用的空间非常有限(所有cookie的~4096b)且所有未来的网络请求都会产生开销。这使得基本认证信息(例如
$_SESSION['user_id'] = 12345;
)和状态管理可行,但如果您的会话包含大量数据或您使用了多个其他cookie,则这不是一个好的选择。 -
由于数据完全存储在客户端,如果用于存储认证数据,将很难构建“从所有地方注销”等功能。
-
与其他任何会话管理一样,如果不通过HTTPS完成,整个操作就没有意义。
-
会话ID的概念在很大程度上被忽略
尽可能情况下,SessionHandler
类会尝试使用现有的PHP配置进行会话处理,以作为直接替换。
以上问题解决后,下面是如何实现它的方法
会话示例
<?php ini_set('session.use_cookies', 0); // Without this, PHP will also send a PHPSESSID cookie, which we neither need nor care about session_set_cookie_params( $lifetime = 0, $path = '/', $domain = '', $secure = true, // <-- Very important $httponly = true // <-- Very important ); require 'vendor/autoload.php'; use Firehed\JWT; use Firehed\Security\Secret; $keys = new JWT\KeyContainer(); $keys->addKey(1, JWT\Algorithm::HMAC_SHA_256, new Secret('some secret key')); $handler = new Firehed\JWT\SessionHandler($keys); session_set_save_handler($handler); try { session_start(); $_SESSION['user_id'] = 12345; } catch (Firehed\JWT\InvalidSignatureException $e) { // The session cookie was tampered with and the signature is invalid // You should log this and investigate session_destroy(); }
SessionHandler
将始终使用KeyContainer
中的默认值。这意味着除非使用->setDefaultKey()
指定了其他值,否则将使用最新的密钥。