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 的值。
[!TIP] 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 地址:keycloak: auth-server-url: http://svc-keycloak:8080/auth -
修改 Keycloak 管理控制台中 Realm 的 Frontend URL 配置项为集群外部访问地址:
https://sso.liujiajia.me/auth。