我使用带有 Keycloak 的 Spring 授权服务器进行社交登录,并将其设置为 OIDC (OpenID Connect)。我的问题是注销端点。我没有自定义任何端点,只是 ID 和访问令牌。
我关注:
这个问题
但对我来说没有任何作用。
我涉及四个主要实体:
我在“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 来存储会话,但它仍然不会删除会话,并且用户也不会注销。
对于这么长的消息,我深表歉意,非常感谢您提供的任何帮助!
使用 OAuth2AuthorizationService 删除或撤销访问令牌、刷新令牌和其他 OAuth2 授权信息,确保令牌在注销过程中正确失效或撤销。