Keycloak集成EHerkenning/EIDAS时SAML消息Base64解码报错
问题描述
我们正尝试使用SAML将Keycloak(20.0.1版本)与EHerkenning/EIDAS(荷兰/欧洲企业身份系统)身份提供商集成。可以正常连接到EHerkenning/EIDAS代理并显示登录页面,但发送至Keycloak端点的响应引发异常,Keycloak提示“Invalid response from identity provider.”。
Keycloak日志中记录的错误如下:
2022-11-09 14:01:30,735 ERROR [org.keycloak.saml.common] (executor-thread-0) Error in base64 decoding saml message.: java.lang.RuntimeException: PL00062: Parser : Unknown tag:EncryptedAttribute::location=Line number = 159 Column number = 252 System Id = null Public Id = null Location Uri= null CharacterOffset = 35248 at org.keycloak.saml.common.DefaultPicketLinkLogger.parserUnknownTag(DefaultPicketLinkLogger.java:359) at org.keycloak.saml.processing.core.parsers.saml.assertion.SAMLAttributeStatementParser.processSubElement(SAMLAttributeStatementParser.java:56) at org.keycloak.saml.processing.core.parsers.saml.assertion.SAMLAttributeStatementParser.processSubElement(SAMLAttributeStatementParser.java:31) at org.keycloak.saml.common.parsers.AbstractStaxParser.parse(AbstractStaxParser.java:97) at org.keycloak.saml.processing.core.parsers.saml.assertion.SAMLAssertionParser.processSubElement(SAMLAssertionParser.java:96) at org.keycloak.saml.processing.core.parsers.saml.assertion.SAMLAssertionParser.processSubElement(SAMLAssertionParser.java:34) at org.keycloak.saml.common.parsers.AbstractStaxParser.parse(AbstractStaxParser.java:97) at org.keycloak.saml.processing.core.parsers.saml.protocol.SAMLResponseParser.processSubElement(SAMLResponseParser.java:77) at org.keycloak.saml.processing.core.parsers.saml.protocol.SAMLResponseParser.processSubElement(SAMLResponseParser.java:38) at org.keycloak.saml.common.parsers.AbstractStaxParser.parse(AbstractStaxParser.java:97) at org.keycloak.saml.processing.core.parsers.saml.SAMLParser.parse(SAMLParser.java:123) at org.keycloak.saml.common.parsers.AbstractParser.parse(AbstractParser.java:98) at org.keycloak.saml.common.parsers.AbstractParser.parse(AbstractParser.java:102) at org.keycloak.saml.processing.api.saml.v2.response.SAML2Response.getSAML2ObjectFromStream(SAML2Response.java:389) at org.keycloak.saml.SAMLRequestParser.parseResponseDocument(SAMLRequestParser.java:104) at org.keycloak.broker.saml.SAMLEndpoint$PostBinding.extractResponseDocument(SAMLEndpoint.java:726) at org.keycloak.broker.saml.SAMLEndpoint$Binding.handleSamlResponse(SAMLEndpoint.java:624) at org.keycloak.broker.saml.SAMLEndpoint$Binding.execute(SAMLEndpoint.java:276) at org.keycloak.broker.saml.SAMLEndpoint.postBinding(SAMLEndpoint.java:187) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:170) at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:130) at org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:660) at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:524) at org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$2(ResourceMethodInvoker.java:474) at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:364) at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:476) at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:434) at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:192) at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:152) at org.jboss.resteasy.core.ResourceLocatorInvoker.invokeOnTargetObject(ResourceLocatorInvoker.java:183) at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:141) at org.jboss.resteasy.core.ResourceLocatorInvoker.invoke(ResourceLocatorInvoker.java:32) at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:492) at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:261) at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:161) at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:364) at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:164) at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:247) at io.quarkus.resteasy.runtime.standalone.RequestDispatcher.service(RequestDispatcher.java:73) at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.dispatch(VertxRequestHandler.java:151) at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.handle(VertxRequestHandler.java:82) at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.handle(VertxRequestHandler.java:42) at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1284) at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:173) at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:140) at io.quarkus.vertx.http.runtime.StaticResourcesRecorder$2.handle(StaticResourcesRecorder.java:84) at io.quarkus.vertx.http.runtime.StaticResourcesRecorder$2.handle(StaticResourcesRecorder.java:71) at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1284) at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:173) at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:140) at io.quarkus.vertx.http.runtime.VertxHttpRecorder$6.handle(VertxHttpRecorder.java:430) at io.quarkus.vertx.http.runtime.VertxHttpRecorder$6.handle(VertxHttpRecorder.java:408) at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1284) at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:173) at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:140) at org.keycloak.quarkus.runtime.integration.web.QuarkusRequestFilter.lambda$createBlockingHandler$0(QuarkusRequestFilter.java:82) at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:564) at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2449) at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1478) at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29) at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29) at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) at java.base/java.lang.Thread.run(Thread.java:829)
已尝试的操作:
- 使用SAML工具解码响应,但工具也无法解码该消息。
- 移除整个SAML响应中的换行符(包括加密的Base64哈希属性和响应本身),但所有修改都会导致Keycloak不接受新的Base64编码SAML响应,或无日志直接崩溃。
解决方案分析
从日志核心错误PL00062: Parser : Unknown tag:EncryptedAttribute可以看出,问题并非Base64解码失败,而是Keycloak的SAML解析器无法识别响应中的<EncryptedAttribute>标签,日志里的“base64解码错误”属于误导性提示。
核心原因
Keycloak 20.0.1版本的基础SAML解析逻辑未支持<EncryptedAttribute>元素——该元素属于SAML 2.0加密属性扩展,解析器遇到未定义的标签时直接抛出异常,导致整个响应处理失败。
解决步骤
- 调整IdP输出配置:检查EHerkenning/EIDAS代理是否支持关闭属性加密,改为返回明文属性。若能调整,这是最直接的解决方案。
- 升级Keycloak版本:Keycloak 21及以上版本已修复对
<EncryptedAttribute>的支持问题,升级到兼容版本可直接解决解析异常。 - 自定义解析扩展:若无法升级或修改IdP配置,可编写Keycloak扩展,扩展
SAMLAttributeStatementParser类,添加对<EncryptedAttribute>标签的处理逻辑,包括解密属性值并转换为普通SAML属性格式。 - 拦截修改SAML响应:在Keycloak与IdP之间添加代理,拦截SAML响应后移除
<EncryptedAttribute>节点,或转换为Keycloak可识别的格式。注意此方式需确保敏感属性的传输安全,避免明文泄露。
额外调试建议
- 启用Keycloak SAML详细日志:在配置文件中设置
logger.org.keycloak.saml=DEBUG,获取XML解析的详细过程,确认<EncryptedAttribute>的具体结构和位置。 - 抓包查看原始响应:用Wireshark或Fiddler捕获完整SAML响应,手动处理URL编码和换行符后进行Base64解码,直接查看XML结构,确认加密属性的存在形式。
内容的提问来源于stack exchange,提问作者Shwahlthy
相关产品推荐
相关产品推荐

