mixerapi / jwt-auth
CakePHP 的 JWT 身份验证插件。
Requires
- php: ^8.1
- ext-openssl: *
- cakephp/authentication: ^3.0
- cakephp/cakephp: ^5.0
- firebase/php-jwt: ^6.2
Requires (Dev)
- josegonzalez/dotenv: ^3.2
- phpunit/phpunit: ^10.0
README
这是一个为 CakePHP 提供支持的 JWT 身份验证库,它支持 HMAC (HS256 或 HS512) 和 RSA (RS256 或 RS512) 加上 JSON Web Keys。在开始之前,您应该确定哪种 签名算法 最适合您的需求。这个库的目标是使两者都变得简单。
有关替代方法,请参阅 admad/cakephp-jwt-auth。
安装
您可以使用 composer 将此插件安装到您的 CakePHP 应用程序中。
composer require mixerapi/jwt-auth
然后加载插件
bin/cake plugin load MixerApi/JwtAuth
配置
接下来,创建一个配置文件(例如 config/mixerapi_jwtauth.php
),并将其加载到您的应用程序中。
# in config/bootstrap.php Configure::load('mixerapi_jwtauth');
alg
alg
字符串是必需的,必须是 HS256、HS512、RS256 或 RS512 之一。
secret
当使用 HMAC 时,需要 secret
字符串。密钥不应提交到您的 VCS,且长度至少为 32 个字符。您可以使用 openssl 或 gpg 等工具生成强密码。
openssl rand -base64 24
gpg --armor --gen-random 1 24
keys
当使用 RSA 时,需要 keys
数组。密钥不应提交到您的 VCS,且长度至少为 2048 位。您可以使用 openssl 生成公钥/私钥对。
openssl genrsa -out config/keys/1/private.pem 2048 openssl rsa -in config/keys/1/private.pem -out config/keys/1/public.pem -pubout
服务提供者
建议使用 JwtAuthServiceProvider
来自动注入依赖项。
# in src/Application.php public function services(ContainerInterface $container): void { /** @var \League\Container\Container $container */ $container->addServiceProvider(new \MixerApi\JwtAuth\JwtAuthServiceProvider()); }
身份验证
您需要配置 CakePHP 身份验证 以使用此库。有关如何快速启动的文档,请参阅快速入门。有关完整示例,请参阅 mixerapi 示例。
确保加载 CakePHP Authentication.Component(通常在您的 AppController 中)。
以下是一个支持 HMAC 和 RSA(基于表单和密码身份验证)的示例。无论您如何实现身份验证,都建议使用 \MixerApi\JwtAuth\Configuration\Configuration
从您的 MixerApi.JwtAuth
配置文件 config/mixerapi_jwtauth.php
中提取值。这将验证您的配置,然后再将其应用到应用程序的身份验证中。
# in src/Application.php public function getAuthenticationService(ServerRequestInterface $request): \Authentication\AuthenticationServiceInterface { $fields = [ \Authentication\Identifier\AbstractIdentifier::CREDENTIAL_USERNAME => 'email', \Authentication\Identifier\AbstractIdentifier::CREDENTIAL_PASSWORD => 'password', ]; $config = new \MixerApi\JwtAuth\Configuration\Configuration(); $service = new \Authentication\AuthenticationService(); $service->loadAuthenticator('Authentication.Form', [ 'fields' => $fields, 'loginUrl' => '/admin/auth/login' ]); $service->loadIdentifier('Authentication.JwtSubject'); if (str_starts_with(haystack: $config->getAlg(), needle: 'HS')) { $service->loadAuthenticator('Authentication.Jwt', [ 'secretKey' => $config->getSecret(), 'algorithm' => $config->getAlg(), ]); } else if (str_starts_with(haystack: $config->getAlg(), needle: 'RS')) { $jsonKeySet = \Cake\Cache\Cache::remember('jwkset', function() { return json_encode((new \MixerApi\JwtAuth\Jwk\JwkSet)->getKeySet()); }); /* * Caching is optional, you may also set the jwks key to the return value of (new JwkSet)->getKeySet() */ $service->loadAuthenticator('Authentication.Jwt', [ 'jwks' => json_decode($jsonKeySet, true), 'algorithm' => $config->getAlg(), ]); } $service->loadIdentifier('Authentication.Password', ['fields' => $fields]); return $service; }
定义您的 JWT
在您的 User 实体中实现 JwtEntityInterface
。这将被用来生成 JWT,例如
namespace App\Model\Entity; use Cake\ORM\Entity; use MixerApi\JwtAuth\Jwt\Jwt; use MixerApi\JwtAuth\Jwt\JwtEntityInterface; use MixerApi\JwtAuth\Jwt\JwtInterface; class User extends Entity implements JwtEntityInterface { /** * @inheritDoc */ public function getJwt(): JwtInterface { return new Jwt( exp: time() + 60 * 60 * 24, sub: (string)$this->get('id'), iss: 'mixerapi', aud: 'mixerapi-client', nbf: null, iat: time(), jti: \Cake\Utility\Text::uuid(), claims: [ 'user' => [ 'email' => $this->get('email') ] ] ); } }
JSON Web Keys
使用 RSA 签名令牌使用公钥/私钥对。如果您使用 HMAC,则可以跳过此部分。
构建密钥
我们将密钥存储在 config/keys/1/
中,但您可以将它们存储在任何位置。密钥不应存储在版本控制中,例如
# in config/mixerapi_jwtauth.php return [ 'MixerApi.JwtAuth' => [ 'alg' => 'RS256', 'keys' => [ [ 'kid' => '1', 'public' => file_get_contents(CONFIG . 'keys' . DS . '1' . DS . 'public.pem'), 'private' => file_get_contents(CONFIG . 'keys' . DS . '1' . DS . 'private.pem'), ] ] ] ];
JWK 集控制器
有关 JSON Web Keys 的更多信息,请参阅 JSON Web Keys。让我们创建一个端点来公开您的 JWK 集合。
use Cake\Controller\Controller; use Cake\Event\EventInterface; use MixerApi\JwtAuth\Jwk\JwkSetInterface; class JwksController extends Controller { public function beforeFilter(EventInterface $event) { parent::beforeFilter($event); $this->Authentication->allowUnauthenticated(['index']); } public function index(JwkSetInterface $jwkSet) { $this->set('data', $jwkSet->getKeySet()); $this->viewBuilder()->setOption('serialize', 'data'); } }
在您的 config/routes.php
文件中添加一个路由到您的控制器。
示例响应
{ "keys": [ { "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "1", "n": "wk865HbUKadJU-Mh-Iv2Z_30ZOMclkK1cbuiTVkINy_R9oHoAht2DS788q_Sll38dtTB4bzptd0u6k4cJd6Lj6nVQTe1uRyuAU47tqitiJmEXX_2SHIRv6aj4vygIfqr1FtQMHPlBW7r4q840H5mh_Z-E_a7d27QbtJ3eYNEiFow6LLvl17_7bdaenlwccY0j-PY1GzL7UwG8uHBZ78ZOcvu_GgaYC5suRrJrV_6_Qu6lySXObDaajr6Foz0m-z4Aj7KA8KmAiM_Rw_Yqm_KqPT3YBGj83TxeEiMPkrMYry123hFQYm09EO2Az9lGjr-PQc6SR08SDqZ3zbwe9iam55dzVZ-vQF3ASnZpBHyIDhCI7PFShceFI1Sv0RW7-Tl0uM2jQa1RyEpFle1xc0RxSFZium0aGMnFuE2W9JDERPw47wFZx2kSk1nB6PDK6XPLJLi_db0VrP5m5z2HDWeYVmsuAVFm6-l1PjiGH4G1TpuYfPKP2P8K-kveo1Ddm14IJSWfcACeAF_gx644Ua_IJ8wS98dQqE-R-jzfEv7aLBacP5_thCUbHfCRrAgtM5lBAM_1tfQ4XsOLnFWkl4arm3TzN2wCjjuqxipgwpUtY_SN6SXhJW4MW2qHVKtHtXl9haF5gEDBL7twDsFozYZCc5k0d85EgfJ5Jn7ZSAgwXk", "e": "AQAB" } ] }
您可以根据密钥轮换策略添加/删除 MixerApi.JwtAuth.keys
配置中的密钥。
注意,如果您不使用依赖注入
public function index() { $this->set('data', (new JwkSet)->getKeySet()); $this->viewBuilder()->setOption('serialize', 'data'); }
登录控制器
以下示例中,我们将进行身份验证,创建之前定义的JWT并返回给请求者。
use Cake\Controller\Controller; use MixerApi\JwtAuth\JwtAuthenticatorInterface; public function LoginController extends Controller { public function beforeFilter(EventInterface $event) { parent::beforeFilter($event); $this->Authentication->allowUnauthenticated(['login']); } public function login(JwtAuthenticatorInterface $jwtAuth) { try { return $this->response->withStringBody($jwtAuth->authenticate($this->Authentication)); } catch (UnauthenticatedException $e) { return $this->response->withStringBody($e->getMessage())->withStatus(401); } } }
在您的 config/routes.php
文件中添加路由到控制器。
这将构建在用户实体中定义的JWT。
{ "iss": "mixerapi", "sub": "5e28e9ed-f3e1-4eb2-aa88-8d618f4021ee", "aud": "api-client", "exp": 1651972707, "jti": "a1f6f5ec-748d-4a1c-9d0e-f8e19ec7f9b2", "user": { "email": "test@example.com" } }
注意,如果您不使用依赖注入
public function login() { try { return $this->response->withStringBody( (new \MixerApi\JwtAuth\JwtAuthenticator)->authenticate($this->Authentication) ); } catch (UnauthenticatedException $e) { return $this->response->withStringBody($e->getMessage())->withStatus(401); } }
或者,如果您想自己处理身份验证,可以传递 JwtInterface
的实例,例如
public function login(JwtAuthenticatorInterface $jwtAuth) { try { $result = $this->Authentication->getResult(); if (!$result->isValid()) { throw new UnauthenticatedException(); } return $this->response->withStringBody($jwtAuth->authenticate($result->getData()->getJwt())); } catch (UnauthenticatedException $e) { return $this->response->withStringBody($e->getMessage())->withStatus(401); } }
安全性
该库内置了一些安全措施
弱HMAC密钥
使用HMAC签名的JWT可以使用像 JWT Tool 这样的工具进行暴力破解。一旦破解,JWT可以被修改。该库通过要求最小密钥长度为32个字符来减轻这一影响,但您可能希望考虑使用64个字符,如果安全性比速度和令牌大小更重要。生成强随机密钥并确保其安全由您负责。
弱RSA密钥
弱密钥同样可以被破解。该库要求最小密钥长度为2048位。根据您的安全要求,您可能希望考虑使用4096位的密钥长度。确保您的密钥安全由您负责。
Alg None Bypass
通过要求单个有效算法来缓解alg=none签名绕过漏洞。在 firebase/php-jwt 库中存在额外的保护措施,该库应保持最新状态。
RS/HS256 公钥不匹配漏洞
通过要求单个有效算法来缓解。在 firebase/php-jwt 库中存在额外的保护措施,该库应保持最新状态。