Spring/Tistory

블로그-V3. Security 테스트 시 주의할 점

JJJAEOoni 2022. 6. 8. 12:37
반응형

 

포스트 컨트롤러의 write 메서드를 테스트 해보자.

 

@ActiveProfiles("test")
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class PostControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext context;

    @BeforeEach
    public void setup() {
        mockMvc = MockMvcBuilders
                .webAppContextSetup(context) // spring의 환경을 알고있어야 set
                .apply(SecurityMockMvcConfigurers.springSecurity())
                .build();
    }

    @WithMockUser
    @Test
    public void write_테스트() throws Exception {
        // given
        PostWriteReqDto postWriteReqDto = PostWriteReqDto.builder()
                .categoryId(1) // 이거 분명히 터짐 FK 연결 안되어있어서!
                .title("스프링1강")
                .content("재밌음")
                .build();

        // when
        ResultActions resultActions = mockMvc.perform(post("/s/post"));

        // then
        resultActions
                .andDo(MockMvcResultHandlers.print());
    }
}

 

1. postWriteReqDto 체크

 

실제 컨트롤러 주소에 요청했을 때 우선 해당 메서드로 잘 들어가는지 확인하자.

 

@PostMapping("/s/post")
public String write(PostWriteReqDto postWriteReqDto, @AuthenticationPrincipal LoginUser loginUser) {
    // postService.게시글쓰기(postWriteReqDto, loginUser.getUser());
    // return "redirect:/user/" + loginUser.getUser().getId() + "/post";
    return "1";
}

 

 

@WithMockUser가 있기 때문에 인증에 성공하여 잘들어간다.

 

@PostMapping("/s/post")
public String write(PostWriteReqDto postWriteReqDto, @AuthenticationPrincipal LoginUser loginUser) {
    // postService.게시글쓰기(postWriteReqDto, loginUser.getUser());
    // return "redirect:/user/" + loginUser.getUser().getId() + "/post";
    postWriteReqDto.getTitle();
    return "1";
}

 

 

컨트롤러로 전송되는 postWriteReqDto의 값이 null이라 무조건 터질줄 알았는데 터지지 않는다.

값에 null이 들어가긴했는데 터지진않네. 직접 터뜨려주자.

 

@PostMapping("/s/post")
public String write(PostWriteReqDto postWriteReqDto, @AuthenticationPrincipal LoginUser loginUser) {
    // postService.게시글쓰기(postWriteReqDto, loginUser.getUser());
    // return "redirect:/user/" + loginUser.getUser().getId() + "/post";
    if (postWriteReqDto.getTitle() == null) {
        throw new NullPointerException();
    }
    return "1";
}

 

null이 아닌 값이 들어오게 먼저 잡아주자.

얘는 json 타입으로 받고있지 않기 때문에 기본전략인 x-www-form-urlencoded 타입으로 전송해줘야한다.

 

x-www-form-urlencoded 타입으로 전송할 때는 param을 사용해야한다.

json 넣을 때는 content 사용

 

@WithMockUser // username="username" password="password"
@Test
public void write_테스트() throws Exception {
        // given
        PostWriteReqDto postWriteReqDto = PostWriteReqDto.builder()
                        .categoryId(1) // 이거 분명히 터짐 FK 연결 안되어있어서!
                        .title("스프링1강")
                        .content("재밌음")
                        .build();

        // when
        ResultActions resultActions = mockMvc.perform(
                        post("/s/post").param("title", postWriteReqDto.getTitle()));

        // then
        resultActions
                        .andDo(MockMvcResultHandlers.print());
}

 

 

터지지 않고 잘 들어간다. param 사용!

param에는 무조건 문자열이 들어감에 주의하자.

통신이니까!

ResultActions resultActions = mockMvc.perform(
      post("/s/post")
              .param("title", postWriteReqDto.getTitle())
              .param("content", postWriteReqDto.getContent())
              .param("categoryId", postWriteReqDto.getCategoryId() + ""));
@PostMapping("/s/post")
public String write(PostWriteReqDto postWriteReqDto, @AuthenticationPrincipal LoginUser loginUser) {
    // postService.게시글쓰기(postWriteReqDto, loginUser.getUser());
    // return "redirect:/user/" + loginUser.getUser().getId() + "/post";
    if (postWriteReqDto.getTitle() == null) {
        throw new NullPointerException("title이 없습니다.");
    }
    if (postWriteReqDto.getContent() == null) {
        throw new NullPointerException("content가 없습니다.");
    }
    if (postWriteReqDto.getCategoryId() == null) {
        throw new NullPointerException("categoryId가 없습니다.");
    }
    return "1";
}

 

다시 테스트해보니 통과하고 값이 다 잘 들어왔다.

 

2. loginUser 값 체크

 

@WithMockUser 체크

 

줄이 한줄만 그이고 80번 라인에서 loginUser를 받아오지 못해서 오류가 터졌다.

 

실제로 @AuthenticationPrincipal은 시큐리티 컨텍스트에서 가져오는데

Mock로 띄운건 가져오지 않는다.

 

 


 

클라이언트의 요청이 들어오면 풀링기법으로 관리되는 request 객체를 가져오고

이 객체들은 모두 하나의 세션 영역을 공유한다.

 

세션에서 클라이언트를 구분하는 방법은 JSESSIONID로 구분했다.

 

스프링에서는 이 세션 영역 내부에 SecurityContextHolder라는 지갑을 하나 만들어둔다.

 

보안의 모든 것을 다 알고있는 지갑인 것이다.

 

