常见原因是公钥格式错误:openssl_verify要求pkcs#8格式(以-----begin public key-----开头),而openssl默认导出的是pkcs#1格式(-----begin rsa public key-----),需用openssl rsa -pubin -in old.pem -outform pem -out new.pem转换。

JWT解析失败时,openssl_verify报错“key is not a valid RSA public key”
常见于用私钥生成的 JWT 试图用公钥验签,但公钥格式不对。PHP 的 openssl_verify 要求 PEM 格式公钥必须以 -----BEGIN PUBLIC KEY----- 开头,而非 -----BEGIN RSA PUBLIC KEY-----(后者是 PKCS#1,前者是 PKCS#8)。OpenSSL 命令行默认导出的是 PKCS#1,需手动转换:
openssl rsa -pubin -in pub_key_rsa.pem -outform pem -out pub_key_pkcs8.pem
- 检查公钥内容是否含
PUBLIC KEY字样,不是RSA PUBLIC KEY - 若用
file_get_contents()加载公钥,确保文件末尾无 BOM 或多余空格 - Laravel 的
JWTGuard或firebase/php-jwtv6+ 默认要求 PKCS#8,旧版可能兼容 PKCS#1,但不建议混用
黑名单校验必须在 JWT 解析后、业务逻辑前执行
很多人把黑名单检查放在中间件末尾或控制器里,导致非法 token 已被解析、用户信息已查库、甚至日志已打点。正确位置是:token base64 解码 + header/payload 解析完成 → 验签通过 → 立即查黑名单(如 Redis 中的 jti 或 user_id:exp_timestamp 键)。
- 黑名单键建议设为
jwt:blacklist:<code>jti,TTL 设为exp - time()+ 缓冲(如 30 秒),避免过期后仍占内存 - 若用 MySQL 存黑名单,务必给
jti加唯一索引,并用SELECT 1 FROM jwt_blacklist WHERE jti = ? AND expires_at > NOW() LIMIT 1快速判存,别查全字段 - 注意时钟漂移:
exp是服务端时间戳,Redis 过期和数据库expires_at必须与 PHP 服务器时间严格同步(NTP)
firebase/php-jwt v6.10+ 的 setLeeway() 不解决黑名单时效问题
setLeeway(60) 只放宽 nbf/exp 时间校验窗口,对已加入黑名单但尚未过期的 token 完全无效。它不能替代实时黑名单查询。
- 黑名单必须独立校验,且优先级高于时间校验——哪怕 token 还没过期,只要在黑名单里就得拒绝
- 不要在
try/catch里吞掉ExpiredException后再查黑名单:顺序反了,该拒的没拒,还多一次 Redis 查询 - 若用 Laravel,
Auth::guard('api')->user()内部已触发解析和验签,黑名单检查必须在调用它之前完成,否则 guard 会缓存用户实例,绕过后续校验
Redis 黑名单键设计不当会导致误放行或雪崩
直接用 user_id 作 key(如 blacklist:123)看似简单,但无法区分同一用户多次登录的不同 token;若用 jti 却不带 TTL,Redis 内存会持续增长。
- 推荐组合键:
jwt:blacklist:<code>jti(string 类型),SET时指定 EX,不依赖后台清理 - 避免用
HSET blacklist:user_id field:jti value:exp:哈希结构查单个jti需HEXISTS+HGET两步,延迟翻倍,且无法自动过期 - 高并发登出场景下,多个请求同时写同一个
jti键无问题,但读取时要防止缓存穿透——可加一层EXISTS判断再查,或用布隆过滤器预筛(小项目不必)
黑名单不是加个 if (in_blacklist($jti)) 就完事。真正难的是键生命周期管理、Redis 查询路径收敛、以及和框架 auth 流程的咬合时机——这三个点漏一个,鉴权就形同虚设。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











