본문 바로가기
Spring

[Spring] TDD 도입기(3) feat) Controller 계층 / Security 검증

by windy7271 2024. 12. 22.
728x90
반응형

이번에는 저번에 이어서 Controller 를 테스트 코드를 짜려고 합니다.

 

컨트롤러를 테스트하기 위해서는 HTTP 호출이 필요하다 스프링에서는 이를 위해서 MockMVC 를 제공하고 있습니다. 

@ExtendWith(MockitoExtension.class)
@WebMvcTest(MemberController.class)
public class MemberControllerTest {

    private MockMvc mockMvc;

    @MockBean
    private MemberService memberService;
    
}

 

MemberService에 가짜 객체 생성을 위해 MockBean을 사용해줍니다. 이제는 getMyEmailAndRole 를 위한 테스트 코드를 작성해주면 됩니다.

 

하지만 잠시만요 

JWT 기반 로그인이 구현되어 있기 때문에 먼저 하고 가겠습니다.

 

우선 저는

@RestController
public class ApiController {

    @GetMapping("/admin/resources")
    public String getAdminResources() {
        return "ADMIN";
    }

    @GetMapping("/user/resources")
    public String getUserResources() {
        return "USER ";
    }

    @GetMapping("/public/resources")
    public String getPublicResources() {
        return "PUBLIC ";
    }
}

 

이것만 작성해주고 TDD 스타일로 테스트 코드를 작성하고자 합니다.

 

우선 저희 서비스는 USER, BRONZE, SILVER, GOLD, ADMIN 이 존재합니다.

 

오른쪽으로 갈수록 권한이 커지고 결제 금액에 따라 등급을 분류할 생각입니다. 그러면

 

1. 200 인 경우

2. 401 인 경우

3. 403 인 경우

 

로 나뉩니다.

 

@Configuration
@EnableWebSecurity
public class SecurityConfigTest {

    @Bean
    public UserDetailsService userDetailsService() {
        List<UserDetails> userDetails = List.of(
                User.withDefaultPasswordEncoder().username("user").password("user1234").roles("USER").build(),
                User.withDefaultPasswordEncoder().username("admin").password("admin1234").roles("ADMIN").build());
        return new InMemoryUserDetailsManager(userDetails);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

 

SecurityConfigTest 를 작성해주고

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SecurityConfigTest.class)
@WebAppConfiguration
@Import(ApiController.class)
public class AuthenticationTest {
    MockMvc mockMvc;

    @Autowired
    WebApplicationContext context;

    @BeforeEach
    void init() {
        mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .apply(springSecurity()).build();
    }

    @Test
    @DisplayName("권한이 없는 사용자가 인증이 필요한 요청 시 : 401")
    void testAuthenticationError() throws Exception {
        mockMvc.perform(get("/user/resources"))
                .andExpect(status().is(401));
    }

    @Test
    @DisplayName(" 권한이 부족한 사용자가 상위 권한의 요청 시 : 403")
    void testAuthorizationError() throws Exception {
        mockMvc.perform(get("/admin/resources").with(user("user").roles("SILVER")))
                .andExpect(status().is(403));
    }

