본문으로 건너뛰기
김신건의 로그

[Spring Security] Testing: @WithMockUser, MockMvc, jwt(), oauth2Login()

· 수정 · 📖 약 2분 · 610자/단어 #spring #security #testing #mockmvc #junit
Spring Security Test, @WithMockUser, @WithUserDetails, MockMvc security, csrf().with, jwt().jwt, 스프링 시큐리티 테스트

정의

Spring Security 가 적용된 컨트롤러를 테스트할 때 흔한 어려움:

  • 매번 실제 로그인 절차를 거쳐야 인증된 요청 가능
  • CSRF token 이 없으면 POST 거부
  • OAuth2 / JWT 환경에서 mock token 생성 번거로움

spring-security-test 모듈이 이걸 한 줄로 해결: @WithMockUser, @WithUserDetails, csrf(), jwt(), oauth2Login(), httpBasic() 등.

의존성

testImplementation("org.springframework.security:spring-security-test")
testImplementation("org.springframework.boot:spring-boot-starter-test")

@WithMockUser (가장 흔함)

테스트 메서드 실행 시 SecurityContext 에 mock user 세팅:

java
@WebMvcTest(UserController.class)
class UserControllerTest {
  @Autowired MockMvc mvc;
  @MockBean UserService userService;

  @Test
  @WithMockUser(username = "alice", roles = {"USER"})
  void getMe_returnsCurrentUser() throws Exception {
      when(userService.findByEmail("alice")).thenReturn(new User("alice"));
      mvc.perform(get("/me"))
          .andExpect(status().isOk())
          .andExpect(jsonPath("$.username").value("alice"));
  }

  @Test
  void getMe_unauthorized_when_anonymous() throws Exception {
      mvc.perform(get("/me"))
          .andExpect(status().isUnauthorized());
  }
}
테스트 결과
getMe_returnsCurrentUser: PASSED
getMe_unauthorized_when_anonymous: PASSED

@WithMockUser 의 기본:

  • username = “user”
  • password = “password”
  • authorities = ROLE_USER

옵션:

@WithMockUser(
    username = "admin",
    roles = {"ADMIN", "USER"},                    // ROLE_ 자동 prefix
    authorities = {"SCOPE_read", "SCOPE_write"}   // raw authority (prefix 없음)
)

@WithUserDetails

실제 UserDetailsService 를 호출해 로딩:

@Test
@WithUserDetails("alice@example.com")
void purchaseHistory() throws Exception {
    mvc.perform(get("/purchases"))
        .andExpect(status().isOk());
}

UserDetailsService 가 DB / mock 에서 사용자를 조회. role / authority / customer-specific 필드까지 실제와 동일. 통합 테스트에 적합.

@WithSecurityContext (커스텀)

@WithMockUser 가 부족할 때:

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithCustomUserFactory.class)
public @interface WithCustomUser {
    long userId();
    String tenantId() default "default";
}

class WithCustomUserFactory implements WithSecurityContextFactory<WithCustomUser> {
    @Override
    public SecurityContext createSecurityContext(WithCustomUser anno) {
        SecurityContext ctx = SecurityContextHolder.createEmptyContext();
        CustomPrincipal p = new CustomPrincipal(anno.userId(), anno.tenantId());
        ctx.setAuthentication(new UsernamePasswordAuthenticationToken(p, null, List.of()));
        return ctx;
    }
}

// 사용
@Test
@WithCustomUser(userId = 42, tenantId = "acme")
void multiTenant() throws Exception { ... }

multi-tenant / 복잡한 principal 구조 테스트에 유용.

CSRF Token Mock

POST / PUT / DELETE 가 CSRF token 없이는 403 거부됨. 테스트에서:

@Test
@WithMockUser
void createPost() throws Exception {
    mvc.perform(post("/posts")
            .with(csrf())                      // ← 핵심
            .contentType(MediaType.APPLICATION_JSON)
            .content("""{"title":"hello"}"""))
        .andExpect(status().isCreated());
}

with(csrf()) 가 valid CSRF token 을 요청에 자동 추가. 의도적으로 실패 테스트하려면 with(csrf().asHeader().useInvalidToken()).

JWT Mock (Resource Server)

@Test
void readApi_withJwt() throws Exception {
    mvc.perform(get("/api/data")
            .with(jwt().jwt(jwt -> jwt
                .claim("sub", "alice")
                .claim("scope", "read write"))
                .authorities(new SimpleGrantedAuthority("SCOPE_read"))))
        .andExpect(status().isOk());
}

jwt() 가 OAuth2 Resource Server 가 검증한 것처럼 SecurityContext 세팅. 실제 JWT 서명 검증은 skip.

OAuth2 Login Mock

@Test
void oauth2_login() throws Exception {
    mvc.perform(get("/me")
            .with(oauth2Login()
                .attributes(attrs -> attrs.put("email", "alice@example.com"))
                .authorities(new SimpleGrantedAuthority("ROLE_USER"))))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.email").value("alice@example.com"));
}

OAuth2User 또는 OidcUser 가 컨트롤러 파라미터로 주입되는 시나리오 테스트.

httpBasic, formLogin

mvc.perform(get("/api/data")
    .with(httpBasic("alice", "secret")))   // Basic Auth 헤더 추가

mvc.perform(formLogin()
    .user("alice").password("secret"))     // POST /login 시뮬레이션
    .andExpect(authenticated())
    .andExpect(redirectedUrl("/"))

authenticated() matcher 는 SecurityContext 가 정상 세팅됐는지 검증.

SecurityMockMvcResultMatchers

import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*;

