Keycloak SAML搭配Tomcat适配器实现SSO时POST流程无法获取SAML Assertion
核心错误1:业务Servlet占用了适配器默认的SAML断言接收端点
Keycloak Tomcat SAML适配器默认将 /saml 作为SAML Assertion的接收(ACS)端点,IdP(Keycloak)发回的POST请求会优先被适配器拦截处理:完成签名校验、断言解析、本地会话建立后,直接302跳转到用户最初访问的受保护资源,全程不会转发到你自定义的业务Servlet。
你在访问日志中看到的 POST /simple/saml 返回302,就是适配器处理完断言后的跳转动作,并非你的AuthServlet执行结果,因此永远不会输出POSTing日志。
修正方式:
- 将AuthServlet的映射路径从
/saml改为非保留路径,例如/userInfo:
<servlet-mapping> <servlet-name>AuthServlet</servlet-name> <url-pattern>/userInfo</url-pattern> </servlet-mapping>
错误2:login-config配置与Keycloak SAML适配器不兼容
你当前配置的<auth-method>BASIC</auth-method>是Servlet容器原生的基础认证配置,完全无法触发Keycloak SAML适配器的认证逻辑。
修正方式:
将login-config替换为SAML适配器专属的认证方法,realm-name需和适配器配置文件中定义的领域名称一致:
<login-config> <auth-method>KEYCLOAK_SAML</auth-method> <realm-name>你的Keycloak SAML领域名称</realm-name> </login-config>
错误3:未完成适配器的必备组件配置
Tomcat版Keycloak SAML适配器无法仅靠web.xml生效,还需要两个核心配置:
- 在应用的
WEB-INF/目录下添加适配器配置文件keycloak-saml.xml,填写Keycloak领域元数据、客户端ID、ACS地址、断言签名校验密钥等信息 - 给Web应用配置SAML认证Valve,可在应用的
META-INF/context.xml中添加:
<Valve className="org.keycloak.adapters.saml.tomcat.SamlAuthenticator"/>
同时需将对应版本的Keycloak Tomcat SAML适配器jar包放到Tomcat的lib目录下(不可仅放到应用的WEB-INF/lib,Valve类需要被容器类加载器加载)。
错误4:ACS端点未配置为公开访问
你当前的安全约束/*会将/saml端点标记为受保护资源,未建立会话的请求(也就是Keycloak发回的SAML POST请求)可能被拦截触发登录跳转死循环。
修正方式:
在原有安全约束前添加单独的规则,将/saml设为公开:
<security-constraint> <web-resource-collection> <web-resource-name>SAML ACS端点</web-resource-name> <url-pattern>/saml</url-pattern> </web-resource-collection> <!-- 无auth-constraint即代表所有用户可访问 --> </security-constraint>
错误5:SamlPrincipal获取逻辑不规范
SAML断言解析完成后会直接绑定到已建立的会话中,不需要在ACS端点处理。修改Servlet路径后,可在业务路径的请求中直接获取,建议增加类型判断避免异常:
public class AuthServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (request.getUserPrincipal() instanceof SamlPrincipal) { SamlPrincipal samlPrincipal = (SamlPrincipal) request.getUserPrincipal(); samlPrincipal.getAttributeNames().asIterator().forEachRemaining(attr -> { String value = samlPrincipal.getAttribute(attr); getServletContext().log("key: " + attr + " value: " + value); }); } // 后续业务逻辑... } }
内容的提问来源于stack exchange,提问作者Dennis Jansky