    @Test
    @DisplayName("적절한 권한이 있는 사용자는 인증이 필요한 요청 시 : Success 200")
    void testHappyCase() throws Exception {

        // 권한 없는 사용자 -> Public API
        mockMvc.perform(get("/public/resources"))
                .andExpect(status().is(200));

        // User -> User API
        mockMvc.perform(get("/user/resources").with(user("user").roles("USER")))
                .andExpect(status().is(200));

        // Admin -> User API
        mockMvc.perform(get("/user/resources").with(user("admin").roles("GOLD")))
                .andExpect(status().is(200));

        // Admin -> Admin API
        mockMvc.perform(get("/admin/resources").with(user("admin").roles("ADMIN")))
                .andExpect(status().is(200));
    }

}

 

다음과 같이 실행할경우

 

 

가운데만 성공하게 되는데

 

인증 관련 로직을 생성하지 않았지만 Spring Security 의존성을 추가했기 때문에

자동 인증 로직이 추가되어

모든 요청에서 401 오류가 발생하여 401 테스트가 통과하게 됩니다.

 

그럼 SecurityConfig 를 수정해보겠습니다.

 

package growup.spring.springserver.global.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

import java.util.List;


@Configuration
@EnableWebSecurity
public class SecurityConfigTest {
    @Bean
    public UserDetailsService userDetailsService() {
        List<UserDetails> userDetails = List.of(
                User.withDefaultPasswordEncoder().username("user").password("user1234").roles("USER").build(),
                User.withDefaultPasswordEncoder().username("admin").password("admin1234").roles("ADMIN").build());
        return new InMemoryUserDetailsManager(userDetails);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/public/**").permitAll()
                        .requestMatchers("/user/**").hasRole("USER")
                        .requestMatchers("/admin/**").hasRole("ADMIN"))
                .formLogin(Customizer.withDefaults())
                .build();


    }
    
    // MockMVC에서 HandlerMappingIntrospector를 사용하기 위함
    // 이게 없으면 requestMatcher 를 정상적으로 하지 못함.
    @Bean(name = "mvcHandlerMappingIntrospector")
    public HandlerMappingIntrospector mvcHandlerMappingIntrospector() {
        return new HandlerMappingIntrospector();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

 

mvcHandlerMappingIntrospector 가 빈으로 등록되지 않으면 정상적으로 실행이 되지 않아 모든 테케가 실패하게 됩니다.

 

이렇게 바꾸고 실행하게 되면

이렇게 바뀌게 되는데요

 

200 응답은 권한이 부족해 로그인 페이지로 이동해 302 응답을 하였고

3번은 ADMIN 역할이 USER 역할을 갖고 있지 않기 때문에 접근할 수 없어 403을 응답합니다.

 

1. 권한이 부족한 경우 401응답을 보내도록 수정하기.

 

제가 구현한 코드에는 401 에러와 403 에러일때 핸들러를 구현했는데요 이것들은 그냥 오버라이딩 해서 작성했습니다.

 

@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
    return httpSecurity
            .authorizeHttpRequests(authorize -> authorize
                    .requestMatchers("/public/**").permitAll()
                    .requestMatchers("/user/**").hasRole("USER")
                    .requestMatchers("/admin/**").hasRole("ADMIN"))
            .formLogin(Customizer.withDefaults())
            .exceptionHandling(exceptionHandling -> exceptionHandling
                    .authenticationEntryPoint(new AuthenticationEntryPoint() {
                        @Override
                        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                            response.setCharacterEncoding("UTF-8");
                            response.sendError(401, "인증 오류");
                        }
                    })
                    .accessDeniedHandler(new AccessDeniedHandler() {
                        @Override
                        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                            response.setCharacterEncoding("UTF-8");
                            response.sendError(403, "인증 오류");
                        }
                    }))
            .build();


}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
    return new UserAuthenticationEntryPoint();
}

//     403
@Bean
public AccessDeniedHandler accessDeniedHandler() {
    return new UserAccessDeniedHandler();
}

 

나머지 코드는 넣어주시고요 

그러면 1개만 실패합니다.

 

저 1개는 계층 권한이 안 이루어져있기 때문입니다. 그것도 가져와서 빈등록을 해줍니다.

@Bean
public RoleHierarchy roleHierarchy() {
    return RoleHierarchyImpl.fromHierarchy("""
    ROLE_ADMIN > ROLE_GOLD
    ROLE_GOLD > ROLE_SILVER
    ROLE_SILVER > ROLE_BRONZE
    ROLE_BRONZE > ROLE_USER
    """);
}

 

SecurityConfigTest에 이렇게 추가 해주시면 

다음과 같은 결과가 나옵니다.

 

반응형

댓글