使用 Keycloak 的 Spring-boot 单点注销

der*_*bby 5 csrf spring-boot keycloak single-logout

描述

\n

我创建了一个使用 Keycloak 12.0.1 作为身份提供程序的应用程序。\n单点登录工作正常,“本地注销”也很好。

\n

问题是单点退出

\n

我在网上搜索了文档和问题,但一无所获。\n下面的日志描述了三种失败的情况。

\n

最后的问题是:

\n
    \n
  • 我究竟做错了什么?
  • \n
  • 我必须如何在我的应用程序中实现反向通道注销?
  • \n
\n

我如何理解 SSOut 应该起作用的示例:

\n
    \n
  • 用户在应用程序 A 中单击“注销”
  • \n
  • 应用程序A结束会话
  • \n
  • App A通知Keycloak
  • \n
  • Keycloak 通过反向通道注销通知 App B
  • \n
  • 应用程序 B 结束会话
  • \n
\n

安全配置

\n

keycloakCsrfRequestMatcher() 方法将库拥有的端点(如“k_logout”)从 csrf 保护中释放,但不是我自己的 url“/sso/logout”。也许可以编写我自己的匹配器,但这超出了我作为开发人员的经验。

\n
import java.util.Arrays;\nimport java.util.List;\n\nimport javax.annotation.PostConstruct;\n\nimport org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;\nimport org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;\nimport org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;\nimport org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;\nimport org.keycloak.adapters.springsecurity.filter.KeycloakSecurityContextRequestFilter;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.context.EnvironmentAware;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.ComponentScan;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.annotation.Profile;\nimport org.springframework.core.env.Environment;\nimport org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;\nimport org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;\nimport org.springframework.security.core.session.SessionRegistryImpl;\nimport org.springframework.security.web.authentication.logout.LogoutFilter;\nimport org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;\nimport org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;\nimport org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;\n\n@Profile("KC")\n@Configuration\n@EnableWebSecurity\n@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)\nclass SecurityConfigurationKeycloak extends KeycloakWebSecurityConfigurerAdapter implements EnvironmentAware {\n\n    private static final Logger LOG = LoggerFactory.getLogger(SecurityConfigurationKeycloak.class);\n    \n    @Autowired\n    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {\n        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();\n        // SimpleAuthorityMapper is used to remove the ROLE_* conventions defined by\n        // Java so we can use only admin or user instead of ROLE_ADMIN and ROLE_USER\n        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());\n        auth.authenticationProvider(keycloakAuthenticationProvider);\n    }\n\n    @Bean\n    public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {\n        return new KeycloakSpringBootConfigResolver();\n    }\n\n    @Bean\n    @Override\n    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {\n        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());\n    }\n\n    @Override\n    protected void configure(HttpSecurity http) throws Exception {\n\n\n//      super.configure(http);\n\n        http\n            .csrf()\n                .requireCsrfProtectionMatcher(keycloakCsrfRequestMatcher())\n            .and()\n                .sessionManagement()\n                .sessionAuthenticationStrategy(sessionAuthenticationStrategy())\n            .and()\n                .addFilterBefore(keycloakPreAuthActionsFilter(), LogoutFilter.class)\n                .addFilterBefore(keycloakAuthenticationProcessingFilter(), LogoutFilter.class)\n                .addFilterAfter(keycloakSecurityContextRequestFilter(), SecurityContextHolderAwareRequestFilter.class)\n                .addFilterAfter(keycloakAuthenticatedActionsRequestFilter(), KeycloakSecurityContextRequestFilter.class)\n                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())\n            /*\n             * LOGOUT\n             */\n            .and()\n                .logout()\n                    .addLogoutHandler(keycloakLogoutHandler())\n                    .logoutUrl("/sso/logout").permitAll()\n                    .logoutSuccessUrl("/")\n\n            .and()\n                .authorizeRequests()\n\n                /*\n                 * ADMIN\n                 */\n                .antMatchers(\n                        "/admin/**"\n                    )\n                .hasRole("ADMIN")\n\n                /*\n                 * PUBLIC\n                 */\n                .antMatchers(\n                        "/webjars/**",\n                        "/css/**",\n                        "/img/**",\n                        "/favicon.ico",\n                        "/**")\n                .permitAll();\n    }\n\n    @Override\n    public void setEnvironment(Environment environment) {\n        // TODO Auto-generated method stub\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

日志A

\n

正如我们所看到的,当 KC 尝试访问“/sso/logout”url 时,出现 CSRF 错误。\n但我不知道这是否是在 KC 中使用的正确端点?\n我在中找到了“/k_logout”使用的库,对我来说似乎是一些“内部重定向”url。

\n

(为了方便起见,删除了日期等。)

\n
o.k.adapters.PreAuthActionsHandler       : adminRequest http://domain.tld/sso/logout\n.k.a.t.AbstractAuthenticatedActionsValve : AuthenticatedActionsValve.invoke /sso/logout\no.k.a.AuthenticatedActionsHandler        : AuthenticatedActionsValve.invoke http://domain.tld/sso/logout\no.k.a.AuthenticatedActionsHandler        : Policy enforcement is disabled.\no.s.security.web.FilterChainProxy        : Securing POST /sso/logout\ns.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext\no.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for https://domain.tld/sso/logout\no.s.s.w.access.AccessDeniedHandlerImpl   : Responding with 403 status code\nw.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext\ns.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request\no.s.security.web.FilterChainProxy        : Securing POST /error\ns.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext\no.k.adapters.PreAuthActionsHandler       : adminRequest https://domain.tld/error\no.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext\no.s.s.w.a.i.FilterSecurityInterceptor    : Authorized filter invocation [POST /error] with attributes [permitAll]\no.s.security.web.FilterChainProxy        : Secured POST /error\ne.p.p.controller.CustomErrorController   : User was not authorized for requested site: /error\nw.c.HttpSessionSecurityContextRepository : Did not store anonymous SecurityContext\ns.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request\n
Run Code Online (Sandbox Code Playgroud)\n

日志B

\n

如果我在 KC 中使用“/k_logout”端点,则会在我的应用程序中收到 JWT 解析错误。\n我尝试对其进行调试,似乎在 org.keycloak.jose.jws.JWSInput 中,encodedHeader 的前缀为“logout_token=” “这似乎是问题所在。至少对我来说。:-)

\n
o.k.adapters.PreAuthActionsHandler       : adminRequest http://domain.tld/k_logout\no.k.adapters.PreAuthActionsHandler       : admin request failed, unable to verify token: Failed to parse JWT\no.k.adapters.PreAuthActionsHandler       : Failed to parse JWT\n\norg.keycloak.common.VerificationException: Failed to parse JWT\n    at org.keycloak.TokenVerifier.parse(TokenVerifier.java:402) ~[keycloak-core-12.0.1.jar:12.0.1]\n    at org.keycloak.TokenVerifier.getHeader(TokenVerifier.java:423) ~[keycloak-core-12.0.1.jar:12.0.1]\n    at org.keycloak.adapters.rotation.AdapterTokenVerifier.createVerifier(AdapterTokenVerifier.java:110) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]\n    at org.keycloak.adapters.PreAuthActionsHandler.verifyAdminRequest(PreAuthActionsHandler.java:210) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]\n    at org.keycloak.adapters.PreAuthActionsHandler.handleLogout(PreAuthActionsHandler.java:140) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]\n    at org.keycloak.adapters.PreAuthActionsHandler.handleRequest(PreAuthActionsHandler.java:80) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]\n    at org.keycloak.adapters.tomcat.AbstractKeycloakAuthenticatorValve.invoke(AbstractKeycloakAuthenticatorValve.java:177) ~[spring-boot-container-bundle-12.0.1.jar:12.0.1]\n    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) ~[tomcat-embed-core-9.0.41.jar:9.0.41]\n    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.41.jar:9.0.41]\n    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.41.jar:9.0.41]\n    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) ~[tomcat-embed-core-9.0.41.jar:9.0.41]\n    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) ~[tomcat-embed-core-9.0.41.jar:9.0.41]\n    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.41.jar:9.0.41]\n    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:888) ~[tomcat-embed-core-9.0.41.jar:9.0.41]\n    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1597) ~[tomcat-embed-core-9.0.41.jar:9.0.41]\n    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.41.jar:9.0.41]\n    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]\n    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]\n    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.41.jar:9.0.41]\n    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]\nCaused by: org.keycloak.jose.jws.JWSInputException: com.fasterxml.jackson.core.JsonParseException: Unexpected character ((CTRL-CHAR, code 150)): expected a valid value (JSON String, Number, Array, Object or token \'null\', \'true\' or \'false\')\n at [Source: (byte[])"\xef\xbf\xbd\xef\xbf\xbd(\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbdG\xef\xbf\xbd\xef\xbf\xbd\xec\x89\x85\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbdIL\xef\xbf\xbd\xef\xbf\xbd\xd8\x88\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xe8\x80\x89)]P\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xe8\x80\x89\xef\xbf\xbd\n8\xef\xbf\xbdUL\xef\xbf\xbd\xc5\x8dYY\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd}=\xef\xbf\xbd!]\xef\xbf\xbd! \xef\xbf\xbd1]\xef\xbf\xbd}\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd1i\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd1]\xe1\x8c\x89\xef\xbf\xbd"; line: 1, column: 2]\n    at org.keycloak.jose.jws.JWSInput.<init>(JWSInput.java:58) ~[keycloak-core-12.0.1.jar:12.0.1]\n    at org.keycloak.TokenVerifier.parse(TokenVerifier.java:400) ~[keycloak-core-12.0.1.jar:12.0.1]\n    ... 19 common frames omitted\nCaused by: com.fasterxml.jackson.core.JsonParseException: Unexpected character ((CTRL-CHAR, code 150)): expected a valid value (JSON String, Number, Array, Object or token \'null\', \'true\' or \'false\')\n at [Source: (byte[])"\xef\xbf\xbd\xef\xbf\xbd(\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbdG\xef\xbf\xbd\xef\xbf\xbd\xec\x89\x85\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbdIL\xef\xbf\xbd\xef\xbf\xbd\xd8\x88\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xe8\x80\x89)]P\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xe8\x80\x89\xef\xbf\xbd\n8\xef\xbf\xbdUL\xef\xbf\xbd\xc5\x8dYY\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd}=\xef\xbf\xbd!]\xef\xbf\xbd! \xef\xbf\xbd1]\xef\xbf\xbd}\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd1i\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd1]\xe1\x8c\x89\xef\xbf\xbd"; line: 1, column: 2]\n    at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:1851) ~[jackson-core-2.11.3.jar:2.11.3]\n    at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:707) ~[jackson-core-2.11.3.jar:2.11.3]\n    at com.fasterxml.jackson.core.base.ParserMinimalBase._reportUnexpectedChar(ParserMinimalBase.java:632) ~[jackson-core-2.11.3.jar:2.11.3]\n    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._handleUnexpectedValue(UTF8StreamJsonParser.java:2686) ~[jackson-core-2.11.3.jar:2.11.3]\n    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._nextTokenNotInObject(UTF8StreamJsonParser.java:865) ~[jackson-core-2.11.3.jar:2.11.3]\n    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser.nextToken(UTF8StreamJsonParser.java:757) ~[jackson-core-2.11.3.jar:2.11.3]\n    at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:4664) ~[jackson-databind-2.11.3.jar:2.11.3]\n    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4513) ~[jackson-databind-2.11.3.jar:2.11.3]\n    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3529) ~[jackson-databind-2.11.3.jar:2.11.3]\n    at org.keycloak.util.JsonSerialization.readValue(JsonSerialization.java:71) ~[keycloak-core-12.0.1.jar:12.0.1]\n    at org.keycloak.jose.jws.JWSInput.<init>(JWSInput.java:56) ~[keycloak-core-12.0.1.jar:12.0.1]\n    ... 20 common frames omitted\n
Run Code Online (Sandbox Code Playgroud)\n

日志C

\n

如果我使用 .csrf().disable() 完全禁用应用程序的 csrf 保护,则上述错误显然不再存在。相反,应用程序无法将注销请求映射到用户。

\n
o.k.adapters.PreAuthActionsHandler       : adminRequest http://192.168.178.31:8090/sso/logout\n.k.a.t.AbstractAuthenticatedActionsValve : AuthenticatedActionsValve.invoke /sso/logout\no.k.a.AuthenticatedActionsHandler        : AuthenticatedActionsValve.invoke http://192.168.178.31:8090/sso/logout\no.k.a.AuthenticatedActionsHandler        : Policy enforcement is disabled.\no.s.security.web.FilterChainProxy        : Securing POST /sso/logout\ns.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext\no.k.adapters.PreAuthActionsHandler       : adminRequest http://192.168.178.31:8090/sso/logout\no.s.s.w.a.logout.LogoutFilter            : Logging out [null]\no.k.a.s.a.KeycloakLogoutHandler          : Cannot log out without authentication\no.s.s.web.DefaultRedirectStrategy        : Redirecting to /\nw.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext\ns.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request\n
Run Code Online (Sandbox Code Playgroud)\n

用于反向通道注销的 Keycloak 配置

\n

钥匙斗篷配置

\n

pom.xml

\n
<?xml version="1.0" encoding="UTF-8"?>\n<project xmlns="http://maven.apache.org/POM/4.0.0"\n    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>tld.domain</groupId>\n    <artifactId>artifact</artifactId>\n    <version>0.0.1-SNAPSHOT</version>\n    <packaging>jar</packaging>\n\n    <name>name</name>\n    <description></description>\n\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>2.4.1</version>\n        <relativePath /> <!-- lookup parent from repository -->\n    </parent>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <java.version>11</java.version>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-jpa</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-security</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.keycloak</groupId>\n            <artifactId>keycloak-spring-boot-starter</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-thymeleaf</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-validation</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-mail</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.session</groupId>\n            <artifactId>spring-session-jdbc</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.thymeleaf.extras</groupId>\n            <artifactId>thymeleaf-extras-springsecurity5</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.session</groupId>\n            <artifactId>spring-session-core</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-configuration-processor</artifactId>\n            <optional>true</optional>\n        </dependency>\n    </dependencies>\n\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>org.keycloak.bom</groupId>\n                <artifactId>keycloak-adapter-bom</artifactId>\n                <version>12.0.1</version>\n                <type>pom</type>\n                <scope>import</scope>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n</project>\n
Run Code Online (Sandbox Code Playgroud)\n