티스토리 뷰

요약

401 Unauthorized -> @WithMockUser, @WithMockUserDetails 사용
403 Forbidden -> with(csrf()) 추가

 

 

@WebMvcTest

Annotation that can be used for a Spring MVC test that focuses only on Spring MVC components.

 

Using this annotation will disable full auto-configuration and instead apply only configuration relevant to MVC tests (i.e. @Controller, @ControllerAdvice, @JsonComponent, Converter/GenericConverter, Filter, WebMvcConfigurer and HandlerMethodArgumentResolver beans but not @Component, @Service or @Repository beans).

 

By default, tests annotated with @WebMvcTest will also auto-configure Spring Security and MockMvc

 

WebMvcTest (Spring Boot 2.7.5 API)

Annotation that can be used for a Spring MVC test that focuses only on Spring MVC components. Using this annotation will disable full auto-configuration and instead apply only configuration relevant to MVC tests (i.e. @Controller, @ControllerAdvice, @JsonC

docs.spring.io

MVC와 관련된 애노테이션(Controller, ControllerAdvice, Filter, WebMvcConfigurer 등..)만 불러오고,

@Component, @Service, @Repository와 같은 빈들은 불러오지 않는다고 되어있다.

 

그리고! @WebMvcTest는 Spring Security를 auto-configure 한다고 하는데, 이것도 뭔 소리인지 아래에서 알아보자.

 

 

1. WebMvcTest와 SpringBootTest의 차이?

공식 문서로만 들으면 사실 잘 감이 안오는데 진짜 안불러올까?

- SpringBootTest 에서의 ApplicationContext Bean

 

@Repository 애노테이션도 다 불러옴

=> @Controller, @Service, @Component, @Bean 등등 모두 다 불러와서 실제 환경과 동일하게 테스트 하고자 할 때 사용하는 @SpringBootTest를 사용한다

 

 

- WebMvcTest에서의 ApplicationContext Bean

@WebMvcTest(TownApiController.class)
class TownApiControllerTest {

    @Autowired private MockMvc mvc;
    
    @MockBean private UserService userService;
    ....
}

 

 

정말로 WebMvcTest가 절반 가랑의 스프링 빈만 불러와서 사용하는 것을 확인할 수 있다.

그리고 beanDefinitionMap을 일일히 뒤져봐도 repository나 component같이 내가 설정해둔 빈들을 불러오지 않은 것을 볼 수 있다.

 

대신에 내가 @MockBean으로 주입해준 UserService 빈은 #0이 뒤에 붙어서 스프링 빈으로 저장되어있는 것을 확인할 수 있다

 

그리고 이 UserService 빈은 MockitoMock을 통해 반들어져있고 내부는 다 null로 채워져있다

그러니까 이 텅 비어있는 userService 빈을 실제 동작하는 것처럼 흉내내기 위해서 (Mocking)

given(userService.findById(1L)).willReturn(user);

이런 given() 메서드를 대신 활용하는 것.

 

 

2. Spring Security의 Auto Congifure

WebMvcTest는 컨트롤러 테스트를 하기 위해 꼭 필요한 빈들만을 불러와서 사용한다는 것을 직접 확인해 보았다.

그래서 문제가 되는 것은 내가 만들어둔 Spring Security Configuration, Bean들도 불러오지 않는다는 것이다.

 

그 대신에 스프링 시큐리티가 자동으로 구성하는 Configuration 파일들을 불러와서 사용한다.

자동 구성되는 클래스들이 엄청 많고, 하나 하나 알아보기에는 너무 힘들지만

대표적으로 SpringBootWebSecurityConfiguration << 이 친구가 있다.

@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {

	@Bean
	@Order(SecurityProperties.BASIC_AUTH_ORDER)
	SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
		http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
		return http.build();
	}

}

 

이거 딱 스프링 시큐리티 맨 처음 적용했을 때 페이지 들어가면 바로 로그인 창 뜨게 만드는 그것이다.

모든 요청에 대해 아무 권한이나 갖고있으면 되도록 간단하게만 설정되어있다.

 

