2025. 6. 12. 20:53ㆍspring/TDD
참조 코드
- https://suhyeon-developer.tistory.com/38
- https://tlatmsrud.tistory.com/77
- https://velog.io/@wonizizi99/SpringSpring-security-CSRF란-disable
- https://velog.io/@jmjmjmz732002/Springboot-Junit5-컨트롤러-테스트-401-에러를-마주쳤다
- https://velog.io/@tjdtn0219/SpringSecurity적용-후-Controller-테스트코드-작성-시-발생했던-오류들-Feat.-Junit5-csrf
- https://lemontia.tistory.com/1088
MockMvc를 활용하여 테스트하는 방법을 알아봤으니, 이제 내 프로젝트에도 적용해보자.
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class SignUpController implements SignUpSwagger {
private final UserService userService;
@PostMapping("/signup")
public Response<UserDto.Response> signup(
@RequestBody @Valid final UserDto.Request request
){
var response = userService.createUser(request);
return Response.ok(response);
}
}
@WebMvcTest(SignUpController.class)
class SignUpControllerTest {
@MockitoBean
private SignUpController signUpController;
@Mock
private UserService userService;
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
@DisplayName("올바른 요청 방식으로 회원가입을 한다.")
void signup() throws Exception{
// given
UserDto.Request userDto = UserDto.Request.builder()
.email("test@test.com")
.password("1234")
.name("천규")
.role(Role.MEMBER)
.build();
// when
mockMvc.perform(
MockMvcRequestBuilders.post("/api/auth/signup")
.content(objectMapper.writeValueAsString(userDto))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(print())
.andReturn();
}
}
현재 나의 코드는 위와 같다.
위 에러를 해결하기 위해
https://suhyeon-developer.tistory.com/38 을 참조하니
403은 csrf와 관련이 있다더라. csrf가 뭐임?
CSRF (Cross-Site Request Forgery) 웹 애플리케이션에서 발생할 수 있는 보안 취약점 중 하나로, 공격자가 사용자의 권한을 이용하여 특정 동작을 수행하도록 속이는 공격
이라고 하더라.
예시는 아래와 같다.
안녕하세요. 저는 아무개 커뮤니티의 회원 "talmobil"이라고 합니다.
유머게시판에 게시글 하나를 썼는데 좋아요가 너무 없어서 자괴감이 들었습니다. 아니, 열받더라구요. 제 글이 정말 재밌는데, 사람들이 일부러 좋아요를 눌러주지 않는 것 같거든요. -ㅅ-
그러던 중 좋은 방법이 하나 떠올랐습니다. 다른 사람이 제 게시글을 보면 좋아요를 자동으로 누르게 하는거죠!
방법은 이래요.
커뮤니티 사이트에서 게시글에 좋아요를 누르니 "http://community.com/like?post=[게시글 id]"이더군요.
제 게시글 ID를 확인해보니 9566 이구요. 그래서 게시글 안에 이미지 태그를 하나 삽입하고 src 값에 "http://community.com/like?post=9566"을 넣어봤습니다. 그랬더니 사람들이 제 게시글을 조회하게 되면 이미지 태그의 URL인 "http://community.com/like?post=9566" 가 자동으로 호출되었습니다! @_@.. 자동으로 제 게시글의 좋아요 수가 올라갔어요. 후후.. 다른 커뮤니티 사이트에도 어그로 게시글을 올리고 마찬가지로 src 태그를 넣었더니 더 빨리 오르더군요!
사용자들은 아무것도 모르고 제가 의도한대로 요청을 하고있는겁니다! 이제는 좋아요가 아닌 다른걸 해봐야겠어요
아무것도 모르는 사용자들은 공격자가 의도한 행위를 사이트간 위조 요청으로 하게 되었다.
이런 공격이 바로 CSRF이다. 그렇다면 이러한 공격을 막는 방법이 없을까?
당연히 있다. 서버에서 요청에 대한 검증을 하는 것이다. 이를 테면 토큰값으로 말이다.
안녕하세요. 아무개 커뮤니티 담당자 "ㅅㄱ"입니다.
제가 집에 우환이 생겼습니다. 웃음이 필요해서 유머 게시판을 쓱 둘러봤는데, 개노잼 글이 하나 있더군요. 뭐지 하고 넘어가려는데 이 게시글의 좋아요 수가 무려 1만이 넘어갔습니다. 그리고 다시 들어가보니 누르지도 않은 좋아요가 눌러져있더라구요. 이게 말이되나? 싶었습니다.
서버 로그를 확인해보니 제가 좋아요를 누른 기록도 있었습니다. 뭔가 이상해서 게시글 내용을 살펴봤는데 이미지 태그의 src에 좋아요 처리를 하는 URL이 들어가 있었습니다. 해당 URL을 구글링 해보니 이미 다른 웹사이트 게시글에도 포함되어 있더군요. 말로만 듣던 CSRF 공격이었습니다.
막을 방법은 클라이언트마다 토큰을 발급하는 겁니다. 서버는 토큰 값을 검증하고요. 프로세스는 다음과 같습니다.
- 저희 커뮤니티를 접근하면 특정 토큰을 클라이언트에게 발급함과 동시에 저희 서버 세션 안에 넣습니다.
- A 클라이언트에 대해 A 토큰을, B클라이언트에 대해 B 토큰을 이렇게 각각 발급하는 겁니다.
- 클라이언트는 모든 API를 호출할 때 필수적으로 이 토큰 값을 헤더에 넣어 보냅니다.
- 서버에서는 요청을 수행하기전 Filter 레벨에서 세션 안에 들어있는 토큰 값과 요청 토큰 값을 비교합니다.
- 토큰 값이 불일치할 경우 비정상적인 요청으로 판단하고 Access Denied 시킵니다.
토큰 검증을 성공하려면 요청 시 CSRF Token 값을 헤더에 넣어줘야하는데, 공격자는 사용자마다 각각 발급된 토큰 값을 알 수 없기때문에 막힐겁니다.
추가적으로, 이러한 방식을 스프링 시큐리티에서 기본적으로 지원하고 있더라구요!
• 이처럼 서버에서 토큰을 발급 및 검증하고 클라이언트에서는 발급받은 토큰을 요청 값에 포함시켜 보내는 방식으로 CSRF 공격을 막을 수 있다.
스프링 시큐리티 의존성을 추가하면 이와 같은 방식을 제공하는 CSRF Filter가 자동으로 추가된다.
csrf().disable() 설정을 통해 해제도 가능하다.
Rest API에서 CSRF가 disable 이유
참조 : https://velog.io/@wonizizi99/SpringSpring-security-CSRF란-disable
CSRF protection은 spring security에서 default로 설정된다.
즉 protection을 통해 GET요청을 제외한 상태를 변화시킬 수 있는 POST, PUT, DELETE 요청으로부터 보호한다.
CSRF protection을 적용하였을 때, html에서 다음과 같은 csrf 토큰이 포함되어야 요청을 받아들이게 됨으로써, 위조 요청을 방지하게 된다.
이렇게 보안 수준을 향상시키는 CSRF를 REST API에서 disable을 한 이유는, spring security documentation에서 non-brower clients만을 위한 서비스라면 CSRF를 disable하여도 좋다고 한다.
이유는 REST API를 이용한 서버라면, session기반 인증과는 다르게 stateless하기 때문에 서버에 인증정보를 보관하지 않는다. REST API에서 client는 권한이 필요한 요청을 하기 위해 요청에 필요한 인증정보(Oauth2, jwt토큰 등)을 포함시켜야한다. 따라서, 서버에 인증정보를 저장하지 않기때문에 굳이 불필요한 csrf 코드들을 작성할 필요가 없다.
다시 본론으로 돌아가서,
실제 애플리케이션 환경에서는 csrf를 disable()을 해주었다면 문제가 안생긴다.
다만 테스트 환경에서는 적용이 안되므로 MockMvc에서 request에 자동으로 유효한 csrf를 제공해준다.
즉 with.(csrf())를 추가하면 된다.
위 코드를 추가하기 위해 아래 의존성을 추가한뒤,
testImplementation 'org.springframework.security:spring-security-test'
@WebMvcTest(SignUpController.class)
class SignUpControllerTest {
...
// when
mockMvc.perform(
post("/api/auth/signup")
.content(objectMapper.writeValueAsString(userDto))
.contentType(MediaType.APPLICATION_JSON)
.with(csrf())) // 코드 추가
.andExpect(status().isOk())
.andDo(print())
.andReturn();
}
}
뚜둥.. 끝난줄 알았는데 다시 401에러가 발생하였다.
위를 해결하기 위해
https://velog.io/@jmjmjmz732002/Springboot-Junit5-컨트롤러-테스트-401-에러를-마주쳤다 를 참조하니,
@WebMvcTest는 Spring MVC와 관련된 Annotation만 구성되며, Spring Security와 MockMvc에 대한 auto-configure를 해준다고 한다.
즉 Spring Security가 자동으로 구성하는 Configuration 파일들을 불러와서 사용한다.
(내가 설정한 Security 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();
}
}
모든 요청에 대하여 어떠한 권한이든 상관없이 인증이 필요하도록 기본적으로 설정되어 있다. 그렇기 때문에 /signup 요청에 대해 특별한 권한 설정을 해주지 않았으므로 401 Unauthorized 에러가 발생할 수 밖에 없었다.
- https://velog.io/@jmjmjmz732002/Springboot-Junit5-컨트롤러-테스트-401-에러를-마주쳤다
- https://velog.io/@tjdtn0219/SpringSecurity적용-후-Controller-테스트코드-작성-시-발생했던-오류들-Feat.-Junit5-csrf
위 사이트 모두 401에러를 해결하기 위해 인증된 Mock유저를 생성(@WithMockUser)하도록 하고 있다.
하지만 회원가입같은 경우는, 인증이 필요하지 않은데 @WebMvcTest(내가 설정한 Security Configuration이 아닌, Spring Security가 자동으로 구성하는 Configuration 파일들을 불러옴) 에 의해
@WithMockUser를 사용하는 것이 논리적으로 말이 되지 않았다.
난 https://lemontia.tistory.com/1088를 참조하여
@AutoConfigureMockMvc(addFilters = false) 추가하여 Spring Security의 자동 구성을 비활성화하고 모든 요청을 허용하도록 설정할 수 있다.
@WebMvcTest(SignUpController.class)
@AutoConfigureMockMvc(addFilters = false) // 코드 추가
class SignUpControllerTest {
...
// when
mockMvc.perform(
post("/api/auth/signup")
.content(objectMapper.writeValueAsString(userDto))
.contentType(MediaType.APPLICATION_JSON)
.with(csrf())) // 코드 추가
.andExpect(status().isOk())
.andDo(print())
.andReturn();
}
}
이제 집가자..
'spring > TDD' 카테고리의 다른 글
Spring Boot MockMvc 이해하기 : 테스트 흐름 및 사용예제 (1) | 2025.06.12 |
---|---|
Spring Boot Junit5 이해하기 - 환경 구성 및 활용 예제 (1) | 2025.06.10 |
Spring Boot JUnit5 이해하기 - 이론 및 구조 (0) | 2025.06.10 |
MockMvc, @Transactional(readOnly = true)와 @Transactional을 분리하라고? (0) | 2024.01.23 |
deleteAll()보다 deleteAllInBatch()가 더 권장되는 이유가 뭐야? (0) | 2024.01.15 |