시큐리티 컨텍스트 홀더 내부에 SecurityContext들이 모여있다.

 

이 시큐리티 컨텍스트 내부에는 각각 Authentication 객체가 들어있다.

 

객체 내부에 원래는 User 오브젝트를 넣어주면 되는데

그냥 User 오브젝트에는 내 마음대로 만들어서 시큐리티가 원하는 정보가 없을 수도 있기 때문에

강제성을 부여하여 오브젝트 타입을 받는게 아닌 UserDetails 객체를 받는다.

 

 

이 Authentication 객체는 로그인 성공시에 만들어진다.

Authentication은 UserDetails를 감싸고 있는 래핑 클래스일 뿐이다.

 

로그인 시에 UserDetailsService의 loadUserByUsername이 호출되며 인증 성공 시에

UserDetails 객체가 Authentication에 담기게 되는 것이다.

 


 

그럼 인증이 필요한 주소(/s)를 어떻게 통과했을까?

 

@WithMockUser를 붙여주면

username="username", password="password" 값이 담긴 가짜 UserDetails 객체를

Authentication 객체에 담겨서 만들어진다.

 

그래서 세션이 존재하기 때문에 인증에 통과하여 진입이 가능한 것이다.

진입에는 성공했지만 loginUser에 접근할 때 null이라고 오류가 발생하게 된다.

 

@PostMapping("/s/post")
    public String write(PostWriteReqDto postWriteReqDto, @AuthenticationPrincipal LoginUser loginUser) {
        postService.게시글쓰기(postWriteReqDto, loginUser.getUser());
        return "redirect:/user/" + loginUser.getUser().getId() + "/post";
    }

 

@WithMockUser가 만들어준 UserDetails에는 User 오브젝트가 없기 때문이다.

 

이는 우리가 UserDetails 타입을 구현한(impl) LoginUser에 필요에 의해 만들어둔 User 오브젝트이기 때문에

당연히 @WithMockUser가 만든 UserDetails에는 User가 존재하지 않는다.

 

그래서 터진것이다.

 

그러면 내가 LoginUser를 만들어줘야 하는데 @WithMockUser로는 불가능하다.

얘는 User를 만들어주지 않으니까!

 

왜 우리가 getUser가 필요한가?

 

게시글쓰기 할 때 getUser로 받아온 principal 객체로 포스트 save 할 때 필요하기 때문이다.

그래서 User를 넣어둔것이다.

 

개념적으로만 봤을 때 UserDetails 오브젝트 내부에 User 오브젝트만 넣어주면 끝난다.

단순하다.

 


 

 

@WithMockUser로 감싸서 인증 필터를 통과만 시켜준거지 값을 가져올 수는 없다.

 

간단한 인증만 통과하고 싶을 때는 @WithMockUser를 사용하고,

세션에 있는 값을 내부에서 사용해야 한다면 이 가짜 객체로는 불가능하다.

 

핵심은 시큐리티 컨텍스트 홀더(SCH)에 우리가 시큐리티 컨텍스트를 하나 만들어서 끼워 넣어줘야 한다.

 

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

 

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

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

tecoble.techcourse.co.kr

 

@WithUserDetails

 

내가 커스텀한 UserDetailsService를 하나 더 만들건데 이 때는 프로파일 설정이 필요하다.

 

테스트용 TestUserDetailsService를 하나 더만들면 UserDetailsService가

IoC 컨테이너에 2개가 뜨게되어 충돌나기 때문에

시큐리티는 어떤 서비스로 가야하는지 구분하지 못한다.

 

TestUserDetailsService에는 @Profile("test")

실제 UserDetailsService에는 @Profile("dev")

 

SCH에다가 직접 만든 UserDetails 타입을 넣어주면 끝난다.

 

package site.metacoding.blogv3.config.auth;

import java.time.LocalDateTime;

import org.springframework.context.annotation.Profile;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import site.metacoding.blogv3.domain.user.User;

@Profile("test")
@Service
public class TestUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = User.builder()
                .id(1)
                .username("ssar")
                .password("1234")
                .email("ssar@nate.com")
                .createDate(LocalDateTime.now())
                .updateDate(LocalDateTime.now())
                .build();

        return new LoginUser(user);
    }
}
// WithMockUser는 간단한 인증만 통과하고 싶을 때 사용!!
// 만약에 세션에 있는 값을 내부에서 사용해야 하면? -> 다른 것을 써야 함.
// @WithMockUser // username="username" password="password"
@WithUserDetails("ssar")
@Test
public void write_테스트() throws Exception {
    // given
    PostWriteReqDto postWriteReqDto = PostWriteReqDto.builder()
            .categoryId(1) // 이거 분명히 터짐
            .title("스프링1강")
            .content("재밌음")
            .build();

    // when
    ResultActions resultActions = mockMvc.perform(
            post("/s/post")
                    .param("title", postWriteReqDto.getTitle())
                    .param("content", postWriteReqDto.getContent())
                    .param("categoryId", postWriteReqDto.getCategoryId() + "")
                    .param("thumnailFile", ""));

    // then
    resultActions
            .andDo(MockMvcResultHandlers.print());
}

 

 

 

 

 

[출처]

 

https://cafe.naver.com/metacoding

 

메타코딩 : 네이버 카페

코린이들의 궁금증

cafe.naver.com

메타 코딩 유튜브

https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9

 

메타코딩

문의사항 : getinthere@naver.com 인스타그램 : https://www.instagram.com/meta4pm 깃헙 : https://github.com/codingspecialist 유료강좌 : https://www.easyupclass.com

www.youtube.com

 

 

반응형