为什么从Spring授权服务器注销时session没有被删除?

问题描述 投票:0回答:1

我使用带有 Keycloak 的 Spring 授权服务器进行社交登录,并将其设置为 OIDC (OpenID Connect)。我的问题是注销端点。我没有自定义任何端点,只是 ID 和访问令牌。

我关注:

但对我来说没有任何作用。

我涉及四个主要实体:

  • Client:在 Spring 授权服务器上注册为机密 OIDC 客户端的 React Web 应用程序。
  • 资源服务器:具有安全 REST 端点的 Spring Boot 应用程序,使用 Spring 授权服务器作为其身份提供者。
  • Keycloak:充当社交登录提供商。
  • Spring Authorization Server:React/资源服务器的授权服务器和Keycloak中用于身份验证的机密客户端。

我在“pom.xml”中使用 Maven 和以下版本:

  • 春季启动3.3.3

  • Spring 授权服务器:1.3.2

  • OAuth2 客户端(Spring Security):6.3.3

当我在 Postman 中测试注销端点(/connect/logout)时,它会重定向到登录页面而不注销。访问令牌仍然适用于资源服务器(我预计会出现 403 错误)。 此外,当在前端使用注销时,它会带我返回主页,而不会清除用户会话或显示登录页面。

我尝试调试这个,我发现

SecurityContextHolder.getContext().getAuthentication();
返回null,而主体是
ANONYMOUS_AUTHENTICATION

我错过了什么?任何帮助将不胜感激!

我的配置如下:

@Configuration
@EnableWebSecurity
@EnableJdbcHttpSession
public class AuthorizationServerConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationSecurityFilterChain(HttpSecurity http) throws Exception {

        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());    // Enable OpenID Connect 1.0

        http
                .sessionManagement(sessionManagement ->
                        sessionManagement
                                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                                .maximumSessions(1)
                                .sessionRegistry(sessionRegistry())
                )
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/keycloak"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                )
                .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository)
            throws Exception {
        http
                .authorizeHttpRequests((authorize) -> authorize
                        .anyRequest().authenticated()
                )
                .csrf(csrf -> csrf.ignoringRequestMatchers(
                        "/userinfo",
                        "/oauth2/token",
                        "/oauth2/jwks"
                ))
                .oauth2Login(Customizer.withDefaults())
                .logout(logoutConfigurer -> logoutConfigurer
                        .logoutUrl("/connect/logout")
                        .clearAuthentication(true)
                        .invalidateHttpSession(true)
                        .deleteCookies("JSESSIONID")
                        .logoutSuccessHandler(oidcLogoutSuccessHandler(clientRegistrationRepository)));
        return http.build();
    }


    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID("2978b2d6-1205-40b5-98df-7e6fbf5f10d0")
//                .algorithm(com.nimbusds.jose.JWSAlgorithm.RS256)
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder()
                .multipleIssuersAllowed(true)
                .build();
    }


    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> idTokenCustomizer() {
        return new FederatedIdentityIdTokenCustomizer();
    }


    @Bean
    OAuth2AuthorizationService jdbcOAuth2AuthorizationService(
            JdbcOperations jdbcOperations,
            RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcOperations, registeredClientRepository);
    }

    @Bean
    OAuth2AuthorizationConsentService jdbcOAuth2AuthorizationConsentService(
            JdbcOperations jdbcOperations,
            RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcOperations, registeredClientRepository);
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }

    private LogoutSuccessHandler oidcLogoutSuccessHandler(ClientRegistrationRepository clientRegistrationRepository) {
        OidcClientInitiatedLogoutSuccessHandler oidcClientInitiatedLogoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
        oidcClientInitiatedLogoutSuccessHandler.setPostLogoutRedirectUri("/oauth2/authorization/keycloak");
        return oidcClientInitiatedLogoutSuccessHandler;
    }
}

