Spring Boot集成Keycloak:同时支持OAuth2登录与JWT认证可行吗?
Spring后端同时支持Keycloak SSO与JWT Bearer令牌访问的解决方案
我想用Keycloak保护Spring后端,已实现两种访问方式:
- 浏览器通过Keycloak SSO登录访问
- Postman携带JWT Bearer令牌访问
但两种方式无法共存:启用SSO配置时,Postman请求会返回登录页面;启用JWT资源服务器配置时,浏览器无法走SSO登录流程。请问能否让后端端点同时支持这两种认证方式?我尝试过不同的安全配置,但不确定是否可以混合使用。
尝试过的配置代码
@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { interface AuthoritiesConverter extends Converter<Map<String, Object>, Collection<GrantedAuthority>> {} @Bean AuthoritiesConverter realmRolesAuthoritiesConverter() { return claims -> { final var realmAccess = Optional.ofNullable((Map<String, Object>) claims.get("realm_access")); final var roles = realmAccess.flatMap(map -> Optional.ofNullable((List<String>) map.get("roles"))); return roles.map(List::stream).orElse(Stream.empty()).map(SimpleGrantedAuthority::new) .map(GrantedAuthority.class::cast).toList(); }; } // @Bean // GrantedAuthoritiesMapper authenticationConverter( // Converter<Map<String, Object>, Collection<GrantedAuthority>> realmRolesAuthoritiesConverter) { // return (authorities) -> authorities.stream() // .filter(authority -> authority instanceof OidcUserAuthority) // .map(OidcUserAuthority.class::cast).map(OidcUserAuthority::getIdToken) // .map(OidcIdToken::getClaims).map(realmRolesAuthoritiesConverter::convert) // .flatMap(roles -> roles.stream()).collect(Collectors.toSet()); // } // @Bean JwtAuthenticationConverter authenticationConverterJWT( Converter<Map<String, Object>, Collection<GrantedAuthority>> authoritiesConverter) { var authenticationConverter = new JwtAuthenticationConverter(); authenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> { return authoritiesConverter.convert(jwt.getClaims()); }); return authenticationConverter; } // @Bean // SecurityFilterChain clientSecurityFilterChain(HttpSecurity http, // ClientRegistrationRepository clientRegistrationRepository) throws Exception { // http.oauth2Login(Customizer.withDefaults()); // http.logout((logout) -> { // final var logoutSuccessHandler = // new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository); // logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}/"); // logout.logoutSuccessHandler(logoutSuccessHandler); // }); // // http.authorizeHttpRequests(requests -> { // requests.requestMatchers("/", "/favicon.ico", "/v3/api-docs/**","/swagger-ui/**", "/swagger-ui.html" ).permitAll(); // // requests.anyRequest().authenticated(); // }); // // return http.build(); // } // @Bean SecurityFilterChain resourceServerSecurityFilterChain( HttpSecurity http, Converter<Jwt, AbstractAuthenticationToken> authenticationConverter) throws Exception { http.oauth2ResourceServer(resourceServer -> { resourceServer.jwt(jwtDecoder -> { jwtDecoder.jwtAuthenticationConverter(authenticationConverter); }); }); http.sessionManagement(sessions -> { sessions.sessionCreationPolicy(SessionCreationPolicy.STATELESS); }).csrf(csrf -> { csrf.disable(); }); http.authorizeHttpRequests(requests -> { requests.requestMatchers("/", "/favicon.ico", "/v3/api-docs/**","/swagger-ui/**", "/swagger-ui.html" ).permitAll(); requests.anyRequest().authenticated(); }); return http.build(); } }
解决方案
通过配置多个不同优先级的安全过滤器链可以实现两种认证方式共存:检查请求头是否包含Bearer令牌,若是则使用资源服务器链处理,否则使用OAuth2登录链处理。
最终配置代码
@Bean @Order(Ordered.LOWEST_PRECEDENCE) SecurityFilterChain clientSecurityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) throws Exception { http.oauth2Login(Customizer.withDefaults()); http.logout((logout) -> { final var logoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository); logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}/"); logout.logoutSuccessHandler(logoutSuccessHandler); }); http.authorizeHttpRequests(requests -> { requests.requestMatchers("/", "/favicon.ico", "/v3/api-docs/**","/swagger-ui/**", "/swagger-ui.html" ).permitAll(); requests.anyRequest().authenticated(); }); return http.build(); } @Bean @Order(Ordered.LOWEST_PRECEDENCE - 1) SecurityFilterChain resourceServerSecurityFilterChain( HttpSecurity http, Converter<Jwt, AbstractAuthenticationToken> authenticationConverter) throws Exception { http.securityMatcher((HttpServletRequest request) -> { return Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION)).map(h -> { return h.toLowerCase().startsWith("bearer "); }).orElse(false); }); http.oauth2ResourceServer(resourceServer -> { resourceServer.jwt(jwtDecoder -> { jwtDecoder.jwtAuthenticationConverter(authenticationConverter); }); }); http.sessionManagement(sessions -> { sessions.sessionCreationPolicy(SessionCreationPolicy.STATELESS); }).csrf(csrf -> { csrf.disable(); }); http.authorizeHttpRequests(requests -> { requests.requestMatchers("/", "/favicon.ico", "/v3/api-docs/**","/swagger-ui/**", "/swagger-ui.html" ).permitAll(); requests.anyRequest().authenticated(); }); return http.build(); }
关键说明
- 过滤器链优先级:通过
@Order注解设置优先级,资源服务器链(LOWEST_PRECEDENCE -1)比SSO登录链(LOWEST_PRECEDENCE)优先级更高,会先被匹配。 - 请求匹配规则:资源服务器链通过
securityMatcher判断请求头是否包含Authorization: Bearer ...,符合条件的请求才会走JWT认证流程。 - SSO登录链:处理不携带Bearer令牌的请求,引导浏览器走Keycloak SSO登录流程。
内容的提问来源于stack exchange,提问作者Chemmic
相关产品推荐
相关产品推荐

