Spring Boot+Keycloak:特定客户端会话过期后强制SSO重新验证登录
解决方案
针对你遇到的应用会话过期后Keycloak自动登录的问题,以下是几个可行的解决办法:
1. 强制OAuth2认证时带上prompt=login参数
通过修改Spring Security的OAuth2登录配置,在认证请求中添加prompt=login参数,这样不管Keycloak端是否存在有效会话,都会强制用户重新输入凭证验证身份。
修改你的OAuth2LoginSecurityConfig,添加自定义的OAuth2AuthorizationRequestResolver:
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; public class OAuth2LoginSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) throws Exception { // 自定义授权请求解析器,添加prompt=login参数 OAuth2AuthorizationRequestResolver authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( clientRegistrationRepository, "/oauth2/authorization") { @Override protected OAuth2AuthorizationRequest customizeAuthorizationRequest( OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request) { Map<String, Object> additionalParameters = new HashMap<>(authorizationRequest.getAdditionalParameters()); additionalParameters.put("prompt", "login"); return OAuth2AuthorizationRequest.from(authorizationRequest) .additionalParameters(additionalParameters) .build(); } }; http.authorizeRequests() .antMatchers("/actuator/**").permitAll() .anyRequest().hasRole("special") .and() .oauth2Login(oauth2 -> oauth2 .authorizationEndpoint(endpoint -> endpoint .authorizationRequestResolver(authorizationRequestResolver))); return http.build(); } }
该配置会让每次触发OAuth2登录流程时,强制Keycloak要求用户重新登录。
2. 注销应用会话时同步注销Keycloak SSO会话
如果希望用户主动注销应用后,同时清除Keycloak端的SSO会话以避免自动登录,可以自定义LogoutHandler,调用Keycloak的注销端点。
首先,添加Keycloak Spring Boot Starter依赖(匹配你的Keycloak版本):
<dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-spring-boot-starter</artifactId> <version>22.0.5</version> </dependency>
实现自定义LogoutHandler:
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.KeycloakBuilder; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutHandler; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class KeycloakLogoutHandler implements LogoutHandler { private final Keycloak keycloakAdminClient; private final String realm = "master"; public KeycloakLogoutHandler() { // 初始化Keycloak Admin客户端,建议通过配置文件注入参数,避免硬编码 this.keycloakAdminClient = KeycloakBuilder.builder() .serverUrl("https://localhost") .realm(realm) .clientId("admin-cli") .username("admin") // 替换为你的Keycloak管理员账号 .password("admin-password") // 替换为管理员密码 .build(); } @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { if (authentication instanceof KeycloakAuthenticationToken token) { String userId = token.getAccount().getKeycloakSecurityContext().getToken().getSubject(); // 注销用户的SSO会话 keycloakAdminClient.realm(realm).users().get(userId).logout(); } } }
在SecurityFilterChain中配置注销逻辑:
@Bean public SecurityFilterChain filterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) throws Exception { // ... 保留原有授权配置 http.logout(logout -> logout .addLogoutHandler(new KeycloakLogoutHandler()) .logoutSuccessUrl("/")); return http.build(); }
3. 在Keycloak客户端配置中强制重新认证
直接在Keycloak控制台修改客户端配置,全局强制该客户端的用户重新认证:
- 进入Keycloak控制台,选择
masterrealm,找到你的my-Client客户端 - 切换到
Login Settings标签页 - 勾选
Require Reauthentication选项并保存
该配置会让Keycloak在每次客户端请求认证时,都要求用户重新验证,即使存在有效的SSO会话,对所有使用该客户端的应用生效。
4. 调整应用端会话超时与认证状态检查
配置应用会话超时,同时通过拦截器检查会话有效性,过期则引导用户重新登录:
在application.yml中添加会话超时配置:
server: servlet: session: timeout: 30m # 设置应用会话超时时间,例如30分钟
添加会话验证拦截器:
import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class SessionValidationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(false); // 应用会话不存在且非登录/健康检查路径时,重定向到登录页 if (session == null && !request.getRequestURI().startsWith("/oauth2/authorization") && !request.getRequestURI().startsWith("/actuator")) { response.sendRedirect("/oauth2/authorization/master"); return false; } return true; } }
注册拦截器:
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SessionValidationInterceptor()) .excludePathPatterns("/actuator/**", "/oauth2/**"); } }
内容的提问来源于stack exchange,提问作者Nordiii
相关产品推荐
相关产品推荐

