Keycloak Invalid token issuer
Keycloak 部署在 TKE 上,之前为了在服务中获取客户端的真实 IP,启用了 Ingress Nginx 的 proxy-protocol
[1]。
但是随后发现了一个奇怪的问题,部署在 TKE 集群中的服务,在通过公网域名访问其自身的其它服务提供的接口时不通。
调查过程
通过 ApiFox 接口是可以正常访问的,于是使用 curl
命令在集群中测试了一下。
root@app-liujiajia-deploy-7c66c786b4-gfwp4:/service# curl --location -v --trace trace_connect_sso.txt --request POST 'https://sso.liujiajia.me/auth/realms/testrealm/protocol/openid-connect/token' \
> --header 'Connection: close' \
> --header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
> --header 'Accept: */*' \
> --header 'Host: sso.liujiajia.me' \
> --header 'Content-Type: application/x-www-form-urlencoded' \
> --data-urlencode 'client_id=testclient' \
> --data-urlencode 'grant_type=password' \
> --data-urlencode 'username=user001' \
> --data-urlencode 'password=pwd001'
Warning: --trace overrides an earlier trace/verbose option
Note: Unnecessary use of -X or --request, POST is already inferred.
curl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to sso.liujiajia.me:443
root@app-liujiajia-deploy-7c66c786b4-gfwp4:/service# cat trace_connect_sso.txt
== Info: Trying 159.72.1.1...
== Info: TCP_NODELAY set
== Info: Connected to sso.liujiajia.me (159.72.1.1) port 443 (#0)
== Info: ALPN, offering h2
== Info: ALPN, offering http/1.1
== Info: successfully set certificate verify locations:
== Info: CAfile: none
CApath: /etc/ssl/certs
=> Send SSL data, 5 bytes (0x5)
0000: 16 03 01 02 00 .....
== Info: TLSv1.3 (OUT), TLS handshake, Client hello (1):
=> Send SSL data, 512 bytes (0x200)
0000: 01 00 01 fc 03 03 77 eb 23 e1 43 b3 0d 51 3a 7b ......w.#.C..Q:{
0010: 68 5d bc c1 20 2e a5 6d 83 54 d4 f5 ad 57 59 94 h].. ..m.T...WY.
0020: a6 33 59 8b 9f 53 20 bb cf 0a 31 06 f7 39 97 ee .3Y..S ...1..9..
......
01e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
== Info: OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to sso.liujiajia.me:443
== Info: Closing connection 0
请求在建立连接时就失败了,并且提示了 SSL_ERROR_SYSCALL
。只有第一次发送握手的日志,后续就没了。同样的又测试访问了集群外服务的接口,都可以正常访问。
这个问题一直没解决,调查了很久也没找到原因。唯一能确定的就是关闭 proxy-protocol
后请求可以正常访问。
对于大部分服务来说,调用内部服务时本就应该使用集群内部服务地址,以避免产生公网流量。这部分服务只要调整下配置就可以了。
对于 Keycloak 服务来说,授权时由于是客户端从外网发起的,所以也没有问题,但是在网关中对授权得到的 access_token
进行校验时,改为使用集群内服务地址后,就会出现文章标题的这个 Invalid token issuer 错误。
HTTP/1.1 401
Date: Tue, 25 Feb 2025 02:19:43 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Connection: close
WWW-Authenticate: Bearer realm="testrealm", error="invalid_token", error_description="Invalid token issuer. Expected 'http://svc-keycloak:8080/auth/realms/testrealm', but was 'https://sso.liujiajia.me/auth/realms/testrealm'"
Strict-Transport-Security: max-age=31536000; includeSubDomains
{
"code": -1,
"message": "access_token invalid",
"data": null,
"error": "Unauthorized /app/v1/user/info",
}
网关使用的是 keycloak-spring-boot-starter 12.0.1 版本,这个错误是在 RealmUrlCheck
类中抛出的。
@Override
public boolean test(JsonWebToken t) throws VerificationException {
if (this.realmUrl == null) {
throw new VerificationException("Realm URL not set");
}
if (! this.realmUrl.equals(t.getIssuer())) {
throw new VerificationException("Invalid token issuer. Expected '" + this.realmUrl + "', but was '" + t.getIssuer() + "'");
}
return true;
}
研究了半天源码,也没找到关闭这个校验的开关或方案,不过找到了这个值是通过 ${authServerBaseUrl}/auth/realms/open/.well-known/openid-configuration
端点获取的。其响应中的 issuer
就是上面代码中的 realmUrl
,而 issuer
值中的基础地址(即前缀部分)默认和是 authServerBaseUrl
一致的。
将 authServerBaseUrl
设置为集群内服务地址 http://svc-keycloak:8080/auth
后,realmUrl
会变成 http://svc-keycloak:8080/auth/realms/testrealm
。
而 access_token
由于是通过访问 https://sso.liujiajia.me/auth/realms/testrealm/protocol/openid-connect/token
获取的,其 issuer
值为 https://sso.liujiajia.me/auth/realms/testrealm
。
两者不一致,导致 RealmUrlCheck
校验失败。
代码中没有找到解决办法,就只能到 Keycloak 配置中去找了。在 Realm Settings 页面的 General 选项卡中,有一个 Endpoints 只读项。我这里有两个
- OpenID Endpoint Configuration
- SAML 2.0 Identity Provider Metadata
其中第一个就是之前发现的获取 issuer
时调用的端点,点开可以看到其 URL 就是 https://sso.liujiajia.me/auth/realms/testrealm/.well-known/openid-configuration
。
仔细查看了这个页面的各个配置项,发现可以通过修改 Frontend URL 配置项来修改这个端点返回的各种地址中的基础地址,其中就包括 issuer
的值。
Frontend URL
Set the frontend URL for the realm. Use in combination with the default hostname provider to override the base URL for frontend requests for a specific realm.
为领域设置前端 URL。与默认主机名提供者结合使用,以覆盖特定领域前端请求的基 URL。
修改 Frontend URL 为 https://sso.liujiajia.me/auth
后,在网关中即使通过集群内服务地址访问,获取到的 realmUrl
也是 https://sso.liujiajia.me/auth/realms/testrealm
,这样 RealmUrlCheck
校验就可以通过了。
解决方案
修改
keycloak.auth-server-url
配置为 Keycloak 的集群内部 Service 地址:yamlkeycloak: auth-server-url: http://svc-keycloak:8080/auth
修改 Keycloak 管理控制台中 Realm 的 Frontend URL 配置项为集群外部访问地址:
https://sso.liujiajia.me/auth
。