[Spring Security] @withUserDetails 알아보기

su_y2on·2022년 7월 11일
4

Security

목록 보기
4/4
post-thumbnail

@withUserDetails 알아보기

SpringSecurity를 통해서 접근제어를 해놓은 URL로 Controller Test를 할 때 매번 인증로직(로그인, 토큰..)을 붙여줘야할까요..?


저는 JWT로 사용자인증을 짰었기 때문에 매번 header로 발급받은 토큰을 넘겨줘야하는 불편함이 생겼습니다. 좀더 쉽게하는 방법이 있지않을까 해서 찾아본 방법이 SecurityContextHolder에 특정 user를 넣어줄 수 있는 방법이 있었습니다.

원래라면 (1)header에서 token을 추출해서 유효성검사 및 정보를 꺼내서 Security Context Holder에 넣어주게됩니다. 우리는 이것을 인증받은 유저라고 판단하여 (2)가져다 쓰게 됩니다.


(1) 코드 (중요부분만 가져왔습니다)

User user = userDetailService.loadUserByUsername(userName);
// user로 User
Authentication authentication = new UsernamePasswordAuthenticationToken
									(user, null,user.getAuthorities());

// contextholder 에 저장
SecurityContextHolder.getContext().setAuthentication(authentication);

(2) 코드
@AuthenticationPrincipal User user를 통해서 user에 SecurityContextHolder에서 user를 빼오게 됩니다.

@PostMapping
public ResponseEntity<Long> insert( @Valid @RequestBody CreateHairshopRequest createHairshopRequest,
		@AuthenticationPrincipal User user) {
        if (!user.getId().equals(createHairshopRequest.getUserId())){
            throw new IllegalArgumentException("본인의 헤어샵만 생성할 수 있습니다.");
        }
        HairshopResponse insert = hairshopService.insert(createHairshopRequest);
        
        return ResponseEntity.created(URI.create("/hairshops/" + insert.getId()))
            .body(insert.getId());
    }

이때 @withUserDetails활용하면 Test에서 header에 token을 넣어주지 않아도 (1)번 과정을 간편하게 구현할 수 있습니다.




1. @withUserDetails로 Test Code작성하기

위에서 헤어샵을 추가하는 Controller를 테스트하는 코드입니다. 이때 @WithUserDetails(value = "example2@naver.com"를 통해서 SecurityContextHolder에 "example2@naver.com"을 username으로 갖는 User가 등록됩니다.따라서 @AuthenticationPrincipal User user에서 그 user를 가져다가 헤어샵을 등록하게 됩니다.

 @DisplayName("헤어샵 등록 테스트")
 @WithUserDetails(value = "example2@naver.com")
 void HAIRSHOP_INSERT_TEST() throws Exception {
        createHairshopRequest = CreateHairshopRequest.builder()
            .name("코스헤어")
            .phoneNumber("010-1234-1234")
            .startTime("11:00")
            .endTime("20:00")
            .closedDay("화")
            .reservationRange("1")
            .reservationStartTime("11:00")
            .reservationEndTime("19:30")
            .sameDayAvailable(true)
            .roadNameNumber("대구 중구 동성로2가 143-9 2층")
            .profileImg(
                "https://mud-kage.kakao.com/dn/fFVWf/btqFiGBCOe6/LBpRsfUQtqrPHAWMk5DDw0/img_1080x720.jpg")
            .introduction("예약 전 DM으로 먼저 문의해주세요 :)")
            .userId(user.getId())
            .build();
        this.mockMvc.perform(post("/hairshops")
                .characterEncoding(StandardCharsets.UTF_8)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(createHairshopRequest)))
            .andExpect(status().isCreated());
            
 }

다른 간단한 방법들도 많지만 이 방법을 썼던 것은 User객체 자체가 필요했기때문입니다. @withUserDetails는 UserDetailsService를 통해서 User객체를 직접 반환하기 때문에 이렇게 객체가 필요할 때는 이 방법을 쓰는게 좋은 것 같습니다.



value는 어떤 username을 갖은 user를 SecurityContextHolder에 넣을지 userDetailsServiceBeanName은 어떤 UserDetailsService를 이용할지 알려주는 것입니다. 만약 Custom을 했다면 그것을 적어주면 되겠죠?

@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")



2. 주의할 점

실제로 해당 username을 가진 user가 존재해야합니다

따라서 Test진행전에 user객체를 넣어놔야하기때문에 @BeforeAll 이나 @BeforeEach를 통해서 user를 저장해놓으면 됩니다.



여기서 또다른 문제 @BeforeEach, @BeforeAll과 함께할 때 오류

SecurityContext는 default로 TestExecutionListener.beforeTestMethod로 설정이 되어있습니다. 즉 @Before 전에 @withUserDetails이 동작해서 SecurityContext안에 넣으려고합니다. 따라서 실 user객체가 생기기전에 해당 user를 찾게 되죠. 결국 SecurityContext가 제대로 생기지 못하고 테스트가 실패합니다.


우리의 의도에 맞게 user를 생성해주고 Test를 진행하려면 TestExecutionListener.beforeTestExecution로 설정해주면 됩니다. 아래와 같이 setupBefore에 넣어주면 쉽게 설정할 수 있습니다.

@WithUserDetails(value = "example2@naver.com", setupBefore = TestExecutionEvent.TEST_EXECUTION)


SpringSecurity공식자료에서 나와있는 해결책을 기반으로 작성하였습니다. 이 부분은 꽤 이슈가 되었었던 것 같고 위와 같은 방법으로 해결책을 낸 것 같네요!!



Spring Security가 포함된 test작성에 도움이 되었으면 좋겠습니다✨✨✨

0개의 댓글