spring security test code 작성시 UserDetails가 필요할때

spring security test code 작성시 UserDetails가 필요할때

스프링 시큐리티 테스트 코드 작성시 ‘@WithMockUser’ 어너테이션을 사용해서 인증을 통과 시킨다
하지만 어떤 API는 UserDetails를 필요로 할수 있다 그럴때 커스텀 어너테이션을 만들어서 사용할수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

@Configuration
public class SecurityConfigruation extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {

http.authorizeRequests().antMatchers("**").authenticated().and().formLogin()
.disable().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS).and()
.exceptionHandling()
.authenticationEntryPoint(new Http403ForbiddenEntryPoint())
.accessDeniedHandler(new AccessDeniedHandlerImpl());
}

}

위처럼 간단하게 인증을 걸고 API를 하나 만들어서 테스트 코드를 작성해서 테스트 해보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

@Slf4j
@RequestMapping("/api/")
@RestController
public class TestController {

@GetMapping("test")
public ResponseEntity<String> getTest(Authentication principal) {

var userName = principal.getName();
log.debug("test ok {}", principal.getDetails());

return ResponseEntity.ok(userName);
}
}

위에 컨트롤러 하나 생성후에 테스트 코드를 작성해 보면

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


@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
class TestControllerTest {

private static final String URL = "/api/test";

@Autowired
private MockMvc mockMvc;

@DisplayName("인증실패")
@Test
void forbiddenTest() throws Exception {
this.mockMvc.perform(get(URL)
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
.andDo(print()).andExpect(status().isForbidden());
}

@DisplayName("WithMockUser 어너테이션 테스트")
@WithMockUser
@Test
void withMockTest() throws Exception {
this.mockMvc.perform(get(URL)
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
.andDo(print()).andExpect(status().isOk());
}

}

인증실패 결과

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
32
33
34
35
36
MockHttpServletRequest:
HTTP Method = GET
Request URI = /api/test
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8", Accept:"application/json"]
Body = null
Session Attrs = {}

Handler:
Type = null

Async:
Async started = false
Async result = null

Resolved Exception:
Type = null

ModelAndView:
View name = null
View = null
Model = null

FlashMap:
Attributes = null

MockHttpServletResponse:
Status = 403
Error message = Access Denied
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []

WithMockUser 어너테이션 테스트 결과

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
32
33
34
35
36
37
38

MockHttpServletRequest:
HTTP Method = GET
Request URI = /api/test
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8", Accept:"application/json"]
Body = null
Session Attrs = {SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@ca25360: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@ca25360: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER}

Handler:
Type = io.github.sejoung.controller.TestController
Method = io.github.sejoung.controller.TestController#getTest(Authentication)

Async:
Async started = false
Async result = null

Resolved Exception:
Type = null

ModelAndView:
View name = null
View = null
Model = null

FlashMap:
Attributes = null

MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"4", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = application/json
Body = user
Forwarded URL = null
Redirected URL = null
Cookies = []

위에 결과를 보면 인증은 통과 했지만 Details: null; 이다.

그럼 값을 넣어 주는것을 테스트 해서 보면

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.springframework.security.test.context.support.WithSecurityContext;

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {

String username() default "sejoung";

String name() default "sejoung kim";
}

위 처럼 어너테이션을 생성 하고

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public class WithMockCustomUserSecurityContextFactory
implements WithSecurityContextFactory<WithMockCustomUser> {

@Override
public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
List<GrantedAuthority> grantedAuthorities = new ArrayList();
grantedAuthorities.add(new SimpleGrantedAuthority("USER"));
User principal = new User(customUser.username(), "1234", true, true, true, true,
grantedAuthorities);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
principal, principal.getPassword(), principal.getAuthorities());

authentication.setDetails(new Detail(customUser.username(), "aaaa"));
context.setAuthentication(authentication);
return context;
}
}

위 처럼 팩토리를 하나 만들어서 사용하면

테스트 코드는 하나 추가해서 보면

1
2
3
4
5
6
7
8
9
@DisplayName("커스텀 어너테이션 테스트")
@WithMockCustomUser
@Test
void customWithMockTest() throws Exception {
this.mockMvc.perform(get(URL)
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
.andDo(print()).andExpect(status().isOk());
}

커스텀 어너테이션 테스트 결과

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
32
33
34
35
36
37
38

MockHttpServletRequest:
HTTP Method = GET
Request URI = /api/test
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8", Accept:"application/json"]
Body = null
Session Attrs = {SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@b3ebb9fe: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@b3ebb9fe: Principal: org.springframework.security.core.userdetails.User@75d00a77: Username: sejoung; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: USER; Credentials: [PROTECTED]; Authenticated: true; Details: Detail(name=sejoung, data=aaaa); Granted Authorities: USER}

Handler:
Type = io.github.sejoung.controller.TestController
Method = io.github.sejoung.controller.TestController#getTest(Authentication)

Async:
Async started = false
Async result = null

Resolved Exception:
Type = null

ModelAndView:
View name = null
View = null
Model = null

FlashMap:
Attributes = null

MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"7", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = application/json
Body = sejoung
Forwarded URL = null
Redirected URL = null
Cookies = []

Details: Detail(name=sejoung, data=aaaa) 이렇게 값이 들어간것을 볼수 있다.

참조