客户端注册bean:

    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        RegisteredClient exampleClient = RegisteredClient.withId("2bee2e72-af4t-43ad-tb2q-0b11501b4a15")
                .clientId("example-client")
                .clientSecret("{noop}password")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://localhost:3000/api/auth/callback/keycloak")
                .redirectUri("https://oauth.pstmn.io/v1/callback")
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE).scope(OidcScopes.EMAIL)
                .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofDays(10))
                        .refreshTokenTimeToLive(Duration.ofDays(8)).reuseRefreshTokens(false)
                        .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
                        .x509CertificateBoundAccessTokens(true)
                        .build()).build();

        JdbcRegisteredClientRepository registeredClientRepository =
                new JdbcRegisteredClientRepository(jdbcTemplate);
        registeredClientRepository.save(exampleClient);

        return registeredClientRepository;
    }

id 令牌和访问自定义:

public final class FederatedIdentityIdTokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {

    private static final Set<String> ID_TOKEN_CLAIMS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
            IdTokenClaimNames.ISS,
            IdTokenClaimNames.SUB,
            IdTokenClaimNames.AUD,
            IdTokenClaimNames.EXP,
            IdTokenClaimNames.IAT,
            IdTokenClaimNames.AUTH_TIME,
            IdTokenClaimNames.NONCE,
            IdTokenClaimNames.ACR,
            IdTokenClaimNames.AMR,
            IdTokenClaimNames.AZP,
            IdTokenClaimNames.AT_HASH,
            IdTokenClaimNames.C_HASH
    )));

    @Override
    public void customize(JwtEncodingContext context) {
        if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
            Map<String, Object> thirdPartyClaims = extractClaims(context.getPrincipal());
            context.getClaims().claims(existingClaims -> {
                // Remove conflicting claims set by this authorization server
                existingClaims.keySet().forEach(thirdPartyClaims::remove);

                // Remove standard id_token claims that could cause problems with clients
                ID_TOKEN_CLAIMS.forEach(thirdPartyClaims::remove);

                // Add all other claims directly to id_token
                existingClaims.putAll(thirdPartyClaims);
            });
        }

        if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
            Authentication principal = context.getPrincipal();

            if (principal.getPrincipal() instanceof OAuth2User) {
                OAuth2User oauth2User = (OAuth2User) principal.getPrincipal();
                Map<String, Object> attributes = oauth2User.getAttributes();

                context.getClaims().claim("email", attributes.get("email"));
                context.getClaims().claim("name", attributes.get("name"));
                context.getClaims().claim("email_verified", attributes.get("email_verified"));
                context.getClaims().claim("family_name", attributes.get("family_name"));
            }
        }
    }

    private Map<String, Object> extractClaims(Authentication principal) {
        Map<String, Object> claims;
        if (principal.getPrincipal() instanceof OidcUser) {
            OidcUser oidcUser = (OidcUser) principal.getPrincipal();
            OidcIdToken idToken = oidcUser.getIdToken();
            claims = idToken.getClaims();
        } else if (principal.getPrincipal() instanceof OAuth2User) {
            OAuth2User oauth2User = (OAuth2User) principal.getPrincipal();
            claims = oauth2User.getAttributes();
        } else {
            claims = Collections.emptyMap();
        }

        return new HashMap<>(claims);
    }
}

我的尝试和期望

我认为问题可能是会话没有被保存,因为

SecurityContextHolder.getContext().getAuthentication();
返回 null。我使用 JDBC 来存储会话,但它仍然不会删除会话,并且用户也不会注销。

对于这么长的消息,我深表歉意,非常感谢您提供的任何帮助!

spring-boot session spring-security spring-authorization-server
1个回答
0
投票

使用 OAuth2AuthorizationService 删除或撤销访问令牌、刷新令牌和其他 OAuth2 授权信息,确保令牌在注销过程中正确失效或撤销。

© www.soinside.com 2019 - 2024. All rights reserved.