그렇기 때문에?

컨트롤러 계층 테스트를 하기 위해 

mvc.perform(delete("/api/towns/1"));

이러한 요청을 해보면 스프링 시큐리티가 적용되어 있기 때문에 401 Unauthorized에러를 반환하는 것을 확인할 수 있다. 

모든 요청에 대해 권한이 필요하도록 기본적으로 적용이 되어있기 때문이다.

 

 

그러니까 결국 요청을 할 때 권한을 같이 넘겨줘야 한다는 것이다.

 

1. ExcludeFilter를 이용해서 Security를 회피하는 방법

2. @WithMockUser, @WithUserDetails 와 같은 애노테이션을 이용해 권한을 넘겨주는 방법

 

(+) @TestConfiguration 파일을 별도로 설정하는 방법도 있다!

 

1번의 경우 일단 되긴 하나 근본적인 해결이 되지 못하는 방법이다. 패스

2번 방법을 사용해 임의의 UserDetails를 만들어서 이를 같이 넘겨주는 방법을 사용하면 된다!

 

@WithMockUser

@WithAnonymousUser - 미인증 사용자

@WithUserDetails - 메서드가 principal 내부의 값을 직접 사용하는 경우 (별도의 사전 설정 필요)

 

등등 다앙한 권한을 설정해서 같이 넘겨줄 수 있다.

@RequiredArgsConstructor
@Validated
@RestController
@RequestMapping(value = "/api/towns")
public class TownApiController {

    private final TownService townService;
    private final UserService userService;
    private final AdminService adminService;
    
    ...

    @Secured({"ROLE_SUPER"})
    @DeleteMapping("/{townId}")
    public ResponseEntity<String> remove(@PathVariable @Positive Long townId) {
        townService.delete(townId);
        return ResponseEntity.ok("성공적으로 삭제했습니다");
    }
    ...
}
    @Test
    @WithMockUser(username = "테스트_최고관리자", roles = {"SUPER"})
    void test() throws Exception {
        ResultActions actions = mvc.perform(
                delete("/api/towns/1")
                        .with(csrf()));

        actions.andExpect(status().isOk());
    }

 

는 것으로 알고 있었고 해결이 되었다면 블로깅을 안했을 것인데 추가적인 문제가 있었다.

@WithMockUser를 사용하고 나니 401 에러가 아니라 403 Forbidden 에러가 떴다

 

 

401 Unauthorized아예 비로그인 상태에서 권한이 필요한 요청을 했을 때 발생하는 에러

403 Forbidden 로그인은 했으나 권한이 맞지 않는 경우에 발생하는 에러라고 보면 된다.

 

근데 request 봐도 세션에 SpringContext 내부에 WithMockUser로 만든 권한들이 다 잘 들어가있는 걸 확인할 수 있고, Authorities도 "ROLE_SUPER"으로 잘 담겨있는데 대체 왜 안된다는걸까??

 

구글링 해보니 csrf와 관련된 문제라고 한다.

"org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf" 를 추가해주면 된다고 하는데,,

ResultActions actions = mvc.perform(
                delete("/api/towns/1")
                        .with(csrf()));

 

정말로 with.(csrf()) 를 추가해주니 참 어처구니 없게도 잘 된다..

 

대체 csrf가 뭔데 그러는 걸까?

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf().disable()
     ...
}

사실 맨 처음에 Spring Security를 얕게 배울 때부터 그냥 csrf().disable() 이거는 솔직히 뭔지도 잘 모르고 관례적으로;;;  그러려니 하고 기본으로 깔고 사용하고 있었는데.... 이 참에 뭔지 알아보자

 

 

# csrf(Cross-Site Request Forgery) 

- 공격자가 악의적인 코드를 심어놓은 사이트를 만들어놓고, 로그인 된 사용자가 클릭하게 만들어 사용자 의지와 무관한 요청을 발생시키는 공격

