PHP中web-token/jwt-framework解码JWE耗时过长求助
JWE解密耗时过长(13分钟)的优化方案(Totara15 + web-token/jwt-framework 2.2.11)
问题背景
基于Totara15(Moodle衍生系统)开发OAuth2登录流程,UserInfo接口返回内容采用JWE加密(对方使用我方公钥加密),需用我方私钥解密后完成登录。当前使用web-token/jwt-framework 2.2.11可成功解密,但单次解密耗时长达13分钟,仅$jweLoader->loadAndDecryptWithKey()这一行代码占用全部耗时,其余步骤仅需毫秒级。由于Totara15不支持PHP8.1,无法升级框架至最新版本。
核心瓶颈分析
排查后确定是RSA私钥解密的性能问题:
- web-token框架的RSAOAEP算法默认优先使用
ext-openssl扩展实现,若未启用扩展则会降级为纯PHP实现,纯PHP的RSA解密(尤其是4096位密钥)速度极慢; - 代码中存在不必要的组件加载,增加了框架内部的判断开销。
优化建议
1. 启用PHP OpenSSL扩展(最关键)
纯PHP实现的RSA解密速度是数量级的差距,必须确保ext-openssl已启用:
- 执行
php -m查看是否包含openssl模块; - 若未启用,修改
php.ini添加extension=openssl并重启PHP服务; - 可在代码中添加
var_dump(extension_loaded('openssl'));验证是否生效。
2. 缓存JWK对象,避免重复加载密钥
每次调用解密函数都重新读取密钥文件并生成JWK对象,虽耗时不高,但可通过缓存减少重复操作:
- 使用静态变量或Totara/Moodle的缓存API(
cache::make)按kid缓存JWK实例,避免重复初始化。
3. 精简框架配置,移除无用组件
当前代码引入了未使用的算法和压缩方法,精简后可减少框架内部的判断开销:
- 仅保留实际使用的加密算法(RSAOAEP、A128CBCHS256),移除
A256KW、A256CBCHS512等未使用的算法实例; - 若确认JWE未使用Deflate压缩,可直接移除
CompressionMethodManager的初始化。
4. 添加HeaderCheckerManager提前验证算法
提前验证JWE的alg/enc头,避免框架尝试不必要的解密算法,减少无效操作:
- 初始化
HeaderCheckerManager并指定允许的算法列表,传入JWELoader。
5. 测试替代方案
若上述优化仍无法满足性能要求,可尝试:
- 手动用OpenSSL解密:绕过框架 overhead,直接处理JWE的Compact格式(拆分5个部分,用OpenSSL解密加密密钥,再解密载荷);
- 测试其他框架:如
namshi/jose(支持JWE且兼容PHP7.4),但需注意API差异。
优化后的代码示例
<?php namespace local_oauth2_extension; require_once(__DIR__ . '/../vendor/autoload.php'); use Jose\Component\KeyManagement\JWKFactory; use Jose\Component\Core\AlgorithmManager; use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP; use Jose\Component\Encryption\Algorithm\ContentEncryption\A128CBCHS256; use Jose\Component\Encryption\Compression\CompressionMethodManager; use Jose\Component\Encryption\Compression\Deflate; use Jose\Component\Encryption\JWEDecrypter; use Jose\Component\Core\JWK; use Jose\Component\Encryption\Serializer\JWESerializerManager; use Jose\Component\Encryption\Serializer\CompactSerializer; use Jose\Component\Encryption\JWELoader; use Jose\Component\Checker\HeaderCheckerManager; use Jose\Component\Checker\AlgorithmChecker; class jwe_helper { public static function decode_response($token, $kid, $pem_path) { try{ // 按kid缓存JWK,避免重复加载密钥文件 static $jwkCache = []; if (!isset($jwkCache[$kid])) { $jwkCache[$kid] = JWKFactory::createFromKeyFile( $pem_path, null, ['use' => 'enc', 'kid' => $kid] ); } $jwk = $jwkCache[$kid]; // 仅保留实际使用的加密算法 $keyEncryptionAlgorithmManager = new AlgorithmManager([ new RSAOAEP() ]); $contentEncryptionAlgorithmManager = new AlgorithmManager([ new A128CBCHS256() ]); // 若JWE未使用压缩,可注释以下两行 $compressionMethodManager = new CompressionMethodManager([ new Deflate(), ]); // 提前验证JWE的算法头,避免无效尝试 $headerCheckerManager = new HeaderCheckerManager( [new AlgorithmChecker(['RSA-OAEP'])], ['enc'] ); $jweDecrypter = new JWEDecrypter( $keyEncryptionAlgorithmManager, $contentEncryptionAlgorithmManager, isset($compressionMethodManager) ? $compressionMethodManager : null ); $serializerManager = new JWESerializerManager([ new CompactSerializer(), ]); $jweLoader = new JWELoader( $serializerManager, $jweDecrypter, $headerCheckerManager ); $jwe = $jweLoader->loadAndDecryptWithKey($token, $jwk, $recipient); $payload = base64_decode($jwe->getPayload()); $userinfo = self::extract_userinfo($payload); return $userinfo; } catch (\Exception $exception) { // 建议记录错误日志以便排查 // error_log('JWE解密失败: ' . $exception->getMessage()); return $token; } } private static function extract_userinfo($payload) { // 此处实现你的用户信息提取逻辑 return json_decode($payload, true); } }
额外调试技巧
- 查看密钥长度:
var_dump($jwk->get('n')->bitLength());,4096位密钥在无openssl扩展时解密会极慢; - 用OpenSSL命令行测试解密速度:提取JWE的第二部分(加密密钥),执行
openssl rsautl -decrypt -inkey private.pem -oaep -in encrypted_key.bin,若命令行速度快则说明PHP的openssl扩展未启用。
内容的提问来源于stack exchange,提问作者Martin S
相关产品推荐
相关产品推荐