mvc.perform(get("/admin"))
    .andExpect(unauthenticated());           // 401 / anonymous

mvc.perform(formLogin().user("alice").password("secret"))
    .andExpect(authenticated().withUsername("alice"));

mvc.perform(formLogin().user("wrong").password("wrong"))
    .andExpect(unauthenticated());

SecurityContext 직접 조작 (단위 테스트)

@WebMvcTest / MockMvc 가 아닌 단위 테스트:

@Test
void serviceMethod() {
    Authentication auth = new UsernamePasswordAuthenticationToken("alice", null,
        List.of(new SimpleGrantedAuthority("ROLE_ADMIN")));
    SecurityContextHolder.getContext().setAuthentication(auth);

    try {
        service.adminOnlyAction();   // @PreAuthorize("hasRole('ADMIN')")
    } finally {
        SecurityContextHolder.clearContext();
    }
}

@PreAuthorize 같은 method security 테스트에 필수. 매번 clearContext 호출 (또는 JUnit @AfterEach).

Method Security 통합 테스트

@PreAuthorize 가 실제 동작하는지:

@SpringBootTest
@AutoConfigureMockMvc
class MethodSecurityTest {
    @Autowired AdminService service;

    @Test
    @WithMockUser(roles = "USER")
    void user_cannot_delete() {
        assertThrows(AccessDeniedException.class, () -> service.deleteAll());
    }

    @Test
    @WithMockUser(roles = "ADMIN")
    void admin_can_delete() {
        assertDoesNotThrow(() -> service.deleteAll());
    }
}

@EnableMethodSecurity 가 활성화돼 있어야 SpEL 평가됨.

@WebMvcTest 와 Security 자동 설정

@WebMvcTest(controllers = UserController.class)
class UserControllerTest {
    @Autowired MockMvc mvc;
    @MockBean UserService userService;
}

@WebMvcTest 는 SecurityFilterChain 도 자동 로딩. 그래서 anonymous 요청은 401. @WithMockUser 없이 호출하려면:

@TestConfiguration
@EnableWebSecurity
class NoSecurityConfig {
    @Bean
    SecurityFilterChain permitAll(HttpSecurity http) throws Exception {
        return http.csrf(c -> c.disable())
            .authorizeHttpRequests(a -> a.anyRequest().permitAll())
            .build();
    }
}

@WebMvcTest(controllers = UserController.class)
@Import(NoSecurityConfig.class)
class UnsecureControllerTest { ... }

또는 @AutoConfigureMockMvc(addFilters = false) 로 filter 비활성화.

@SpringBootTest 통합 시나리오

@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureMockMvc
class FullStackSecurityTest {
    @Autowired MockMvc mvc;

    @Test
    void anonymous_to_protected_redirects_to_login() throws Exception {
        mvc.perform(get("/admin"))
            .andExpect(status().is3xxRedirection())
            .andExpect(redirectedUrlPattern("**/login"));
    }

    @Test
    void login_with_form() throws Exception {
        mvc.perform(formLogin().user("admin@example.com").password("admin123"))
            .andExpect(authenticated().withUsername("admin@example.com"));
    }
}

실제 SecurityFilterChain, UserDetailsService, PasswordEncoder 동작. CI 가 잡아낼 회귀 가장 많음.

함정과 베스트 프랙티스

  • with(csrf()) 잊지 말 것: POST / PUT / DELETE 모두 필요
  • @WithMockUserroles 는 자동 ROLE_ prefix: authorities 는 prefix 없이 raw
  • SecurityContextHolder 수동 조작 후 cleanup 필수: 누수 시 다른 테스트 오염
  • @WebMvcTest 는 Security 포함: 명시적으로 permit 하거나 mock 사용
  • JWT 테스트는 jwt(): 실제 token decode 시뮬레이션 필요 없음
  • OAuth2 callback 시나리오: MockMvc 로는 한계. WebClient + WireMock 통합 테스트 고려
  • password encoder slow: 테스트에서 NoOpPasswordEncoder 또는 BCrypt strength 4 사용
  • Testcontainers 가 추천: 실제 PostgreSQL / Redis 로 통합 (UserDetailsService + 세션)

관련 위키

이 글의 용어 (5개)
[Spring Security] OAuth2 Authorization Code Flow + PKCEspring
정의 가 OAuth2 Login (소셜 로그인) 의 개요를 다룬다면, 이 페이지는 표준 Authorization Code Grant 의 전체 흐름과 PKCE, refresh to…
[Spring Security] Session Management와 Remember-Mespring
정의 가 JWT / stateless 위주를 다룬다면, 이 페이지는 session-based 인증 의 세부 동작 (생성 정책, 동시 세션 제한, fixation 방어, remem…
[Spring] Security: Filter Chain, JWT, OAuth2spring
정의 Spring Security는 인증·인가의 사실상 표준. 서블릿 필터 체인 위에 다양한 인증/권한 처리를 모듈식으로. 6.0+ 부터 Lambda DSL 표준, Bean 방식…
[Spring] Spring MVC: @RestController, @RequestMappingspring
정의 Spring MVC는 Servlet 기반 동기 웹 프레임워크. 이 요청을 받아 핸들러(컨트롤러 메서드)에 라우팅. REST API와 전통 MVC(HTML) 모두 지원. We…
[Spring] Testing: @SpringBootTest, MockMvc, Testcontainersspring
정의 Spring Boot Test는 단위/슬라이스/통합 테스트 모두 지원. 가 JUnit 5, Mockito, AssertJ, Hamcrest, JsonPath 포함. 슬라이스…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기