- 사용자는 로그인 한 상태고 쿠키, 권한을 갖고있기 때문에 공격자가 위조한 웹사이트에 방문하게 되면 사용자 모르게 악의적인 POST, DELETE 요청을 정상 수행하도록 만들어버리는 공격

- 이를 해결하기 위해 스프링 시큐리티에서는 "CSRF 토큰" 을 이용해 토큰 값을 비교해서 일치하는 경우에만 메서드를 처리하도록 만든다. (Synchronizer Token Pattern 이라고 한다)

 

 

# Synchronizer Token Pattern 

- 서버가 뷰를 만들어줄 때 사용자 별 랜덤값을 만들어 세션에 저장한 다음 이를 뷰 페이지에 같이 담아 넘겨주게 된다.

- 클라이언트는 HTTP 요청마다 숨겨진 csrf 토큰을 같이 넘겨줘야 하는 방식.

- 서버는 HTTP Request에 있는 csrf 토큰값과 세션에 저장되어있는 토큰값을 비교해 일치하는 경우에만 처리를 진행하는 방식이다

-> 위조된 사이트의 경우 csrf 토큰값이 일치하지 않기 때문에 공격자가 악의적인 코드를 심어놔도 이를 실행하지 않음. 

 

* GET 요청에 대해서는 csrf 검증을 수행하지 않는다

 

* with(csrf()) 를 추가한 경우 파라미터로 _csrf 값을 같이 보내주는 것을 실제로 확인할 수 있다.

 

* with(csrf())를 사용하지 않은 경우 -> 세션에 저장된 CSRF 값과 매치되지 않음

-> 요청을 수행하지 않고 403 에러 반환

 

 

# 그럼 왜 안전한 csrf 기능을 disable 한걸까??

csrf 토큰 방식을 살펴보면 각 사용자에 대한 세션을 이용하는 방식이라는 것을 확인할 수 있다. 

때문에 웹 브라우저를 통한 접근을 하는 경우, 세션/쿠키를 사용해 상태를 유지하려고 하는 경우 csrf를 사용하는 것이 안전하다.

 

하지만 REST API의 경우는 대개 무상태성을 유지하며 JWT와 같은 토큰 방식으로 인증하게 되면 요청이 세션에 의존하지 않기 때문이다

 

https://docs.spring.io/spring-security/site/docs/5.0.x/reference/html/csrf.html

 

* 타임리프와 같은 템플릿 엔진을 통해 View를 같이 제공하는 애플리케이션 / 웹 브라우저를 통해 요청을 받는 애플리케이션 ->  csrf 사용 권장

*  Rest API만 제공하는 애플리케이션 = csrf 사용 안해도 무방.

 

 

 

어째 알면 알수록 더 어려운 것 같다.

<Security 참고>

https://tecoble.techcourse.co.kr/post/2020-09-30-spring-security-test/

 

Spring Security가 적용된 곳을 효율적으로 테스트하자.

Spring Security와 관련된 기능을 테스트하다보면 인증 정보를 미리 주입해야 하는 경우가 종종 발생한다. 기본적으로 생각할 수 있는 가장 간단한 방법은 테스트 전에 SecurityContext에 직접 Authenticatio

tecoble.techcourse.co.kr

 

<CSRF 참고>

https://codevang.tistory.com/282

 

Spring Security_CSRF Token의 개념과 사용 방법

- Develop OS : Windows10 Ent, 64bit - WEB/WAS Server : Tomcat v9.0 - DBMS : MySQL 5.7.29 for Linux (Docker) - Language : JAVA 1.8 (JDK 1.8) - Framwork : Spring 3.2.9 Release - Build Tool : Maven 3.6.3 - ORM : Mybatis 3.2.8 [ CSRF(Cross Site Request Forgery

codevang.tistory.com

https://docs.spring.io/spring-security/reference/features/exploits/csrf.html

 

Cross Site Request Forgery (CSRF) :: Spring Security

When should you use CSRF protection? Our recommendation is to use CSRF protection for any request that could be processed by a browser by normal users. If you are only creating a service that is used by non-browser clients, you will likely want to disable

docs.spring.io

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함