使用 Keycloak 进行 Spring-boot 单点注销

问题描述

说明

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

问题在于单点退出

我在网上搜索了文档和问题,但一无所获。 我有以下日志描述的三个失败场景。

最后的问题是:

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

示例我如何理解 SSOut 应该工作:

  • 用户在应用 A 中点击“退出
  • App A 结束会话
  • 应用 A 通知 Keycloak
  • Keycloak 通过反向通道注销通知应用程序 B
  • 应用 B 结束会话

安全配置

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

'pr'

日志 A

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

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

import java.util.Arrays;
import java.util.List;

import javax.annotation.postconstruct;

import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.filter.KeycloakSecurityContextRequestFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.logout.logoutFilter;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;

@Profile("KC")
@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
class SecurityConfigurationKeycloak extends KeycloakWebSecurityConfigurerAdapter implements EnvironmentAware {

    private static final Logger LOG = LoggerFactory.getLogger(SecurityConfigurationKeycloak.class);
    
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        // SimpleAuthorityMapper is used to remove the ROLE_* conventions defined by
        // Java so we can use only admin or user instead of ROLE_ADMIN and ROLE_USER
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    @Bean
    public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {


//      super.configure(http);

        http
            .csrf()
                .requireCsrfProtectionMatcher(keycloakCsrfRequestMatcher())
            .and()
                .sessionManagement()
                .sessionAuthenticationStrategy(sessionAuthenticationStrategy())
            .and()
                .addFilterBefore(keycloakPreAuthActionsFilter(),logoutFilter.class)
                .addFilterBefore(keycloakAuthenticationProcessingFilter(),logoutFilter.class)
                .addFilterafter(keycloakSecurityContextRequestFilter(),SecurityContextHolderAwareRequestFilter.class)
                .addFilterafter(keycloakAuthenticatedActionsRequestFilter(),KeycloakSecurityContextRequestFilter.class)
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
            /*
             * logoUT
             */
            .and()
                .logout()
                    .addlogoutHandler(keycloaklogoutHandler())
                    .logoutUrl("/sso/logout").permitAll()
                    .logoutSuccessUrl("/")

            .and()
                .authorizeRequests()

                /*
                 * ADMIN
                 */
                .antMatchers(
                        "/admin/**"
                    )
                .hasRole("ADMIN")

                /*
                 * PUBLIC
                 */
                .antMatchers(
                        "/webjars/**","/css/**","/img/**","/favicon.ico","/**")
                .permitAll();
    }

    @Override
    public void setEnvironment(Environment environment) {
        // Todo Auto-generated method stub
    }
}

日志 B

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

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

日志 C

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

o.k.adapters.PreAuthActionsHandler       : adminRequest http://domain.tld/k_logout
o.k.adapters.PreAuthActionsHandler       : admin request Failed,unable to verify token: Failed to parse JWT
o.k.adapters.PreAuthActionsHandler       : Failed to parse JWT

org.keycloak.common.VerificationException: Failed to parse JWT
    at org.keycloak.TokenVerifier.parse(TokenVerifier.java:402) ~[keycloak-core-12.0.1.jar:12.0.1]
    at org.keycloak.TokenVerifier.getHeader(TokenVerifier.java:423) ~[keycloak-core-12.0.1.jar:12.0.1]
    at org.keycloak.adapters.rotation.AdapterTokenVerifier.createVerifier(AdapterTokenVerifier.java:110) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]
    at org.keycloak.adapters.PreAuthActionsHandler.verifyAdminRequest(PreAuthActionsHandler.java:210) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]
    at org.keycloak.adapters.PreAuthActionsHandler.handlelogout(PreAuthActionsHandler.java:140) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]
    at org.keycloak.adapters.PreAuthActionsHandler.handleRequest(PreAuthActionsHandler.java:80) ~[keycloak-adapter-core-12.0.1.jar:12.0.1]
    at org.keycloak.adapters.tomcat.AbstractKeycloakAuthenticatorValve.invoke(AbstractKeycloakAuthenticatorValve.java:177) ~[spring-boot-container-bundle-12.0.1.jar:12.0.1]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:888) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1597) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at org.apache.tomcat.util.net.socketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.41.jar:9.0.41]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
Caused 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')
 at [Source: (byte[])"��(���G��쉅����IL��؈�������耉)]P��������耉�
8�UL�ōYY����}=�!]�! �1]�}���1i���1]ጉ�"; line: 1,column: 2]
    at org.keycloak.jose.jws.JWSInput.<init>(JWSInput.java:58) ~[keycloak-core-12.0.1.jar:12.0.1]
    at org.keycloak.TokenVerifier.parse(TokenVerifier.java:400) ~[keycloak-core-12.0.1.jar:12.0.1]
    ... 19 common frames omitted
Caused by: com.fasterxml.jackson.core.JsonParseException: Unexpected character ((CTRL-CHAR,column: 2]
    at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:1851) ~[jackson-core-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:707) ~[jackson-core-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.core.base.ParserMinimalBase._reportUnexpectedChar(ParserMinimalBase.java:632) ~[jackson-core-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._handleUnexpectedValue(UTF8StreamJsonParser.java:2686) ~[jackson-core-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._nextTokennotinObject(UTF8StreamJsonParser.java:865) ~[jackson-core-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser.nextToken(UTF8StreamJsonParser.java:757) ~[jackson-core-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:4664) ~[jackson-databind-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4513) ~[jackson-databind-2.11.3.jar:2.11.3]
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3529) ~[jackson-databind-2.11.3.jar:2.11.3]
    at org.keycloak.util.JsonSerialization.readValue(JsonSerialization.java:71) ~[keycloak-core-12.0.1.jar:12.0.1]
    at org.keycloak.jose.jws.JWSInput.<init>(JWSInput.java:56) ~[keycloak-core-12.0.1.jar:12.0.1]
    ... 20 common frames omitted

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

Keycloak config

pom.xml

o.k.adapters.PreAuthActionsHandler       : adminRequest http://192.168.178.31:8090/sso/logout
.k.a.t.AbstractAuthenticatedActionsValve : AuthenticatedActionsValve.invoke /sso/logout
o.k.a.AuthenticatedActionsHandler        : AuthenticatedActionsValve.invoke http://192.168.178.31:8090/sso/logout
o.k.a.AuthenticatedActionsHandler        : Policy enforcement is disabled.
o.s.security.web.FilterChainProxy        : Securing POST /sso/logout
s.s.w.c.SecurityContextPersistenceFilter : Set SecurityContextHolder to empty SecurityContext
o.k.adapters.PreAuthActionsHandler       : adminRequest http://192.168.178.31:8090/sso/logout
o.s.s.w.a.logout.logoutFilter            : Logging out [null]
o.k.a.s.a.KeycloaklogoutHandler          : Cannot log out without authentication
o.s.s.web.DefaultRedirectStrategy        : Redirecting to /
w.c.HttpSessionSecurityContextRepository : Did not store empty SecurityContext
s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)