Spring Boot+Keycloak17多租户静态资源404及URL优化咨询
改造Spring Boot + Keycloak 17多租户应用遇到的问题
我正在将单体应用改造为基于Spring Boot、Keycloak 17的多租户微服务应用,采用路径关联Keycloak配置文件的方式。目前已实现JSON部署加载、登录功能,可正常解析URL中tenant后的租户标识(如http://localhost:8100/tenant/branch1/)。
核心问题
- 静态资源加载异常:访问含租户名的静态资源URL(如
http://localhost:8100/tenant/branch1/resources/bootstrap/js/bootstrap.min.js)返回404,JSP中仍按原有方式引入资源:<link rel="stylesheet" href="resources/bootstrap/css/bootstrap.min.css"> - 认证后URL优化:希望认证后返回不含
tenant/branch1的URL,当前应用使用CurrentTenantIdentifierResolver。
相关配置代码
Keycloak配置文件示例
{"realm": "branch1", "auth-server-url": "http://localhost:8181/", "ssl-required": "external", "resource": "app", "public-client": true, "confidential-port": 0, "principal-attribute": "preferred_username"}
PathBasedConfigResolver类代码
@ConditionalOnProperty(prefix = "keycloak.config", name = "resolver", havingValue = "path") public class PathBasedConfigResolver implements KeycloakConfigResolver { private final ConcurrentHashMap<String, KeycloakDeployment> cache = new ConcurrentHashMap<>(); @SuppressWarnings("unused") private static AdapterConfig adapterConfig; @Override public KeycloakDeployment resolve(OIDCHttpFacade.Request request) { System.out.println("inside resolve :: "); String realm = SubdomainUtils.obtainTenantFromSubdomain(request.getURI()); if (realm.contains("?")) { realm = realm.split("\\?")[0]; } if (!cache.containsKey(realm)) { InputStream is = this.getClass().getResourceAsStream("/" + realm + "-keycloak.json"); cache.put(realm, KeycloakDeploymentBuilder.build(is)); } return cache.get(realm); } static void setAdapterConfig(AdapterConfig adapterConfig) { PathBasedConfigResolver.adapterConfig = adapterConfig; } }
SpringKeycloakSecurityConfiguration类代码
public class SpringKeycloakSecurityConfiguration { @DependsOn("keycloakConfigResolver") @KeycloakConfiguration @ConditionalOnProperty(name = "keycloak.enabled", havingValue = "true", matchIfMissing = true) public static class KeycloakConfigurationAdapter extends KeycloakWebSecurityConfigurerAdapter { /** * Registers the KeycloakAuthenticationProvider with the authentication manager. */ @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider(); SimpleAuthorityMapper soa = new SimpleAuthorityMapper(); keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(soa); auth.authenticationProvider(keycloakAuthenticationProvider); } /** * Defines the session authentication strategy. */ @Bean @Override protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { // required for bearer-only applications. // return new NullAuthenticatedSessionStrategy(); return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl()); } @Override protected AuthenticationEntryPoint authenticationEntryPoint() throws Exception { return new MultitenantKeycloakAuthenticationEntryPoint(adapterDeploymentContext()); } @Override protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception { KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter( authenticationManager(), new AntPathRequestMatcher("/tenant/*/sso/login")); filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy()); return filter; } @SuppressWarnings({ "rawtypes", "unchecked" }) @Bean public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean( KeycloakAuthenticationProcessingFilter filter) { FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); registrationBean.setEnabled(false); return registrationBean; } @SuppressWarnings({ "rawtypes", "unchecked" }) @Bean public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean( KeycloakPreAuthActionsFilter filter) { FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); registrationBean.setEnabled(false); return registrationBean; } @SuppressWarnings({ "rawtypes", "unchecked" }) @Bean public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean( KeycloakAuthenticatedActionsFilter filter) { FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); registrationBean.setEnabled(false); return registrationBean; } @SuppressWarnings({ "rawtypes", "unchecked" }) @Bean public FilterRegistrationBean keycloakSecurityContextRequestFilterBean( KeycloakSecurityContextRequestFilter filter) { FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter); registrationBean.setEnabled(false); return registrationBean; } @Bean @Override @ConditionalOnMissingBean(HttpSessionManager.class) protected HttpSessionManager httpSessionManager() { return new HttpSessionManager(); } /** * Configuration spécifique à keycloak (ajouts de filtres, etc) * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.sessionManagement() // use previously declared bean .sessionAuthenticationStrategy(sessionAuthenticationStrategy()) // keycloak filters for securisation .and().addFilterBefore(keycloakPreAuthActionsFilter(), LogoutFilter.class) .addFilterBefore(keycloakAuthenticationProcessingFilter(), X509AuthenticationFilter.class) .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint()) .and().logout().addLogoutHandler(keycloakLogoutHandler()).logoutUrl("/tenant/*/logout") .logoutSuccessHandler( // logout handler for API (HttpServletRequest request, HttpServletResponse response, Authentication authentication) -> response.setStatus(HttpServletResponse.SC_OK)) .and().authorizeRequests().antMatchers("mobileservlet/**").permitAll().antMatchers("**/favicon.ico") .permitAll().antMatchers("/error").permitAll().antMatchers("/login.go").permitAll() .antMatchers("/resources/*").permitAll().anyRequest().authenticated(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("*")); configuration.setAllowedMethods(Arrays.asList(HttpMethod.OPTIONS.name(), "GET", "POST")); configuration.setAllowedHeaders( Arrays.asList("Access-Control-Allow-Headers", "Access-Control-Allow-Origin", "Authorization")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }
MultitenantKeycloakAuthenticationEntryPoint类代码
public class MultitenantKeycloakAuthenticationEntryPoint extends KeycloakAuthenticationEntryPoint { public MultitenantKeycloakAuthenticationEntryPoint(AdapterDeploymentContext adapterDeploymentContext) { super(adapterDeploymentContext); } public MultitenantKeycloakAuthenticationEntryPoint(AdapterDeploymentContext adapterDeploymentContext, RequestMatcher apiRequestMatcher) { super(adapterDeploymentContext, apiRequestMatcher); } @Override protected void commenceLoginRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException { System.out.println("inside commenceLoginRedirect :: "); String path = request.getRequestURI(); int multitenantIndex = path.indexOf("tenant/"); if (multitenantIndex == -1) { throw new IllegalStateException("Not able to resolve the realm from the request path!"); } String realm = path.substring(path.indexOf("tenant/")).split("/")[1]; if (realm.contains("?")) { realm = realm.split("\\?")[0]; } String contextAwareLoginUri = request.getContextPath() + "/tenant/" + realm + DEFAULT_LOGIN_URI; response.sendRedirect(contextAwareLoginUri); } }
内容的提问来源于stack exchange,提问作者gasser
相关产品推荐
相关产品推荐

