I have set up a role hierarchy in my SecurityConfig class and it works fine for method security, but it fails when testing URL access restrictions. Here’s what I have:
package KKSC.page.global.config;
import KKSC.page.domain.member.repository.MemberRepository;
import KKSC.page.domain.member.service.impl.MemberDetailsService;
import KKSC.page.global.auth.*;
import KKSC.page.global.auth.service.JwtService;
import KKSC.page.global.exception.CustomAccessDeniedHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig {
private final MemberDetailsService memberDetailsService;
private final MemberRepository memberRepository;
private final JwtService jwtService;
private final ObjectMapper objectMapper;
private final CustomAccessDeniedHandler customAccessDeniedHandler; // Custom access denied handler
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(configurationSource()))
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.headers(header -> header.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(req -> req
.requestMatchers("/login", "/register", "/profile").permitAll()
.requestMatchers("/profile_edit").hasRole("permission_level0")
.requestMatchers("/board-idea", "/board-notice", "/board-portfolio", "/board-view").permitAll()
.requestMatchers("/board-form").hasRole("permission_level0")
.requestMatchers("/calendar", "/part-introduction").permitAll()
.requestMatchers("/").permitAll()
.requestMatchers(HttpMethod.POST, "/member/").permitAll()
.requestMatchers(HttpMethod.GET, "/notice/list", "/notice/search").permitAll()
.requestMatchers(HttpMethod.GET, "/notice/").permitAll()
.requestMatchers("/swagger-ui/**").permitAll()
.requestMatchers("/v3/api-docs/**").permitAll()
.anyRequest().authenticated())
.logout(logout -> logout
.logoutUrl("/member/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true))
.exceptionHandling(req -> req.authenticationEntryPoint(jwtAuthenticationEntryPoint()))
.addFilterAfter(jsonUsernamePasswordAuthenticationFilter(), LogoutFilter.class)
.addFilterBefore(jwtAuthenticationProcessingFilter(), JsonUsernamePasswordAuthenticationFilter.class)
.exceptionHandling(exception -> exception
.accessDeniedHandler(customAccessDeniedHandler))
.build();
}
@Bean
public CorsConfigurationSource configurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOriginPattern("*");
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addExposedHeader("Authorization");
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return urlBasedCorsConfigurationSource;
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(memberDetailsService);
return new ProviderManager(provider);
}
@Bean
public JwtLoginSuccessHandler jwtLoginSuccessHandler() {
return new JwtLoginSuccessHandler(jwtService, memberRepository);
}
@Bean
public JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint() {
return new JwtAuthenticationEntryPoint();
}
@Bean
public JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter() {
return new JwtAuthenticationProcessingFilter(jwtService, memberRepository);
}
@Bean
public LoginFailureHandler loginFailureHandler() {
return new LoginFailureHandler();
}
@Bean
public JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter() throws Exception {
JsonUsernamePasswordAuthenticationFilter filter = new JsonUsernamePasswordAuthenticationFilter(objectMapper);
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(jwtLoginSuccessHandler());
filter.setAuthenticationFailureHandler(loginFailureHandler());
return filter;
}
@Bean
public RoleHierarchyImpl roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_permission_level0 > ROLE_permission_level1nROLE_permission_level1 > ROLE_permission_level2");
return roleHierarchy;
}
@Bean
public DefaultWebSecurityExpressionHandler customWebSecurityExpressionHandler() {
DefaultWebSecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler();
expressionHandler.setRoleHierarchy(roleHierarchy());
return expressionHandler;
}
}
package KKSC.page.global.securityconfig;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
public class SecurityConfigTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = "permission_level0")
public void testPermissionLevel0Access() throws Exception {
mockMvc.perform(get("/profile_edit"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "permission_level1")
public void testPermissionLevel1Access() throws Exception {
mockMvc.perform(get("/profile_edit"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = "permission_level2")
public void testPermissionLevel2Access() throws Exception {
mockMvc.perform(get("/profile_edit"))
.andExpect(status().isForbidden());
}
}
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2024-08-07T01:48:50.744+09:00 DEBUG 16548 --- [ main] o.s.security.web.FilterChainProxy : Securing GET /profile_edit
2024-08-07T01:48:50.753+09:00 INFO 16548 --- [ main] .p.g.a.JwtAuthenticationProcessingFilter : JwtAuthenticationProcessingFilter.doFilterInternal 진입
2024-08-07T01:48:50.753+09:00 INFO 16548 --- [ main] .p.g.a.JwtAuthenticationProcessingFilter : request URL = http://localhost/profile_edit
2024-08-07T01:48:50.753+09:00 INFO 16548 --- [ main] .p.g.a.JwtAuthenticationProcessingFilter : request Authorization = null
2024-08-07T01:48:50.762+09:00 DEBUG 16548 --- [ main] o.s.s.a.h.RoleHierarchyImpl : getReachableGrantedAuthorities() - From the roles [ROLE_permission_level1] one can reach [ROLE_permission_level1] in zero or more steps.
MockHttpServletRequest:
HTTP Method = GET
Request URI = /profile_edit
Parameters = {}
Headers = []
Body = null
Session Attrs = {}
Handler:
Type = null
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", X-Content-Type-Options:"nosniff", X-XSS-Protection:"0", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"SAMEORIGIN"]
Content type = null
Body =
Forwarded URL = /access-denied
Redirected URL = null
Cookies = []
java.lang.AssertionError: Status expected:<403> but was:<200>
Expected :403
Actual :200
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:59)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:122)
at org.springframework.test.web.servlet.result.StatusResultMatchers.lambda$matcher$9(StatusResultMatchers.java:637)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:214)
at KKSC.page.global.securityconfig.SecurityConfigTest.testPermissionLevel1Access(SecurityConfigTest.java:46)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
Why is the role hierarchy not working as expected for URL access restrictions, and how can I fix this?
version,,, cache clean,,, and everything…. sorry…
New contributor
leet is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.