ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

WSO2令牌交换实战:三方IdP接入与token exchange failed排查

2026/9/9 22:44:55 拓冰建站 浏览量
WSO2令牌交换实战:三方IdP接入与token exchange failed排查 先交代一下背景我这边有一套系统上游是客户自己建的IdP下游一堆API服务。客户的要求很明确——所有下游服务只认WSO2 Identity Server签发的token不认三方IdP直接发的token。也就是说三方IdP负责登录认证WSO2负责下发和校验API访问令牌中间必须有一道token置换的动作用三方IdP的token去WSO2换一个WSO2自己的token。这个需求听起来不复杂但真正落地时踩的坑不少尤其是token exchange failed这个报错我前后排查了两三天。这篇文章把整个token置换链路、WSO2的配置要点、以及各种报错的排查思路完整写出来给后面要做同样事情的兄弟省点时间。1. 明确一个前提token置换到底解决什么问题1.1 我遇到的实际场景多套系统、多个IdP、一个统一出口这个项目的背景是客户原本有多个独立的业务系统每个系统各自对接自己的身份源。有的走SAML有的走OIDC有的甚至直接放一个静态token就能调API。后来他们要做统一治理要求所有API的访问必须经过统一审计、统一鉴权于是引入了WSO2 Identity Server作为统一身份网关。问题来了不可能让每个业务系统立刻改掉自己已有的IdP对接方式更不可能强制所有用户重新注册一遍。最平滑的方案就是——保留各系统原有的三方IdP登录流程登录成功之后由WSO2把三方IdP给的token翻译成WSO2自己签发的token。下游API只认WSO2的token至于原始token是Google签的还是Okta签的下游完全不关心。这就是token置换Token Exchange的核心场景不做认证只做信任转移。1.2 Token Exchange解决的层次信任而不是认证很多人会把token置换和SSO登录混在一起这两个东西解决的问题其实不一样。SSO解决的是用户已经在A系统登录了B系统怎么免登录。常见的OIDC授权码流程、SAML断言流本质上都是让一个身份提供方告诉另一个服务提供方这个用户我已经验过了你可以信任他。Token Exchange解决的是一个token怎么转换成另一个token。它不关心用户是怎么来的它关心的是你拿着A系统发的token我能不能给你签一个B系统认的token。这里的交换动作是发生在系统后台的不需要用户参与也不需要跳转浏览器。拿最常用的RFC 8693来说Token Exchange定义了一种OAuth2的授权类型关键参数就几个参数作用我的建议grant_type固定为urn:ietf:params:oauth:grant-type:token-exchange不要拼错大小写敏感subject_token你要拿出去交换的那个token通常是三方IdP的access tokensubject_token_typesubject_token的类型access token填urn:ietf:params:oauth:token-type:access_tokenid token就填对应的JWT类型requested_token_type你想要换回来什么类型的token默认就是access token但你可以显式要JWTaudience换回来的token打算给哪个服务用这个参数最容易忽略忘了填WSO2可能签出一个谁也认不了的tokenscope希望换回来的token带哪些权限一定要在WSO2端提前配置好否则会被拒理解了这个边界后面配置WSO2的时候才不会被各种选项绕晕。1.3 什么时候必须用Token Exchange而不是重新走OAuth授权码一个很容易想到的问题是三方IdP已经签发了token为什么不让下游API直接拿这个token去三方IdP验或者干脆让WSO2重新走一遍OAuth授权码流程不也能拿到WSO2的token吗从理论上说让下游API直接信任三方IdP的token是可行的但这要求每个下游API都对接三方IdP的JWKS、管理三方IdP的client配置、处理三方IdP的token签名算法——这等于把统一出口这件事做成了到处对接。而重新走授权码流程的痛点是授权码流程需要用户交互需要浏览器跳转还需要三方IdP授权WSO2访问用户资源。在很多纯API调用的场景里根本没有浏览器只有一个已经存在的access token。Token Exchange就是为这种场景设计的拿着已有的token后台悄悄换一个新的。不需要用户再点一次同意不需要浏览器参与纯接口调用就能完成。WSO2的token exchange实现本质上是把三方IdP当做一个token源WSO2检测到你拿来的token我信得过之后直接签一个自己的token给你。2. WSO2的token置换机制一次交换请求的完整解剖2.1 先从一次成功的token exchange请求说起我在WSO2 Identity Server 7.0IS 7.0上验证过完整的交换流程。假设现在已经把三方IdP配置好了Service Provider也建好了最核心的一次调用长这样curl -k -X POST https://is.example.com/oauth2/token \ -H Authorization: Basic base64(wso2_client_id:wso2_client_secret) \ -H Content-Type: application/x-www-form-urlencoded \ -d grant_typeurn:ietf:params:oauth:grant-type:token-exchange \ -d subject_tokeneyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... \ -d subject_token_typeurn:ietf:params:oauth:token-type:access_token \ -d requested_token_typeurn:ietf:params:oauth:token-type:jwt \ -d audiencehttps://api.example.com/orders \ -d scoperead_orders注意这里的几个关键设计Authorization头用的是WSO2这边注册的OAuth2应用的client_id和client_secret不是三方IdP的。也就是说WSO2先要确认你是谁调用方身份再确认你拿来的token是谁签的三方IdP信任关系两个都通过才给你换。subject_token是三方IdP发的access tokenWSO2不会直接透传或盲信它会去三方IdP的introspection端点或者本地配置的JWKS里验一遍签名和有效性。requested_token_type我习惯指定为urn:ietf:params:oauth:token-type:jwt这样WSO2返回的是一个标准JWT。如果不指定WSO2默认返回的可能是opaque token对调试不友好。成功的响应大概长这样{ access_token: eyJhbGciOiJSUzI1NiIsImtpZCI6IldTTzJfU0VSVklDRV9QUklWQVRFX0tFWSJ9..., refresh_token: ..., scope: read_orders, token_type: Bearer, expires_in: 3600 }这里面的access_token就是WSO2自己签的JWT下游API只要配置了WSO2的JWKS或者公钥就能离线验签不再需要回调WSO2做introspection。这也是很多系统最终选择JWT而不是opaque token的原因——少一次网络往返。2.2 WSO2在这条链路上做了什么验证很多人以为token exchange就是把一个token原样包装一下其实WSO2内部做的事情不少。我梳理了一下至少有这么几道验证第一验证调用方身份。也就是HTTP Basic认证里的client_id和client_secret这对应的是WSO2里注册的OAuth2应用。这一步通不过直接401。第二验证grant_type是否被允许。不是你在WSO2里创建了OAuth2应用就能用token exchange必须在应用配置里显式加上这个grant type否则营业执照上写着不支持此业务。第三验证subject_token。WSO2需要确认你拿来的token确实是由已配置的三方IdP签发的、没有过期、没有被篡改。这一步通常通过三方IdP的JWKSJWT签名公钥或者introspection端点不透明token的验真接口完成。我实测下来如果三方IdP用的是JWT格式的access tokenWSO2会主动去三方IdP配置里记录的jwks_uri拉取公钥如果三方IdP给的是opaque token则必须在WSO2的IdP配置里填好introspection端点。第四验证audience和scope。WSO2签新token的时候会把请求里的audience放进JWT的aud字段把scope放进scp或scope字段。如果向WSO2要的scope超出了这个OAuth2应用被授权的范围WSO2会拒绝。这是经常被忽略的一个坑——很多人以为token exchange是万能钥匙换了就能要任何权限其实WSO2在交换时也会做最小权限控制。2.3 交换前后的token形态差异为什么推荐要JWT我在配置时特意对比了交换前后的token。三方IdP的token可能是opaque的一串随机字符串也可能是JWTWSO2的token可以配置成JWT格式也可以配置成不透明随机串。推荐把requested_token_type定成JWT原因有三个一是排查问题方便。JWT的payload可以直接base64解码看内容sub、aud、scope、exp一目了然opaque token出了问题只能靠日志猜。二是下游API可以离线验签。只要下游拿到WSO2的JWKS公钥就能自己验JWT签名和有效期不用每次请求都回调WSO2的introspection端点性能上差一个数量级。三是claims映射可控。JWT里的字段是可以由WSO2端做映射的比如把三方IdP token里的email映射成WSO2 token里的emailAddress把uid映射成sub。这种映射在opaque token时代很难做因为不透明token本身不携带可读信息。当然JWT也有缺点——一旦签发在过期之前无法主动失效。如果你们对即时吊销有强需求那还是用opaque token加introspection更合适。这是一对权衡没有绝对的对错。3. 一步步配置从三方IdP到WSO2的token交换3.1 准备阶段确认版本和前置依赖先说版本。WSO2 Identity Server从6.0开始对RFC 8693的支持才比较完善7.0以后配置界面更友好。我强烈建议直接用IS 7.0及以上6.x版本虽然也能配但很多参数要手改XML排查问题时长线作战很痛苦。其次是确认三方IdP的能力清单这直接关系到后面配置能走多远三方IdP的token是JWT还是opaque如果是JWT它的JWKS端点地址是什么签名算法是什么RS256常见ES256也有如果是opaque它有没有提供introspection端点调introspection需要什么凭据三方IdP支持OIDC discovery吗如果支持能不能直接填一个well-known配置URL就自动拉取所有端点。把这几个问题确认清楚WSO2侧的配置是半小时的事确认不清楚配置完你会在调试阶段浪费几天。3.2 WSO2中注册三方IdP身份提供者配置实操WSO2里管外部身份源的抽象叫Identity Provider简称IdP。入口路径是Identity Providers → Add Identity Provider名字自己起建议用能一眼认出来源的命名比如AzureAD_Prod、Okta_Test。重点配置在Federated Authenticators这里。如果你要用OIDC方式对接选择OpenID Connect Configuration填入三方IdP的Client ID / Client Secret这是WSO2在三方IdP那边注册的凭据不是WSO2自己的。Authorization Endpoint、Token Endpoint、Userinfo Endpoint如果三方IdP支持OIDC Discovery可以直接填well-known地址WSO2会自动拉取。JWKS Endpoint验证subject_token签名用的公钥地址JWT格式token必填。这里有个容易犯迷糊的点我刚开始配置时用的是WSO2自己的client_id去调三方IdP的token端点结果一直报错。后来才意识到WSO2作为客户端去连接三方IdP时用的是WSO2在三方IdP注册的client身份而在token exchange时调用方拿的client是WSO2自己这边OAuth2应用的client。两套凭据不能混。如果三方IdP的token是opaque格式还需要在Introspection Endpoint里填上验真接口并且配置好调用introspection时用的Basic认证凭据。WSO2的文档里这部分写得很简略实际上很多opaque token失败都是这里没配好。3.3 创建Service Provider并开启token exchange授权类型IdP配好之后下一步是创建一个Service Provider服务提供方这是WSO2里代表下游业务系统的概念。路径是Service Providers → Add Service Provider。创建之后进Inbound Authentication Configuration → OAuth2 / OpenID Connect Configuration点Configure。这里有几个关键选项Allowed Grant Types必须勾上Token Exchange在IS 7.0里叫urn:ietf:params:oauth:grant-type:token-exchange。如果用的是IS 6.0可能要自己加自定义grant type。我见过很多人卡在这一步——grant type没开token exchange请求发过去永远报invalid_grant。Callback URL / Redirect URL虽然是token exchange场景不涉及浏览器跳转但WSO2会校验这个字段随便填一个合法的https地址就行。Allowed Audience如果你在token exchange请求里传了audience务必确保它在WSO2端的允许列表里否则会报invalid_target。IS 7.0里可以在OAuth2应用的高级设置里配这个白名单。配完之后还要做一步在Service Provider的Claim Configuration里配置claims映射。默认情况下WSO2不会把三方IdP token里的所有字段都搬进新token只搬映射过的。我需要把三方IdP的email、name、sub映射到WSO2的本地claims比如http://wso2.org/claims/emailaddress下游API拿到JWT后才能读到这些字段。3.4 写代码调用token endpoint完成交换配置全部完成后实际交换的代码很简单。我这边用的是Java环境核心逻辑长这样String clientId wso2_client_id; String clientSecret wso2_client_secret; String auth Basic Base64.getEncoder() .encodeToString((clientId : clientSecret).getBytes()); HttpClient client HttpClient.newHttpClient(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(https://is.example.com/oauth2/token)) .header(Authorization, auth) .header(Content-Type, application/x-www-form-urlencoded) .POST(BodyPublishers.ofString( grant_typeurn:ietf:params:oauth:grant-type:token-exchange subject_token三方IdP返回的access_token subject_token_typeurn:ietf:params:oauth:token-type:access_token requested_token_typeurn:ietf:params:oauth:token-type:jwt audiencehttps://api.example.com/orders)) .build(); HttpResponseString response client.send(request, BodyHandlers.ofString());如果你是用Python那就更简洁了用requests库发一个POST就行。核心点是拼URL编码的form参数时token本身如果包含特殊字符一定要URL编码我见过有人直接用原始token拼接导致签名被截断的。换回来的JWT可以直接解一下payload验证aud和scope是否符合预期{ iss: https://is.example.com/oauth2/token, sub: userexample.com, aud: [https://api.example.com/orders], scope: [read_orders], exp: 1710000000, iat: 1709996400 }到这一步token置换的主链路就跑通了。4. 高频报错token exchange failed的完整排查链路4.1 先看最要命的token endpoint returned status 403 forbidden这个报错是我在整个过程中被折磨最久的而且热搜词里也反复出现403 forbidden: country, region, or territory not supported。我遇到的第一个403场景是WSO2所在服务器所在区域被三方IdP拒绝。三方IdP的token端点做了地域限制而WSO2服务器发出的请求从被限制的地区发起于是一连到token端点就被403拦下。这种问题光看WSO2日志是看不出来的因为日志里只记录token exchange failed: token endpoint returned status 403 forbidden不会告诉你你的地区被拒绝。排查方式先直接在三方IdP的token端点上手工发起一次HTTP请求看看返回体里有没有更详细的错误描述。我那次返回的是一个JSON里面明确写了region not supported。如果你的WSO2服务器和当地网络没办法改最简单的方案是看三方IdP是否允许配置IP白名单或者换一个不受地域限制的访问入口。这个其实不是WSO2的问题而是网络拓扑的问题。第二个常见403是WSO2去三方IdP做introspection或者JWKS拉取时三方IdP拒绝了WSO2的client凭据。这种情况的排查点是WSO2的IdP配置里填的client_secret有没有填错、三方IdP端有没有限制grant类型为授权码流程、以及是否有allowed scope拦截。4.2 报错二error sending request for url这个报错比403更让人头疼因为它只告诉你发请求的时候出错了不告诉你为什么出错。我的第一个念头是网络不通在WSO2服务器上ping一下三方IdP的token端点通。又用curl发了一次发现能通。但WSO2就是一直报error sending request for url。后来我把WSO2的调试日志打开才发现WSO2在拉取三方IdP的JWKS时走的那个URL用了内网DNS解析解析出来的IP是内网地址WSO2服务器访问不到。也就是WSO2配置里填的well-knownURL是内网地址但三方IdP返回的JWKS URL却是一个公网内网都无法访问的地址。解决办法是显式在三方IdP配置里把JWKS Endpoint填成一个WSO2能访问的公网地址不要依赖well-known自动发现。同时确认WSO2服务器是否需要走代理访问外网如果需要要在deployment.toml里配置HTTP代理[transport.http] proxyHost proxy.example.com proxyPort 8080还有一种情况是SSL证书链问题。三方IdP的证书如果使用了私有CA签发WSO2的Java信任库不认也会表现成error sending request for url。这个解法是把三方IdP的证书导入WSO2的client-truststore.jks。排查时先看WSO2的IS_HOME/repository/logs/wso2carbon.log里有没有SSLHandshakeException或者PKIX path building failed字样有的话基本可以断定证书问题。4.3 报错三invalid_grant 与 subject token 类型不匹配如果你在token exchange请求里把subject_token_type配成了urn:ietf:params:oauth:token-type:access_token但实际传的是三方IdP的id tokenWSO2会去尝试用access token的验证方式验证id token结果大概率是invalid_grant。这是最容易自摆乌龙的场景。三方IdP在OIDC登录成功后通常会返回两个token一个access_token一个id_token。很多人不注意随手把id_token拿来交换。id_token的aud是三方IdP的client_id而access token的aud是三方IdP自己WSO2在三方IdP的JWKS里验签名时可能能过但校验aud的时候就会对不上。如果确实想用id token交换subject_token_type要改成urn:ietf:params:oauth:token-type:id_token并且WSO2的IdP配置里要允许id token作为subject token。我个人的建议是能用access token就别用id token。id token的语义是证明用户是谁access token的语义是允许调用资源token exchange的场景下我们关心的是后者。4.4 报错四invalid_scope 与 audience 配置缺失invalid_scope这个报错通常会出现在你请求的scope超出了WSO2端OAuth2应用允许范围的情况。排查思路很简单先查WSO2里Service Provider的OAuth2应用配置看Allowed Scopes列表里有没有你请求的scope。没有就加上或者把请求里的scope缩小。audience的问题则更隐蔽。有些三方IdP的JWT payload里本身就带了一个aud字段WSO2在交换时如果发现请求里没有传audience参数可能直接把subject token里的aud搬过来当成新token的aud。这个行为看起来是自动补全实际上会让下游API校验aud时一脸懵——明明我只要调用orders服务结果token里的aud是三方IdP的client id。我的做法是token exchange请求里永远显式传audience并且确保它在WSO2端OAuth2应用的allowed audience里。这样换出来的JWTaud一定是下游API能校验的值。4.5 排错方法论日志、后端日志、协议日志三级排查踩了这么多坑之后我总结出一套排错顺序分享出来第一级看WSO2日志。默认日志在IS_HOME/repository/logs/wso2carbon.logtoken exchange失败的信息一般会出现在这里。先把日志级别调成DEBUG特别是org.wso2.carbon.identity.oauth2.token.handlers和org.wso2.carbon.identity.oauth2.dto这两个包能看得很细。第二级直接手测三方IdP端点。不管WSO2报什么错先用curl手动请求三方IdP的token、introspection、JWKS端点排除三方IdP自身的问题。如果手动请求都失败那大概率不是WSO2的锅先修三方IdP侧。第三级才是看协议级报文。如果WSO2日志和手测都没问题但WSO2依旧失败可以用tcpdump或Wireshark抓包看WSO2和三方IdP之间的HTTPS流量。虽然HTTPS流看不全body但能确认请求是否真的发出去了、TLS握手是否成功、响应码到底是什么。5. token置换落地之后的几条实用建议5.1 别把三方IdP的claims一股脑搬进新token默认情况下WSO2做token exchange时只搬映射过的claims。我见过有人图省事把三方IdP返回的所有字段全部映射到WSO2的claims里结果新token巨大JWT有时候超过4KB每次请求都带着一个大头API网关解析header都变慢。合理的做法是只映射业务真正需要的最小字段集。比如大多数下游API只需要知道用户的唯一标识和email那就映射这两个就够了。JWT这个格式虽然方便但体积控制是要放在心里的。最小权限原则不只是说scopeclaims的暴露范围同样重要——你从三方IdP换来的token里包含的字段等于变相告诉了所有下游API你看这个人的信息这么多要不要顺手拿走。5.2 token过期时间与缓存策略WSO2签发的token默认过期时间通常在OAuth2应用配置里设置。我在实测中发现token exchange场景下新token的有效期不应该设置得比subject token更长。逻辑很简单subject token是信任的根如果三方IdP的token只给30分钟有效WSO2换出的token却给了60分钟那在后30分钟里WSO2签发的token仍然有效但它已经无法用原始的subject token来追溯。这意味着如果用户在30分钟后被三方IdP彻底注销他在三方IdP侧已经完全失联了但在WSO2这一侧还能访问60分钟。这在安全审计上是明显的漏洞。所以我的建议是把WSO2侧token有效期设置得比三方IdP的token短或者至少相等。同时在业务侧做好缓存——一次token exchange的往返大概在几十到几百毫秒如果下游API调用很频繁可以拿换回的token做短时缓存快到过期时间再重新交换避免每次请求都在做一种很细粒度的token交换。5.3 定期巡检IdP证书和JWKS更新最后说一个特别容易被忽略的运维问题。三方IdP的签名密钥不是永久的很多正规IdP会定期轮换JWKS。如果WSO2的缓存里一直存着旧的公钥等三方IdP轮换完密钥WSO2再去验subject_token的签名就会失败表现为突然从某个时间点开始大量token exchange failed。WSO2对JWKS是有缓存的默认缓存时间可能比较长。排查方法是看WSO2日志里有没有类似cannot find matching key或者no usable public key的报错。解决思路有两个一是把三方IdP的JWKS缓存时间调短deployment.toml里搜jwks相关配置二是做一个定时任务提前把三方IdP的新公钥推送到WSO2的信任配置里。我个人的实操体会是token置换这件事本身并不难难的是你想清楚谁信任谁谁给谁授权token的生命周期怎么管这几个问题。把这些问题想清楚WSO2的配置只是抄作业想不清楚就算配置全通后面也会被各种隐蔽的边界情况打爆。如果你也在做WSO2和多方IdP集成的方案建议先拿着本文的排查顺序走一遍把网络层、证书层、grant type层逐个确认能少走很多弯路。