微服务认证与授权: 12 — JWKS 深入解析 📅 发布时间:2026/7/9 20:16:33 👁️ 浏览次数: 12 — JWKS 深入解析 GitHub: https://github.com/geekchow/micro-service-authJWKS 是一个实时的公钥目录它让banking-api-service能按kid选出正确的密钥并安全地验证 JWT 签名 —— 即便Keycloak轮换了签名密钥。全文使用的固定术语见 01 — 概念KeycloakIdP、KongPEP、OPAPDP、banking-api-service资源服务器。快速定义JWK—— 描述一把密码学密钥的 JSON 对象。JWKS—— 一个含有keys数组的 JSON 文档数组里有一个或多个 JWK。kid—— 密钥 ID用于选出正确的密钥来做验证。jwk-set-uri—— 提供 JWKS 文档的端点。JWKS 为什么存在对于像RS256这样的非对称签名Keycloak用私钥签名 JWT。服务用配对的公钥验证 JWT。公钥可能轮换。如果服务硬编码某一把密钥轮换时验证就会失败。JWKS 通过在一个稳定的 URL 上发布当前的公钥来解决这个问题。服务按需从该 URL 获取并缓存结果。它在本 PoC 中如何落位banking-api-service在services/banking-api-service/src/main/resources/application.yml中配置了spring.security.oauth2.resourceserver.jwt.jwk-set-uri这告诉 Spring Security 到哪里去获取用于校验 JWT 签名的公钥。在本 PoC 中该值由docker-compose.yml注入为BANKING_API_JWK_SET_URI:http://keycloak:8080/realms/banking-poc/protocol/openid-connect/certs为什么是http://keycloak:8080而不是http://localhost:9081banking-api-service运行在 Docker Compose 网络内部。在该网络内部keycloak是服务主机名。容器内的localhost指向的是banking-api-service容器自身而不是Keycloak。访问者URL宿主机http://localhost:9081/realms/banking-poc/protocol/openid-connect/certs容器对容器http://keycloak:8080/realms/banking-poc/protocol/openid-connect/certsJWKS 查找流程当一个带 bearer 令牌的请求到来时Spring Security 按以下步骤进行接收带 bearer 令牌的请求。解码 JWT 的 header 并读取kid。从Keycloak获取 JWKS或使用已缓存的密钥集。选出kid与令牌 header 中kid匹配的那把 JWK。用该密钥验证 JWT 签名。若签名及所有其他校验器都通过则认证该请求。Keycloakbanking-api-serviceKongaliceKeycloakbanking-api-serviceKongaliceGET /api/accounts/A-1001 Bearer JWTForward request Bearer JWTDecode JWT header, read kidGET /realms/banking-poc/protocol/openid-connect/certs{ keys: [ ... ] }Select key by kid, verify signature200 / 401 / 403response本 PoC 中实际的 JWKS 响应获取Keycloak的 JWKS 端点会返回这种形态的 JSON{keys:[{kid:gMTvER9Ofps6D0UuEk2av7caU5GlZd4sS-c7fWkyxoA,kty:RSA,alg:RSA-OAEP,use:enc,n:...,e:AQAB,x5c:[...],x5t:...,x5t#S256:...},{kid:0BYek66uebuec84BqxfwJ9_qxIDr1Wka-1siBT2z0Lk,kty:RSA,alg:RS256,use:sig,n:...,e:AQAB,x5c:[...],x5t:...,x5t#S256:...}]}注意该端点返回了不止一把密钥。并非每把密钥用途相同 ——use用来区分它们。为什么出现了两把密钥Keycloak暴露了两把用途不同的 RSA 密钥。密钥 1 —— 加密密钥{alg:RSA-OAEP,use:enc}use: enc表示这把密钥用于加密场景而非签名验证。banking-api-service不会用这把密钥来验证 JWT 签名。密钥 2 —— 签名密钥{kid:0BYek66uebuec84BqxfwJ9_qxIDr1Wka-1siBT2z0Lk,kty:RSA,alg:RS256,use:sig}use: sig表示这把密钥用于签名验证。alg: RS256与本 PoC 中 JWT header 的算法匹配。这就是 Spring Security 用来验证 JWT 签名的密钥。banking-api-service关心的是第二把密钥而非第一把。一把 JWK 的解剖RSA一把典型的 RSA 公钥 JWK 看起来是这样{kty:RSA,kid:0BYek66uebuec84BqxfwJ9_qxIDr1Wka-1siBT2z0Lk,use:sig,alg:RS256,n:...,e:AQAB}字段含义kty密钥类型。这里是RSA。kid密钥 ID。必须与 JWT header 中的kid匹配。use预期用途。sig表示签名验证。alg预期算法。本 PoC 中是RS256。nRSA 模数公开部分。eRSA 公钥指数。x5c、x5t与x5t#S256的含义Keycloak在 RSA 密钥材料旁还附带了证书字段。字段含义x5cX.509 证书链 —— 公钥材料的另一种表示。x5t证书的 SHA-1 指纹。x5t#S256证书的 SHA-256 指纹。Spring Security 主要需要n与e来重建用于验证的 RSA 公钥。证书字段在 JWKS 响应中是正常的对本 PoC 的验证过程没有影响。通过kid把 JWT header 匹配到 JWK本 PoC 中的一个 JWT header 形如{alg:RS256,typ:JWT,kid:0BYek66uebuec84BqxfwJ9_qxIDr1Wka-1siBT2z0Lk}密钥选择过程从 JWT header 取出kid。在 JWKS 的keys数组中找到kid相同的那把 JWK。用该 JWK 的公钥验证签名。如果没有 JWK 匹配该kid验证失败Spring Security 返回401 Unauthorized。本 PoC 中的具体匹配Spring 寻找满足以下条件的 JWKkid 0BYek66uebuec84BqxfwJ9_qxIDr1Wka-1siBT2z0Lk它还确认use sigalg RS256那正是用于验证该令牌的确切公钥条目。密钥轮换与多把密钥JWKS 在轮换期间通常会同时持有多把密钥。轮换期间的示例形态{keys:[{kid:old-key,kty:RSA,use:sig,n:...,e:AQAB},{kid:new-key,kty:RSA,use:sig,n:...,e:AQAB}]}为什么这很重要旧令牌在过期前仍用旧密钥验证通过。新令牌立即用新密钥验证。服务不会仅因为签名密钥轮换就需要重新部署。每个令牌 header 中的kid始终精确指向那把正确的密钥无论此刻 JWKS 中有多少把密钥。实践中的 Spring Security 行为banking-api-service使用NimbusJwtDecoder它由jwk-set-uri构建并与签发者、受众校验器组合。运行时Spring Security 按以下顺序进行读取 JWT header 并提取kid。若尚未缓存则从Keycloak获取 JWKS。定位kid匹配的那把 JWK。从n与e重建 RSA 公钥。验证 JWT 签名。检查令牌未过期exp。检查iss与配置的签发者匹配。检查aud包含配置的受众。若任一检查失败Spring Security 会在控制器逻辑运行之前就拒绝该请求。请求被接受要求以下全部成立检查描述签名对所选 JWK 有效过期exp尚未过签发者iss与配置的Keycloakrealm 匹配受众aud包含期望的值所以 JWKS 端点实际上就是banking-api-service的一个实时公钥目录。排障时如何检查 JWKS从宿主机直接获取 JWKScurl-sShttp://localhost:9081/realms/banking-poc/protocol/openid-connect/certs|jq解码一个令牌的 header 看它带的是哪个kidTOKENaccess-tokenprintf%s$TOKEN|cut-d.-f1|base64--decode2/dev/null|jq检查令牌 header 中的kid存在于 JWKS 的keys数组中。令牌 header 中的算法与所选 JWK 的alg匹配。令牌载荷中的iss与aud与banking-api-service的配置匹配。常见失败模式失败症状错误的jwk-set-uribanking-api-service无法获取密钥所有令牌校验都失败。陈旧或缺失的kid令牌由一把不在所获取 JWKS 中的密钥签名401 Unauthorized。签发者不匹配令牌由另一个Keycloakrealm 签发。受众不匹配令牌并非发给banking-api-service。网络或路径问题banking-api-service在启动或刷新时无法到达Keycloak的 certs 端点。以上所有都会表现为401 Unauthorized。安全说明JWKS 端点只包含公钥 —— 绝不含私钥。暴露 JWKS 是预期且正常的它不是机密。信任来自 HTTPS、签发者验证与正确的端点配置 —— 而非来自把密钥藏起来。不要仅因为签名通过就关闭签发者或受众检查。四个校验器一起才构成正确的信任边界。← Prev: 11 — JWT 签名、校验与内省 · Next: 13 — 访问令牌与刷新令牌的生命周期 →
工业级负载控制方案:TPD2015FN与STM32F031C6应用解析 1. 项目概述:工业级负载控制方案设计在工业自动化、机械控制和能源管理领域,可靠地控制电感和电阻负载是一项基础但关键的技术需求。TPD2015FN作为东芝半导体推出的8通道高端智能功率开关IC,与STMicroelectronics的STM32F031C6微控制器组合&a… 2026/7/9 20:14:32
MinIO 迁移 OtterIO 实战:国产化对象存储替换指南 1. 项目概述:为什么要把 MinIO 示例迁到 OtterIO?最近两周,我连续帮三家公司做了对象存储层的架构梳理,发现一个特别有意思的现象:几乎每家都在用 MinIO 做开发测试环境的底座,但一到生产上线阶段ÿ… 2026/7/9 20:14:32
C++ TCP客户端实现智能音箱控制电脑:巴法云物联网平台实战 1. 项目概述:用代码打通智能音箱与电脑的最后一公里 如果你和我一样,是个喜欢折腾的开发者,同时又对智能家居的自动化有着近乎偏执的追求,那你肯定也想过:能不能让家里的小米智能音箱,直接指挥我的电脑干点… 2026/7/9 20:12:32
C++17 std::optional:安全处理可选值的现代C++核心工具 1. 项目概述:为什么我们需要 std::optional ? 在C编程的日常里,我们经常遇到一个经典难题:一个函数执行后,结果可能有效,也可能无效。比如,从数据库中查询一条用户记录,或者解析一… 2026/7/9 23:36:53
PInVerify:具身AI主动验证离线基准 1. 项目概述:这不是又一个“刷榜”基准,而是给具身AI装上“质疑本能”的离线考场你有没有想过,当一个机器人在厨房里第一次看到“电水壶”时,它凭什么相信自己识别对了?不是靠训练集里见过一万张同款图片,而… 2026/7/9 23:34:52
基于TM4C1299NCZAD与PAM8904的智能音频通知系统设计 1. 项目概述:基于TM4C1299NCZAD与PAM8904的智能通知系统设计在工业控制、智能家居和医疗设备等领域,可靠的事件通知机制是保障系统安全运行的关键。我们经常遇到这样的场景:一台自动化设备需要在不依赖显示屏的情况下,通过声音提示… 2026/7/9 23:30:50
Openclaw智能体框架:本地部署Kimi实现办公自动化 1. 项目概述:这不是“买个会员”,而是亲手把 Kimi 的智能体能力装进自己电脑里 最近在技术圈和产品团队里,几乎每天都能看到同事发来截图:“Kimi Openclaw 199元订阅试用分享心得”——这标题乍看像电商测评,实则藏着… 2026/7/9 23:30:50
知医邦内测微信AI助手[小微]关于中医AI智能体的对话原文 [知医邦] 说说知医邦智能体吧 [小微] 你说的"知医邦智能体"是想在微信里打开知医邦相关的小程序吗?还是想了解一下这个智能体是做什么的? 如果是想打开他们的小程序,我帮你搜一下看看能不能直接拉起来。如果是想了解这个产品本身… 2026/7/9 23:30:49
三角形内心坐标计算 3 种方法:向量法、角平分线法、重心坐标法 三角形内心坐标计算的三种实用算法:从数学推导到代码实现在计算机图形学、游戏开发和几何算法竞赛中,三角形内心坐标的计算是一个基础但至关重要的操作。内心不仅是三角形内切圆的圆心,也是许多几何算法中的关键参考点。本文将深入探讨三种计… 2026/7/9 23:28:47
机器视觉与PLC集成:轮毂缺陷检测与字符识别误差控制在0.2mm内 机器视觉与PLC集成:轮毂缺陷检测与字符识别误差控制在0.2mm内的技术实现轮毂作为汽车关键零部件,其表面质量直接影响行车安全与美观。传统人工检测效率低且易漏检,而采用机器视觉与PLC集成方案可实现微米级精度检测。本文将深入解析高精度视觉… 2026/7/9 0:01:04
GBase 8a vs MySQL 8.0:ALTER TABLE语法与限制的5点关键差异对比 GBase 8a与MySQL 8.0:ALTER TABLE语法差异深度解析与实战指南1. 两种数据库的ALTER TABLE能力全景对比在数据库架构设计和运维过程中,表结构变更(DDL操作)是不可避免的需求。GBase 8a作为国产分析型数据库代表,与开源M… 2026/7/9 0:03:06
【大数据毕业设计】基于多源旅游数据的景区热度分析与推荐系统的设计与实现 基于 Django 的旅游偏好挖掘与景区推荐系统(源码+文档+远程调试,全bao定制等) 博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围:&am… 2026/7/9 0:05:09
6个月转型AI工程师:实战路径与核心技能 1. 项目概述:6个月转型AI工程师的可行性路径在2023年大模型技术爆发的背景下,AI工程师岗位需求同比增长217%(LinkedIn数据)。不同于传统算法工程师需要3-5年培养周期,现代AI工程师更侧重工程化落地能力。我在硅谷科技公… 2026/7/7 11:26:57
TPAFE0808与PIC18F87K22的多通道信号采集方案 1. 项目背景与核心需求在工业自动化、医疗设备和科研仪器等领域,多通道信号采集与系统监测是基础且关键的技术需求。传统方案往往面临通道数量不足、信号调理复杂、系统集成度低等问题。TPAFE0808作为一款8通道模拟前端芯片,与PIC18F87K22微控制器的组合… 2026/7/8 20:15:17
STC3115与PIC18LF26K80构建高精度电池管理系统 1. STC3115与PIC18LF26K80在电池管理系统中的核心价值在现代电子设备中,电池管理系统(BMS)的重要性不亚于设备的核心处理器。STC3115作为一款高精度电池电量监测IC,与PIC18LF26K80微控制器的组合,构成了一个既能精确监控又能智能管理的完整解… 2026/7/8 14:25:08