스프링 시큐리티 공식 레퍼런스를 한글로 번역한 문서입니다.
전체 목차는 여기에 있습니다.
목차:
- 19.1. Testing Method Security
- 19.2. Spring MVC Test Integration
- 19.2.1. Setting Up MockMvc and Spring Security
- 19.2.2. SecurityMockMvcRequestPostProcessors
- Testing with CSRF Protection
- Running a Test as a User in Spring MVC Test
- Running as a User in Spring MVC Test with RequestPostProcessor
- Testing HTTP Basic Authentication
- Testing OAuth 2.0
- Testing OIDC Login
- Testing OAuth 2.0 Login
- Testing OAuth 2.0 Clients
- Testing JWT Authentication
- Testing Opaque Token Authentication
- 19.2.3. SecurityMockMvcRequestBuilders
- 19.2.4. SecurityMockMvcResultMatchers
이번 섹션에선 스프링 시큐리티가 지원하는 테스트 기능을 설명한다.
스프링 시큐리티 테스트 기능을 사용하려면 프로젝트 의존성에
spring-security-test-5.3.2.RELEASE.jar
를 추가해야 한다.
19.1. Testing Method Security
이번 섹션은 스프링 시큐리티의 테스트 기능으로 메소드 시큐리티를 테스트하는 방법을 설명한다. 먼저 인증된 사용자만 접근할 수 있는 MessageService
를 소개한다.
public class HelloMessageService implements MessageService {
@PreAuthorize("authenticated")
public String getMessage() {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
return "Hello " + authentication;
}
}
getMessage
결과는 현재 스프링 시큐리티 Authentication
에게 “Hello”라고 인사하는 문자열이다. 아래 보이는 건 출력 예시다.
Hello 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
19.1.1. Security Test Setup
스프링 시큐리티 테스트 기능을 사용하려면 먼저 몇 가지를 설정해야 한다. 예제는 바로 아래에 있다:
@RunWith(SpringJUnit4ClassRunner.class) // (1)
@ContextConfiguration // (2)
public class WithMockUserTests {
이 코드는 스프링 시큐리티 테스트를 세팅하는 기본적인 코드다. 주요 내용은 다음과 같다:
(1) 스프링 테스트 모듈은 @RunWith
를 보고 ApplicationContext
를 생성한다. 기존 스프링 테스트 지원과 동일하다. 자세한 정보는 스프링 레퍼런스를 참고해라.
(2) @ContextConfiguration
은 스프링 테스트 모듈에 ApplicationContext
를 생성할 설정을 알려준다. 설정을 명시하지 않았기 때문에 디폴트 설정 위치를 찾는다. 기존 스프링 테스트 지원과 동일하다. 자세한 정보는 스프링 레퍼런스를 참고해라.
스프링 시큐리티는
WithSecurityContextTestExecutionListener
로 스프링 테스트와 연결된다. 이 리스너는 테스트를 실행하기 전에SecurityContextHolder
를 채워 올바른 사용자로 테스트를 실행할 수 있게 해준다. 리액티브 메소드 시큐리티를 사용한다면ReactiveSecurityContextHolder
를 채우는ReactorContextTestExecutionListener
도 필요하다. 리스너는 테스트를 완료한 후에SecurityContextHolder
를 비운다. 스프링 시큐리티 관련 기능만 필요하다면@ContextConfiguration
대신@SecurityTestExecutionListeners
를 사용해도 된다.
HelloMessageService
엔 @PreAuthorize
어노테이션을 달았었다는 걸 떠올려보자. 따라서 이 메소드를 호출하려면 사용자를 인증해야 한다. 다음 테스트를 실행하면 통과한다:
@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
messageService.getMessage();
}
19.1.2. @WithMockUser
특정 사용자로 테스트를 실행할 수 있는 가장 쉬운 방법은 뭘까? 이 질문에 대한 답은 @WithMockUser
다. 다음 테스트는 사용자 이름 “user”, 비밀번호 “password”, role “ROLE_USER”를 가진 사용자로 실행된다.
@Test
@WithMockUser
public void getMessageWithMockUser() {
String message = messageService.getMessage();
...
}
이 테스트 코드는 다음과 같은 특징이 있다:
- 사용자 이름이 “user”인 사용자를 모킹하므로, 실제로는 없어도 된다.
SecurityContext
에 채워지는Authentication
은UsernamePasswordAuthenticationToken
이다.Authentication
에 있는 principal은 스프링 시큐리티의User
객체다.User
의 사용자 이름은 “user”, 비밀번호는 “password”이며, “ROLE_USER”란 이름의GrantedAuthority
하나를 사용한다.
이 예제는 사용할 수 있는 디폴트 값이 많다는 장점이 있다. 다른 사용자 이름으로 테스트를 실행하고 싶었다면? 아래 테스트는 “customUser”라는 이름으로 실행한다. 다시 말하지만, 이 사용자가 실제로 존재해야 하는 건 아니다.
@Test
@WithMockUser("customUsername")
public void getMessageWithMockUserCustomUsername() {
String message = messageService.getMessage();
...
}
role도 쉽게 커스텀할 수 있다. 예를 들어 이 테스트 코드는 사용자 이름 “admin”과 “ROLE_USER”, “ROLE_ADMIN” role로 실행된다.
@Test
@WithMockUser(username = "admin", roles = { "USER", "ADMIN" })
public void getMessageWithMockUserCustomUser() {
String message = messageService.getMessage();
...
}
자동으로 ROLE_ 프리픽스가 붙는게 싫다면 authorities 속성을 사용하면 된다. 예를 들어 이 코드는 사용자 이름 “admin”과 “USER”, “ADMIN” 권한으로 실행된다.
@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
String message = messageService.getMessage();
...
}
물론 테스트 메소드마다 전부 어노테이션을 다는 건 지루한 일이다. 메소드 대신 클래스 레벨에 어노테이션을 달면 모든 테스트에서 지정한 사용자를 사용한다. 예를 들어 다음 코드는 모든 테스트를 사용자 이름 “admin”, 비밀번호 “password”, role “ROLE_USER”, “ROLE_ADMIN”을 가진 사용자로 실행한다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WithMockUser(username = "admin", roles = { "USER", "ADMIN" })
public class WithMockUserTests {
기본적으로 SecurityContext
는 TestExecutionListener.beforeTestMethod
이벤트에서 설정한다. 이 이벤트는 JUnit의 @Before
실행 전에 해당한다. JUnit의 @Before
실행 후, 테스트 메소드 실행 전에 해당하는 TestExecutionListener.beforeTestExecution
이벤트에서 설정하도록 바꿀 수도 있다.
@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
19.1.3. @WithAnonymousUser
@WithAnonymousUser
를 사용하면 익명 사용자로 테스트를 실행할 수 있다. 테스트 대부분을 지정한 사용자로 실행하고, 일부만 익명 사용자로 실행하고 싶을 때 특히 편리하다. 예를 들어 다음 코드는 withMockUser1(), withMockUser2() 메소드는 @WithMockUser로, anonymous() 메소드는 익명 사용자로 실행한다.
@RunWith(SpringJUnit4ClassRunner.class)
@WithMockUser
public class WithUserClassLevelAuthenticationTests {
@Test
public void withMockUser1() {
}
@Test
public void withMockUser2() {
}
@Test
@WithAnonymousUser
public void anonymous() throws Exception {
// override default to run as anonymous user
}
}
기본적으로 SecurityContext
는 TestExecutionListener.beforeTestMethod
이벤트에서 설정한다. 이 이벤트는 JUnit의 @Before
실행 전에 해당한다. JUnit의 @Before
실행 후, 테스트 메소드 실행 전에 해당하는 TestExecutionListener.beforeTestExecution
이벤트에서 설정하도록 바꿀 수도 있다.
@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
19.1.4. @WithUserDetails
처음 시작할 땐 @WithMockUser
가 편리하지만, 모든 인스턴스에서 동작하지는 않는다. 예를 들어 흔히들 Authentication
principal을 특정 인스턴스 타입으로 사용한다. 이렇게 하면 어플리케이션에선 principal을 커스텀 타입으로 참조할 수 있으며, 스프링 시큐리티와의 결합도도 줄일 수 있다.
커스텀 principal은 보통 커스텀 UserDetailsService
로 UserDetails
와 커스텀 타입을 모두 구현한 객체를 반환한다. 이런 상황에선 커스텀 UserDetailsService
로 테스트 사용자를 만들 수 있어야 한다. 이게 바로 @WithUserDetails
가 하는 일이다.
UserDetailsService
를 빈으로 등록했다고 가정하면, 아래 테스트는 UsernamePasswordAuthenticationToken
타입 Authentication
과 UserDetailsService
가 리턴한 principal, 사용자 이름 “user”로 실행된다.
@Test
@WithUserDetails
public void getMessageWithUserDetails() {
String message = messageService.getMessage();
...
}
UserDetailsService
에서 사용자를 찾을 때 쓸 사용자 이름도 커스텀할 수 있다. 예를 들어 이 테스트는 UserDetailsService
에서 리턴한 principal과 사용자 이름 “customUsername”으로 실행된다.
@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
String message = messageService.getMessage();
...
}
UserDetailsService
를 찾을 때 사용할 빈 이름을 명시하는 것도 가능하다. 예를 들어 이 테스트는 “myUserDetailsService”란 이름을 가진 UserDetailsService
빈을 사용해서 사용자 이름 “customUsername”을 찾는다.
@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
public void getMessageWithUserDetailsServiceBeanName() {
String message = messageService.getMessage();
...
}
@WithMockUser
처럼 클래스 레벨에 어노테이션을 달면 모든 테스트에서 같은 사용자를 사용한다. 하지만 @WithUserDetails
는 @WithMockUser
를 사용할 때와는 달리 실제로 사용자가 있어야 한다.
기본적으로 SecurityContext
는 TestExecutionListener.beforeTestMethod
이벤트에서 설정한다. 이 이벤트는 JUnit의 @Before
실행 전에 해당한다. JUnit의 @Before
실행 후, 테스트 메소드 실행 전에 해당하는 TestExecutionListener.beforeTestExecution
이벤트에서 설정하도록 바꿀 수도 있다.
@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)
19.1.5. @WithSecurityContext
Authentication
principal을 커스텀하지 않으면 @WithMockUser
를 사용하는 게 더 낫다는 것을 확인했다. 다음으로 @WithUserDetails
는 커스텀 UserDetailsService
를 사용해서 Authentication
principal을 만들 수 있지만, 해당 사용자가 실제로 있어야 한다는 것을 알아봤다. 이제부터 살펴볼 옵션은 가장 유연하게 사용할 수 있는 옵션이다.
이제 원하는 SecurityContext
를 생성해 주는 @WithSecurityContext
를 사용해서 자체 어노테이션을 만들 수 있다. 예를 들어 아래와 같은 @WithMockCustomUser
어노테이션을 만들 수 있다.
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
String username() default "rob";
String name() default "Rob Winch";
}
보이는 바와 같이 @WithMockCustomUser
엔 @WithSecurityContext
어노테이션이 선언돼 있다. 스프링 시큐리티 테스트 기능에선 이를 SecurityContext
를 생성해서 테스트를 실행하라는 신호로 받아들인다. @WithSecurityContext
어노테이션엔, @WithMockCustomUser
어노테이션을 선언했을 때 새 SecurityContext
를 만들 SecurityContextFactory
를 지정해야 한다. WithMockCustomUserSecurityContextFactory
구현은 다음과 같다:
public class WithMockCustomUserSecurityContextFactory
implements WithSecurityContextFactory<WithMockCustomUser> {
@Override
public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
CustomUserDetails principal =
new CustomUserDetails(customUser.name(), customUser.username());
Authentication auth =
new UsernamePasswordAuthenticationToken(principal, "password", principal.getAuthorities());
context.setAuthentication(auth);
return context;
}
}
이제 테스트 클래스나 메소드에 이 새 어노테이션을 선언하면 스프링 시큐리티의 WithSecurityContextTestExecutionListener
가 적절한 SecurityContext
를 채워준다.
자체 WithSecurityContextFactory
구현체를 만든다면, 표준 스프링 어노테이션을 선언할 수 있다는 점을 알아두면 좋다. 예를 들어 WithUserDetailsSecurityContextFactory
에선 @Autowired
어노테이션을 사용해서 UserDetailsService
를 주입받는다:
final class WithUserDetailsSecurityContextFactory
implements WithSecurityContextFactory<WithUserDetails> {
private UserDetailsService userDetailsService;
@Autowired
public WithUserDetailsSecurityContextFactory(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public SecurityContext createSecurityContext(WithUserDetails withUser) {
String username = withUser.value();
Assert.hasLength(username, "value() must be non-empty String");
UserDetails principal = userDetailsService.loadUserByUsername(username);
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities());
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
return context;
}
}
기본적으로 SecurityContext
는 TestExecutionListener.beforeTestMethod
이벤트에서 설정한다. 이 이벤트는 JUnit의 @Before
실행 전에 해당한다. JUnit의 @Before
실행 후, 테스트 메소드 실행 전에 해당하는 TestExecutionListener.beforeTestExecution
이벤트에서 설정하도록 바꿀 수도 있다.
@WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)
19.1.6. Test Meta Annotations
여러 테스트에서 같은 사용자를 재사용한다면, 반복해서 속성을 지정하는 건 좋지 않다. 예를 들어 사용자 이름 “admin”과 ROLE_USER
, ROLE_ADMIN
role을 가진 관리자 사용자를 사용하는 테스트가 많다면 매번 다음 코드를 작성할 거다:
@WithMockUser(username = "admin", roles = { "USER", "ADMIN" })
이 코드를 여기저기 반복하는 대신 메타 어노테이션을 활용할 수 있다. 예를 들어 WithMockAdmin
이란 메타 어노테이션을 만들 수 있다:
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="rob",roles="ADMIN")
public @interface WithMockAdmin { }
이제 장황했던 @WithMockUser
대신 @WithMockAdmin
을 똑같은 방법으로 사용할 수 있다.
메타 어노테이션은 위에서 설명한 모든 테스트 어노테이션에 적용할 수 있다. 예를 들어 @WithUserDetails("admin")
이란 메타 어노테이션을 만들 수도 있다.
19.2. Spring MVC Test Integration
스프링 시큐리티는 스프링 MVC 테스트와 종합적으로 통합된다.
19.2.1. Setting Up MockMvc and Spring Security
스프링 시큐리티를 스프링 MVC 테스트와 함께 사용하려면 스프링 시큐리티 FilterChainProxy
를 Filter
로 추가해야 한다. 또한 스프링 시큐리티의 TestSecurityContextHolderPostProcessor
를 추가해야 어노테이션으로 사용자를 지정해 스프링 MVC 테스트를 실행할 수 있다. 이땐 스프링 시큐리티의 SecurityMockMvcConfigurers.springSecurity()
를 사용하면 된다. 예를 들어:
스프링 시큐리티의 테스트 기능은 spring-test-4.1.3.RELEASE나 그 이상이 필요하다.
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SecurityConfig.class)
@WebAppConfiguration
public class CsrfShowcaseTests {
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity()) // (1)
.build();
}
...
(1) SecurityMockMvcConfigurers.springSecurity()
는 스프링 시큐리티를 스프링 MVC 테스트와 통합할 때 필요한 모든 초기 세팅을 수행한다.
19.2.2. SecurityMockMvcRequestPostProcessors
스프링 MVC 테스트는 요청을 수정할 수 있는 편리한 인터페이스 RequestPostProcessor
를 제공한다. 스프링 시큐리티는 테스트를 쉽게 만들어줄 다양한 RequestPostProcessor
구현체를 제공한다. 스프링 시큐리티의 RequestPostProcessor
구현체를 사용하려면 아래 있는 스태틱 임포트를 사용해야 한다:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
Testing with CSRF Protection
스프링 시큐리티의 CSRF 방어를 사용 중인 unsafe HTTP 메소드를 테스트하려면 요청에 유효한 CSRF 토큰을 추가해야 한다. 요청 파라미터에 유효한 CSRF 토큰을 지정하려면 다음 코드를 사용하면 된다:
mvc
.perform(post("/").with(csrf()))
원한다면 CSRF 토큰을 헤더에 넣을 수도 있다:
mvc
.perform(post("/").with(csrf().asHeader()))
아래 코드로 유효하지 않은 CSRF 토큰으로 테스트를 실행할 수도 있다:
mvc
.perform(post("/").with(csrf().useInvalidToken()))
Running a Test as a User in Spring MVC Test
테스트는 특정 사용자로 실행해야 할 때가 많다. 간단하게 사용자를 지정하는 방법은 두 가지가 있다:
Running as a User in Spring MVC Test with RequestPostProcessor
사용자를 현재 HttpServletRequest
로 연결하는 옵션은 다양하다. 예를 들어 다음 코드는 사용자 이름 “user”, 비밀번호 “password”, role “ROLE_USER” 사용자로 (실제로는 없어도 되는 사용자) 실행한다:
사용자를
HttpServletRequest
로 연결하는 방식으로 테스트를 지원한다. 요청을SecurityContextHolder
에 연계하려면SecurityContextPersistenceFilter
를MockMvc
와 연결시켜야 한다. 다음과 같은 방법으로 필터를 연결할 수 있다:
- apply(springSecurity()) 실행
MockMvc
에 스프링 시큐리티의FilterChainProxy
추가MockMvcBuilders.standaloneSetup
을 사용할 땐 수동으로MockMvc
인스턴스에SecurityContextPersistenceFilter
를 추가하는 것도 괜찮다
mvc
.perform(get("/").with(user("user")))
커스텀도 쉽게 할 수 있다. 예를 들어 다음 코드는 사용자 이름 “admin”, 비밀번호 “pass”, role “ROLE_USER”, “ROLE_ADMIN”을 가진 사용자로 (실제로는 없어도 되는 사용자) 실행한다.
mvc
.perform(get("/admin").with(user("admin").password("pass").roles("USER","ADMIN")))
커스텀 UserDetails
를 사용하고 싶다면 함께 명시하면 된다. 예를 들어 다음 코드는 지정한 UserDetails
를 사용해서 (실제로는 없어도 되는), 여기에 있는 principal을 가진 UsernamePasswordAuthenticationToken
으로 테스트를 실행한다:
mvc
.perform(get("/").with(user(userDetails)))
익명 사용자는 다음 코드로 실행할 수 있다:
mvc
.perform(get("/").with(anonymous()))
테스트 대부분을 지정한 사용자로 실행하고, 일부만 익명 사용자로 실행하고 싶을 때 특히 편리하다.
커스텀 Authentication
을 사용하고 싶다면 (실제로는 없어도 되는) 다음 코드를 사용해라:
mvc
.perform(get("/").with(authentication(authentication)))
다음 코드를 사용하면 SecurityContext
도 커스텀할 수 있다:
mvc
.perform(get("/").with(securityContext(securityContext)))
MockMvcBuilders
의 디폴트 요청을 설정하면 모든 요청을 지정한 사용자로 실행할 수도 있다. 예를 들어 다음 코드는 사용자 이름 “admin”, 비밀번호 “password”, role “ROLE_ADMIN”을 가진 사용자로 (실제로는 없어도 되는) 실행한다:
mvc = MockMvcBuilders
.webAppContextSetup(context)
.defaultRequest(get("/").with(user("user").roles("ADMIN")))
.apply(springSecurity())
.build();
여러 테스트에서 같은 사용자를 사용한다면, 사용자 정의를 메소드로 분리하는 게 좋다. 예를 들어 CustomSecurityMockMvcRequestPostProcessors
클래스에 다음 코드를 정의할 수 있다:
public static RequestPostProcessor rob() {
return user("rob").roles("ADMIN");
}
이제 SecurityMockMvcRequestPostProcessors
메소드를 스태틱으로 임포트해서 테스트에 사용할 수 있다:
import static sample.CustomSecurityMockMvcRequestPostProcessors.*;
...
mvc
.perform(get("/").with(rob()))
Running as a User in Spring MVC Test with Annotations
RequestPostProcessor
로 사용자를 만드는 대신 메소드 시큐리티 테스트에서 설명한 어노테이션을 사용해도 된다. 예를 들어 다음 코드는 사용자 이름 “user”, 비밀번호 “password”, role “ROLE_USER”를 가진 사용자로 테스트를 실행한다:
@Test
@WithMockUser
public void requestProtectedUrlWithUser() throws Exception {
mvc
.perform(get("/"))
...
}
또는 다음 코드로 사용자 이름 “user”, 비밀번호 “password”, role “ROLE_ADMIN”을 가진 사용자로 테스트를 실행할 수도 있다:
@Test
@WithMockUser(roles="ADMIN")
public void requestProtectedUrlWithUser() throws Exception {
mvc
.perform(get("/"))
...
}
Testing HTTP Basic Authentication
지금까지도 HTTP 기본 인증을 실행할 순 있었지만, 헤더 이름과 포맷을 기억해두고 값을 인코딩하는 건 지루한 일이었다. 이제는 스프링 시큐리티의 httpBasic
RequestPostProcessor
를 사용할 수 있다. 예를 들어 아래 코드는:
mvc
.perform(get("/").with(httpBasic("user","password")))
HTTP 요청에 아래 헤더를 추가해서 사용자 이름 “user”, 비밀번호 “password”를 가진 사용자로 HTTP 기본 인증을 수행한다:
Authorization: Basic dXNlcjpwYXNzd29yZA==
Testing OAuth 2.0
OAuth 2.0에 관해서라면, 이전에 다룬 원칙이 그대로 적용된다: 궁극적으로 테스트할 메소드가 SecurityContextHolder
안에서 무엇을 사용하냐에 달렸다.
예를 들어 다음과 같은 컨트롤러가 있다면:
@GetMapping("/endpoint")
public String foo(Principal user) {
return user.getName();
}
OAuth2에 특화된 코드가 없기 때문에 예상한대로 단순히 @WithMockUser
를 사용하면 된다.
하지만 다음처럼 테스트하려는 컨트롤러가 스프링 시큐리티의 OAuth 2.0 기능을 사용한다면:
@GetMapping("/endpoint")
public String foo(@AuthenticationPrincipal OidcUser user) {
return user.getIdToken().getSubject();
}
이럴땐 스프링 시큐리티의 테스트 기능이 유용하다.
Testing OIDC Login
스프링 MVC 테스트로 위 메소드를 테스트하려면 인가 서버로 일종의 권한 부여 플로우를 시뮬레이션해야 한다. 시뮬레이션은 확실히 벅찬 일이다. 스프링 시큐리티는 이런 보일러플레이트 없이도 테스트할 수 있도록 지원한다.
예를 들어 아래처럼 SecurityMockMvcRequestPostProcessors#oidcLogin
메소드를 사용해서 스프링 시큐리티에 디폴트 OidcUser
를 추가할 수 있다:
mvc
.perform(get("/endpoint").with(oidcLogin()));
이렇게 하면 관련 MockHttpServletRequest
에, 간단한 OidcIdToken
과 OidcUserInfo
, 부여받은 권한 Collection
을 가지고 있는 OidcUser
를 설정한다.
특히, OidcIdToken
에 user
라는 sub
클레임을 추가해 준다:
assertThat(user.getIdToken().getClaim("sub")).isEqualTo("user");
클레임 셋이 없는 OidcUserInfo
도 추가되며:
assertThat(user.getUserInfo().getClaims()).isEmpty();
SCOPE_read
하나를 가지고 있는 권한 Collection
도 설정된다:
assertThat(user.getAuthorities()).hasSize(1);
assertThat(user.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SCOPE_read"));
스프링 시큐리티는 OidcUser
인스턴스를 @AuthenticationPrincipal
어노테이션에서 사용할 수 있도록 필요한 일을 해준다.
게다가 OidcUser
를 HttpSessionOAuth2AuthorizedClientRepository
에 보관하는 간단한 OAuth2AuthorizedClient
인스턴스로 연결해 준다. @RegisteredOAuth2AuthorizedClient
어노테이션을 사용해서 테스트할 때 유용하다.
Configuring Authorities
많은 경우에 메소드를 필터나 메소드 시큐리티로 보호하고 있으며, 요청을 허용하려면 Authentication
에 특정 권한을 부여해야 한다.
이럴땐 authorities()
메소드로 필요한 권한을 부여할 수 있다:
mvc
.perform(get("/endpoint")
.with(oidcLogin()
.authorities(new SimpleGrantedAuthority("SCOPE_message:read"))
)
);
Configuring Claims
권한 부여는 스프링 시큐리티에서 전반적으로 다루고 있지만, OAuth 2.0에는 클레임이란 개념도 있다.
예를 들어 시스템 내 사용자의 ID를 나타내는 user_id
클레임이 있다고 해보자. 컨트롤러에서는 다음과 같이 클레임에 접근할 수 있다:
@GetMapping("/endpoint")
public String foo(@AuthenticationPrincipal OidcUser oidcUser) {
String userId = oidcUser.getIdToken().getClaim("user_id");
// ...
}
이럴땐 idToken()
메소드로 클레임을 지정할 수 있다:
mvc
.perform(get("/endpoint")
.with(oidcLogin()
.idToken(token -> token.claim("user_id", "1234"))
)
);
이렇게 하면 OidcUser
는 OidcIdToken
에서 클레임을 수집할 수 있다.
Additional Configurations
다른 메소드로도 인증 정보를 설정할 수 있다. 컨트롤러에서 사용하는 데이터에 따라 필요한 메소드를 사용하면 된다.
userInfo(OidcUserInfo.Builder)
-OidcUserInfo
인스턴스 설정clientRegistration(ClientRegistration)
-ClientRegistration
으로 관련OAuth2AuthorizedClient
설정oidcUser(OidcUser)
- 완전한OidcUser
인스턴스 설정
마지막 메소드는 1. OidcUser
자체 구현체를 쓰거나, 2. name 속성을 바꿔야 할 때 유용하다.
예를 들어 인가 서버에서 principal 이름을 sub
클레임이 아닌 user_name
클레임으로 전송한다고 가정해보자. 이럴땐 직접 만든 OidcUser
를 설정할 수 있다:
OidcUser oidcUser = new DefaultOidcUser(
AuthorityUtils.createAuthorityList("SCOPE_message:read"),
Collections.singletonMap("user_name", "foo_user"),
"user_name");
mvc
.perform(get("/endpoint")
.with(oidcLogin().oidcUser(oidcUser))
);
Testing OAuth 2.0 Login
OIDC 로그인 테스트와 마찬가지로, OAuth 2.0 로그인에서도 유사하게 권한 부여 플로우를 모킹해야 한다. 꽤나 까다로운 일이기 때문에, 스프링 시큐리티는 OIDC 외에 다른 테스트도 지원한다.
로그인한 사용자 정보를 OAuth2User
로 받는 컨트롤러가 있다고 가정해보자:
@GetMapping("/endpoint")
public String foo(@AuthenticationPrincipal OAuth2User oauth2User) {
return oauth2User.getAttribute("sub");
}
이럴땐 아래처럼 SecurityMockMvcRequestPostProcessors#oauth2User
를 사용해서 스프링 시큐리티에 디폴트 OAuth2User
를 추가할 수 있다:
mvc
.perform(get("/endpoint").with(oauth2Login()));
이렇게 하면 관련 MockHttpServletRequest
에, 간단한 속성 Map
과 부여받은 권한 Collection
을 가지고 있는 OAuth2User
를 설정한다.
특히, Map
에 키/값 sub
/user
를 추가해 준다:
assertThat((String) user.getAttribute("sub")).isEqualTo("user");
SCOPE_read
하나를 가지고 있는 권한 Collection
도 설정된다:
assertThat(user.getAuthorities()).hasSize(1);
assertThat(user.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SCOPE_read"));
스프링 시큐리티는 OAuth2User
인스턴스를 @AuthenticationPrincipal
어노테이션에서 사용할 수 있도록 필요한 일을 해준다.
게다가 OAuth2User
를 HttpSessionOAuth2AuthorizedClientRepository
에 보관하는 간단한 OAuth2AuthorizedClient
인스턴스로 연결해 준다. @RegisteredOAuth2AuthorizedClient
어노테이션을 사용해서 테스트할 때 유용하다.
Configuring Authorities
많은 경우에 메소드를 필터나 메소드 시큐리티로 보호하고 있으며, 요청을 허용하려면 Authentication
에 특정 권한을 부여해야 한다.
이럴땐 authorities()
메소드로 필요한 권한을 부여할 수 있다:
mvc
.perform(get("/endpoint")
.with(oauth2Login()
.authorities(new SimpleGrantedAuthority("SCOPE_message:read"))
)
);
Configuring Claims
권한 부여는 스프링 시큐리티에서 전반적으로 다루고 있지만, OAuth 2.0에는 클레임이란 개념도 있다.
예를 들어 시스템 내 사용자의 ID를 나타내는 user_id
속성이 있다고 해보자. 컨트롤러에서는 다음과 같이 속성에 접근할 수 있다:
@GetMapping("/endpoint")
public String foo(@AuthenticationPrincipal OAuth2User oauth2User) {
String userId = oauth2User.getAttribute("user_id");
// ...
}
이럴땐 attributes()
메소드로 속성을 지정할 수 있다:
mvc
.perform(get("/endpoint")
.with(oauth2Login()
.attributes(attrs -> attrs.put("user_id", "1234"))
)
);
Additional Configurations
다른 메소드로도 인증 정보를 설정할 수 있다. 컨트롤러에서 사용하는 데이터에 따라 필요한 메소드를 사용하면 된다.
clientRegistration(ClientRegistration)
-ClientRegistration
으로 관련OAuth2AuthorizedClient
설정oauth2User(OAuth2User)
- 완전한OAuth2User
인스턴스 설정
마지막 메소드는 1. OAuth2User
자체 구현체를 쓰거나, 2. name 속성을 바꿔야 할 때 유용하다.
예를 들어 인가 서버에서 principal 이름을 sub
클레임이 아닌 user_name
클레임으로 전송한다고 가정해보자. 이럴땐 직접 만든 OAuth2User
를 설정할 수 있다:
OAuth2User oauth2User = new DefaultOAuth2User(
AuthorityUtils.createAuthorityList("SCOPE_message:read"),
Collections.singletonMap("user_name", "foo_user"),
"user_name");
mvc
.perform(get("/endpoint")
.with(oauth2Login().oauth2User(oauth2User))
);
Testing OAuth 2.0 Clients
사용자를 인증하는 방법과는 상관 없이, 테스트하고 싶은 요청에서 다른 토큰과 클라이언트 등록 정보를 사용할 수도 있다. 예를 들어 컨트롤러에서 클라이언트에 부여한 credential로 사용자 아무런 연관이 없는 토큰을 가져올 수 있다:
@GetMapping("/endpoint")
public String foo(@RegisteredOAuth2AuthorizedClient("my-app") OAuth2AuthorizedClient authorizedClient) {
return this.webClient.get()
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String.class)
.block();
}
인가 서버와의 핸드셰이킹을 시뮬레이션하긴 번거롭다. 대신에 SecurityMockMvcRequestPostProcessor#oauth2Client
를 사용해서 HttpSessionOAuth2AuthorizedClientRepository
에 OAuth2AuthorizedClient
를 추가할 수 있다:
mvc
.perform(get("/endpoint").with(oauth2Client("my-app")));
어플리케이션에서 사용 중인 HttpSessionOAuth2AuthorizedClientRepository
가 없다면 @TestConfiguration
에 하나를 등록할 수 있다:
@TestConfiguration
static class AuthorizedClientConfig {
@Bean
OAuth2AuthorizedClientRepository authorizedClientRepository() {
return new HttpSessionOAuth2AuthorizedClientRepository();
}
}
이렇게 하면 간단한 ClientRegistration
, OAuth2AccessToken
, 리소스 소유자 이름을 가지고 있는 OAuth2AuthorizedClient
를 생성한다.
특히 ClientRegistration
에 클라이언트 ID “test-client”, 클라이언트 secret “test-secret”을 가지고 있는 ClientRegistration
을 추가해 준다:
assertThat(authorizedClient.getClientRegistration().getClientId()).isEqualTo("test-client");
assertThat(authorizedClient.getClientRegistration().getClientSecret()).isEqualTo("test-secret");
리소스 소유자 이름 “user”도 추가되며:
assertThat(authorizedClient.getPrincipalName()).isEqualTo("user");
read
스코프 하나를 가지고 있는 OAuth2AccessToken
도 설정된다:
assertThat(authorizedClient.getAccessToken().getScopes()).hasSize(1);
assertThat(authorizedClient.getAccessToken().getScopes()).containsExactly("read");
스프링 시큐리티는 OAuth2AuthorizedClient
인스턴스를 관련 HttpSession
에서 사용할 수 있도록 필요한 일을 해준다. 덕분에 HttpSessionOAuth2AuthorizedClientRepository
에서 OAuth2AuthorizedClient
를 조회할 수 있다.
Configuring Scopes
OAuth 2.0 액세스 토큰은 흔히 스코프 셋을 함께 제공한다. 컨트롤러에선 다음과 같이 스코프를 참조할 수 있다:
@GetMapping("/endpoint")
public String foo(@RegisteredOAuth2AuthorizedClient("my-app") OAuth2AuthorizedClient authorizedClient) {
Set<String> scopes = authorizedClient.getAccessToken().getScopes();
if (scopes.contains("message:read")) {
return this.webClient.get()
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String.class)
.block();
}
// ...
}
스코프는 accessToken()
메소드로 설정할 수 있다:
mvc
.perform(get("/endpoint")
.with(oauth2Client("my-app")
.accessToken(new OAuth2AccessToken(BEARER, "token", null, null, Collections.singleton("message:read"))))
)
);
Additional Configurations
다른 메소드로도 인증 정보를 설정할 수 있다. 컨트롤러에서 사용하는 데이터에 따라 필요한 메소드를 사용하면 된다.
principalName(String)
- 리소스 소유자 이름 설정clientRegistration(Consumer<ClientRegistration.Builder>)
- 관련ClientRegistration
설정clientRegistration(ClientRegistration)
- 완전한ClientRegistration
설정
마지막 메소드는 실제 ClientRegistration
을 사용하고 싶을 때 유용하다.
예를 들어 application.yml
에 있는 어플리케이션의 ClientRegistration
정의 중 하나를 사용하고 싶다고 해보자.
이럴땐 테스트 코드에 ClientRegistrationRepository
를 주입하면 필요한 빈을 찾아준다:
@Autowired
ClientRegistrationRepository clientRegistrationRepository;
// ...
mvc
.perform(get("/endpoint")
.with(oauth2Client()
.clientRegistration(this.clientRegistrationRepository.findByRegistrationId("facebook"))));
Testing JWT Authentication
리소스 서버에 권한을 부여받은 요청을 만들려면 bearer 토큰이 필요하다.
리소스 서버를 JWT로 설정했다면 bearer 토큰을 서명한 다음 JWT 스펙에 따라 인코딩해야 한다. 이 모든 일은 꽤나 벅찬 일이며, 특히나 테스트하려는 핵심 로직이 아닐 땐 더 그렇다.
다행히도 테스트 코드에선 bearer 토큰 표현 대신 간단히 인가 로직에만 집중할 수 있는 방법이 많이 있다. 여기서는 두 가지 방법을 살펴보겠다.
jwt() RequestPostProcessor
첫 번째 방법은 RequestPostProcessor
를 사용하는 방법이다. 가장 간단하게는 아래처럼 사용할 수 있다:
mvc
.perform(get("/endpoint").with(jwt()));
이렇게 하면 관련 목 Jwt
를 생성해서 인증 API로 넘겨주기 때문에, 인가 메커니즘 검증에 활용할 수 있다.
디폴트 JWT
는 다음과 같이 생성한다:
{
"headers" : { "alg" : "none" },
"claims" : {
"sub" : "user",
"scope" : "read"
}
}
테스트를 실행하면 결과적으로 다음과 같은 Jwt
를 전달한다:
assertThat(jwt.getTokenValue()).isEqualTo("token");
assertThat(jwt.getHeaders().get("alg")).isEqualTo("none");
assertThat(jwt.getSubject()).isEqualTo("sub");
GrantedAuthority authority = jwt.getAuthorities().iterator().next();
assertThat(authority.getAuthority()).isEqualTo("read");
물론 이 값들도 설정할 수 있다.
헤더나 클레임은 전용 메소드를 사용해 설정한다:
mvc
.perform(get("/endpoint")
.with(jwt().jwt(jwt -> jwt.header("kid", "one").claim("iss", "https://idp.example.org"))));
mvc
.perform(get("/endpoint")
.with(jwt().jwt(jwt -> jwt.claims(claims -> claims.remove("scope")))));
여기선 일반 bearer 토큰 요청과 동일하게 scope
, scp
클레임을 처리한다. 하지만 테스트에 필요한 GrantedAuthority
인스턴스 목록을 제공해서 간단히 재정의할 수 있다:
mvc
.perform(get("/endpoint")
.with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_messages"))));
또는 Jwt
를 Collection<GrantedAuthority>
로 변환하는 커스텀 컨버터로도 권한을 생성할 수 있다:
mvc
.perform(get("/endpoint")
.with(jwt().authorities(new MyConverter())));
Jwt.Builder
를 사용해서 Jwt
를 직접 만들 수도 있다:
Jwt jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.claim("sub", "user")
.claim("scope", "read")
.build();
mvc
.perform(get("/endpoint")
.with(jwt().jwt(jwt)));
authentication()
RequestPostProcessor
두 번째 방법은 authentication()
RequestPostProcessor
를 사용하는 방법이다. 기본적으로 아래 코드처럼 직접 만든 JwtAuthenticationToken
인스턴스를 테스트에 제공할 수 있다:
Jwt jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.claim("sub", "user")
.build();
Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("SCOPE_read");
JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities);
mvc
.perform(get("/endpoint")
.with(authentication(token)));
이 두 가지 방법 외에도 @MockBean
어노테이션으로 JwtDecoder
빈 자체를 모킹하는 방법도 있다.
Testing Opaque Token Authentication
opaque 토큰도 JWT와 유사하게 유효성 검증을 위해서는 인가 서버가 필요하므로, 테스트하기가 더 까다롭다. 이를 위해 스프링 시큐리티는 opaque 토큰을 위한 테스트 기능을 지원한다.
BearerTokenAuthentication
으로 인증 정보를 가져오는 컨트롤러가 있다고 가정해보자:
@GetMapping("/endpoint")
public String foo(BearerTokenAuthentication authentication) {
return (String) authentication.getTokenAttributes("sub");
}
이럴땐 아래처럼 SecurityMockMvcRequestPostProcessors#opaqueToken
메소드를 사용해서 스프링 시큐리티에 디폴트 BearerTokenAuthentication
을 추가할 수 있다:
mvc
.perform(get("/endpoint").with(opaqueToken()));
이렇게 하면 관련 MockHttpServletRequest
에, 간단한 OAuth2AuthenticatedPrincipal
과, 속성 Map
, 부여받은 권한 Collection
을 가지고 있는 BearerTokenAuthentication
을 설정한다.
특히, Map
에 키/값 sub
/user
를 추가해 준다:
assertThat((String) token.getTokenAttributes().get("sub")).isEqualTo("user");
SCOPE_read
하나를 가지고 있는 권한 Collection
도 설정된다:
assertThat(token.getAuthorities()).hasSize(1);
assertThat(token.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SCOPE_read"));
스프링 시큐리티는 BearerTokenAuthentication
인스턴스를 컨트롤러 메소드에서 사용할 수 있도록 필요한 일을 해준다.
Configuring Authorities
많은 경우에 메소드를 필터나 메소드 시큐리티로 보호하고 있으며, 요청을 허용하려면 Authentication
에 특정 권한을 부여해야 한다.
이럴땐 authorities()
메소드로 필요한 권한을 부여할 수 있다:
mvc
.perform(get("/endpoint")
.with(opaqueToken()
.authorities(new SimpleGrantedAuthority("SCOPE_message:read"))
)
);
Configuring Claims
권한 부여는 스프링 시큐리티에서 전반적으로 다루고 있지만, OAuth 2.0에는 속성이란 개념도 있다.
예를 들어 시스템 내 사용자의 ID를 나타내는 user_id
속성이 있다고 해보자. 컨트롤러에서는 다음과 같이 속성에 접근할 수 있다:
@GetMapping("/endpoint")
public String foo(BearerTokenAuthentication authentication) {
String userId = (String) authentication.getTokenAttributes().get("user_id");
// ...
}
이럴땐 attributes()
메소드로 속성을 지정할 수 있다:
mvc
.perform(get("/endpoint")
.with(opaqueToken()
.attributes(attrs -> attrs.put("user_id", "1234"))
)
);
Additional Configurations
다른 메소드로도 인증 정보를 설정할 수 있다. 컨트롤러에서 사용하는 데이터에 따라 필요한 메소드를 사용하면 된다.
한 가지 메소드는 BearerTokenAuthentication
에 넣을 OAuth2AuthenticatedPrincipal
인스턴스를 직접 설정할 수 있는 principal(OAuth2AuthenticatedPrincipal)
이다.
이 메소드는 1. OAuth2AuthenticatedPrincipal
자체 구현체를 쓰거나, 2. principal 이름을 바꿔야 할 때 유용하다.
예를 들어 인가 서버에서 principal 이름을 sub
속성이 아닌 user_name
속성으로 전송한다고 가정해보자. 이럴땐 직접 만든 OAuth2AuthenticatedPrincipal
을 설정할 수 있다:
Map<String, Object> attributes = Collections.singletonMap("user_name", "foo_user");
OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal(
(String) attributes.get("user_name"),
attributes,
AuthorityUtils.createAuthorityList("SCOPE_message:read"));
mvc
.perform(get("/endpoint")
.with(opaqueToken().principal(principal))
);
opaqueToken()
을 사용하는 방법 외에도 @MockBean
어노테이션으로 OpaqueTokenIntrospector
빈 자체를 모킹하는 방법도 있다.
19.2.3. SecurityMockMvcRequestBuilders
스프링 MVC 테스트는 테스트에 사용할 MockHttpServletRequest
생성하는 RequestBuilder
인터페이스도 지원한다. 스프링 시큐리티는 더 편리한 RequestBuilder
구현체를 몇 가지 제공한다. 스프링 시큐리티의 RequestBuilder
구현체를 사용하려면 아래 있는 스태틱 임포트를 사용해야 한다:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.*;
Testing Form Based Authentication
스프링 시큐리티의 테스트 기능을 사용하면 쉽게 폼 기반 인증 테스트 요청을 만들 수 있다. 예를 들어 다음 코드는 사용자 이름 “user”, 비밀번호 “password”와 유효한 CSRF 토큰으로 “/login”에 POST 요청을 제출한다:
mvc
.perform(formLogin())
요청을 커스텀하는 것도 간단하다. 예를 들어 다음 코드는 사용자 이름 “admin”, 비밀번호 “pass”와 유효한 CSRF 토큰으로 “/auth”에 POST 요청을 제출한다:
mvc
.perform(formLogin("/auth").user("admin").password("pass"))
사용자 이름과 비밀번호를 넣을 파라미터 이름도 커스텀할 수 있다. 예를 들어 다음은 위에 있는 요청을 HTTP 파라미터 “u”에 사용자 이름을, HTTP 파라미터 “p”에 비밀번호를 추가하도록 수정한 코드다:
mvc
.perform(formLogin("/auth").user("u","admin").password("p","pass"))
Testing Logout
간단하게 표준 스프링 MVC 테스트를 사용할 수도 있지만, 스프링 시큐리티 테스트 기능을 사용하면 로그아웃 테스트를 더 쉽게 구현할 수 있다. 예를 들어 다음 코드는 유효한 CSRF 토큰으로 “/logout”에 POST 요청을 보낸다:
mvc
.perform(logout())
요청을 보낼 URL을 커스텀할 수도 있다. 예를 들어 아래 코드는 유효한 CSRF 토큰으로 “/signout”에 POST 요청을 보낸다:
mvc
.perform(logout("/signout"))
19.2.4. SecurityMockMvcResultMatchers
때로는 보안 로직을 검증해야 할 때도 있다. 이를 위해 스프링 시큐리티 테스트에선 스프링 MVC 테스트의 ResultMatcher
인터페이스 구현체를 제공한다. 스프링 시큐리티의 ResultMatcher
구현체를 사용하려면 아래 있는 스태틱 임포트를 사용해야 한다:
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*;
Unauthenticated Assertion
MockMvc
를 호출했을 때 인증된 사용자가 없었음을 확인해야할 때가 있다. 예를 들어 유효하지 않은 사용자 이름과 비밀번호를 제출했을 땐 인증된 사용자가 없었다는 것을 검증해야 한다. 다음과 같이 스프링 시큐리티의 테스트 기능을 사용하면 쉽게 구현할 수 있다:
mvc
.perform(formLogin().password("invalid"))
.andExpect(unauthenticated());
Authenticated Assertion
반대로 인증된 사용자가 있음을 확인해야 할 때도 있다. 예를 들어 인증에 성공했는지 검증해야 할 수 있다. 다음 코드를 사용하면 폼 기반 로그인에 성공했는지 검증할 수 있다:
mvc
.perform(formLogin())
.andExpect(authenticated());
사용자의 role을 확인하고 싶다면 아래처럼 코드를 수정하면 된다:
mvc
.perform(formLogin().user("admin"))
.andExpect(authenticated().withRoles("USER","ADMIN"));
아니면 사용자 이름을 검증할 수도 있다:
mvc
.perform(formLogin().user("admin"))
.andExpect(authenticated().withUsername("admin"));
여러 가지 검증 조건을 조합하는 것도 가능하다:
mvc
.perform(formLogin().user("admin").roles("USER","ADMIN"))
.andExpect(authenticated().withUsername("admin"));
인증에 대한 임의의 검증 조건을 추가할 수도 있다:
mvc
.perform(formLogin())
.andExpect(authenticated().withAuthentication(auth ->
assertThat(auth).isInstanceOf(UsernamePasswordAuthenticationToken.class)));
Next :Spring Security Crypto Module
스프링 시큐리티 암호화(Crypto) 모듈을 소개합니다. 공식 문서에 있는 "Spring Security Crypto Module" 챕터를 한글로 번역한 문서입니다.
전체 목차는 여기에 있습니다.