spring security ํ ์คํธ์ฉ ์ค์ ๋ฐฉ๋ฒ
by JiwonDev๊ณต์๋ฌธ์ ํ๊ธ๋ฒ์ญ
# ๋จ์ํ ์คํธ
@WebMvcTest์ ๊ฒฝ์ฐ ํ ์คํธ์ Security ๋ ์ ์ ์ฉ๋๋ฏ๋ก Spring-Security-Test ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์๋ ๊ธฐ๋ฅ์ ์ ์ ํ๊ฒ ์ฌ์ฉํ๋ฉด ๋๋ค.
์์ฝํ์๋ฉด Web๊ณผ ๊ด๋ จ๋ ๋น๋ค (ํํฐ, ์ธํฐ์ ํฐ, ์๊ท๋จผํธ๋ฆฌ์กธ๋ฒ)๋ง ๋ฑ๋กํ๋ค.
@ControllerAdvice, @JsonComponent, Converter, Filter, WebMvcConfigurer, HandlerMethodArgumentResolver
ํ์ง๋ง ์น๊ณผ ๊ด๋ จ์๋ ๋น๋ค(@Component, @Service, @Repository...)๋ ๋ฑ๋กํ์ง ์๋๋ค.
import static org.springframework.security.test.
web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
@Test
@DisplayName("์
๋ ฅ๊ฐ์ ์ค๋ฅ๊ฐ ์๋ค๋ฉด ํ์๊ฐ์
์ ์คํจํ๊ณ ๊ฐ์
ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ์
ํ๋ค.")
void signup_wrong_input() throws Exception {
// Act
var actions = mockMvc.perform(post("/sign-up")
.param("nickname", "jiwon")
.param("email", "email...")
.param("password", "12345")
.with(csrf())); // security.test ์ CSRF ์ค์
// Assert
actions.andDo(print())
.andExpect(status().isOk())
.andExpect(view().name("account/sign-up"));
}
๐งจ ๋ง์ฝ SecurityConfiguration์ ๋ค๋ฅธ ๋น์ ์์กด์ฑ์ด ์๋ค๋ฉด, @WebMvcTest๋ก ๋์ธ ์ ๋น์ ์ฐพ์ ์ ์๋ค๋ ์์ธ๊ฐ ๋ฐ์ํ๋ค. ์ด ๊ฒฝ์ฐ ์๋์ ๊ฐ์ด ์๋์ผ๋ก ์ ์ธ์์ผ์ฃผ์. (์ปจํธ๋กค๋ฌ ๊ธฐ๋ฅ์ ํ ์คํธํ๊ธฐ ์ํจ์ด๋๊น)
@WebMvcTest(
controllers = AccountController.class,
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class)}
)
final class AccountControllerTest {
}
๋ง์ฝ, ์์ Controller ํ ์คํธ์ SpringSecurity ์ค์ ์ ์ ์ธํ๊ณ ์ถ๋ค๋ฉด ์๋์ ๊ฐ์ด ์ฌ์ฉํ๋ฉด ๋๋ค.
@WebMvcTest(
controllers = AccountController.class,
excludeAutoConfiguration = SecurityAutoConfiguration.class, // ์ถ๊ฐ
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class)}
)
๋๋ ํ ์คํธ์ฉ ์์ธ ์ค์ ์ ๋ฐ๋ก ์ถ๊ฐํ๋ ๋ฐฉ๋ฒ๋ ์๋ค.
@WebMvcTest(AccountController.class)
final class AccountControllerTest {
@TestConfiguration
static class DefaultConfigWithoutCsrf extends WebSecurityConfigurerAdapter {
@Override
protected void configure(final HttpSecurity http) throws Exception {
super.configure(http);
http.csrf().disable();
}
public void configure(WebSecurity web) throws Exception {
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
web.ignoring().antMatchers("/api/auth/**"); // ํ
์คํธ์ฉ API์ Security ๋นํ์ฑํ
}
}
@Test
void test() {...}
}
@WithMockUser
๋ค๋ฅธ ๋ฐฉ๋ฒ์ผ๋ก๋ MockMvc์ ๊ฐ์ง ์ ์ ๋ฅผ ๋ฑ๋กํ๋ ๋ฐฉ๋ฒ๋ ์๋ค. Spring Security 4.1 ๋ถํฐ ํ ์คํธ์ ์ง์ํ๋ ๊ธฐ๋ฅ์ด๋ค.
์ฐธ๊ณ ๋ก (2022.03)๊ธฐ์ค ์ต์ ๋ฒ์ ์ Spring Seucirty 5.3์ด๋ค
testImplementation 'org.springframework.security:spring-security-test' // ์ฌ๊ธฐ์ ์๋ ๊ธฐ๋ฅ
@WithAnonymousUser // ์ต๋ช
์ ์ ์ ์ธ์ฆ์ ๋ณด๋ฅผ ์ค์ ํ๊ธฐ ์ํ ์ด๋
ธํ
์ด์
@WithUserDetails // UserDetailsService๋ฅผ ํตํด์ ์ ์ ์ ๋ณด๋ฅผ ์ทจ๋ํ์ฌ ์ค์ ํ๊ธฐ ์ํ ์ด๋
ธํ
์ด์
@WithMockUser // ๋ณ๋์ UserDetailsService์ ๊ฐ์ ์คํ
์ ์ ๊ณตํ์ง ์์๋ ๊ฐ๋จํ๊ฒ ์ธ์ฆ์ ๋ณด๋ฅผ ์ค์ ํ๊ธฐ ์ํ ์ด๋
ธํ
์ด์
์ฌ์ฉ๋ฒ์ mockMvc์์ .with(user) ๋ก ๋ฑ๋กํ๊ฑฐ๋, @WithMockUser, @WithAnonymousUser๋ก ์ถ๊ฐํ๋ค.
@Test
public void index_anonymous() throws Exception {
mockMvc.perform(get("/").with(anonymous()))
.andDo(print())
.andExpect(status().isOk());
}
@WithAnonymousUser
public void index_anonymous() throws Exception {
mockMvc.perform(get("/"))
.andDo(print())
.andExpect(status().isOk());
}
@Test
public void index_user() throws Exception {
mockMvc.perform(get("/")
.with(user("jake").roles("USER")))
.andDo(print())
.andExpect(status().isOk());
}
@Test
public void admin_user() throws Exception {
mockMvc.perform(get("/admin")
.with(user("jake").roles("USER")))
.andDo(print())
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(username = "jake", roles = "USER")
public void index_user() throws Exception {
mockMvc.perform(get("/"))
.andDo(print())
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "jake", roles = "USER")
public void admin_user() throws Exception {
mockMvc.perform(get("/admin"))
.andDo(print())
.andExpect(status().isForbidden());
}
๋งค๋ฒ ๋๊ฐ์ ์ ์ ์ ๋ณด๋ฅผ ์ ๋ ฅํ๋๊ฒ ๋ฒ๊ฑฐ๋กญ๋ค๋ฉด, ํ ์คํธ์ฉ ์ด๋ ธํ ์ด์ ์ ์๋์ ๊ฐ์ด ๋ง๋ค์ด์ ์ฌ์ฉํด๋ ๋๋ค.
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(username = "jake", roles = "USER")
public @interface WithUser {
}
@Test
@WithUser
public void index_user() throws Exception {
mockMvc.perform(get("/"))
.andDo(print())
.andExpect(status().isOk());
}
์ ์ฌ์ฉ์ ์ํ์ง๋ง, @WithUserDeatils ๋ ์๋์ ๊ฐ์ด ํ ์คํธ์ฉ ๋น์ ๋ณ๋๋ก ์ถ๊ฐํด์ฃผ์ด์ผํ๋ค.
@Bean
@Profile("test")
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
return User.withUsername(username).password("password")
.authorities(new SimpleGrantedAuthority("ROLE_USER")).build();
}
};
}
ํด๋น UserDeatilsService๋ฅผ ์ ์ฉ์ํค๊ธฐ ์ํด์๋, ์๋์ ๊ฐ์ด @WithUserDetails(์ ์ ๋ช )๋ฅผ ์ ์ด์ฃผ๋ฉด ๋๋ค.
@Test
@WithUserDetails("test_user") // "test_user"๋ก ์ ๊ทผ์ ์์์ ์ค์ ํ UserDetails๊ฐ ๋ฐํ๋จ.
public void some_test() { ... }
ํ ์คํธ์ฉ ๋น ์ด๋ฆ(userDetailsService)์ ๋ค๋ฅด๊ฒ ๋ฑ๋กํ๊ณ ์ถ๋ค๋ฉด, ์ง์ ์ง์ ํ๋ฉด ๋๋ค.
@Test
@WithUserDetails(value="user", userDeatilsServiceBeanName="myUserDetailsServiceBean")
# ํตํฉํ ์คํธ (SpringBootTest)
์คํ๋ง๋น์ ๋์ฐ๋ ํตํฉํ ์คํธ๋ผ๋ฉด ๊ฐ ํ ์คํธ๊ฐ ๋ ๋ฆฝ์ ์ด๊ฒ @Transcational์ ๋ถ์ฌ์ฃผ์.
๋ค๋ง ์ค์ ์๋น์ค์์๋ @Transcational์ ๋ถ์ด๊ฒ๋๋ฉด ํ ์คํธ-๋น๋ ๊ณผ์ ์ด ๋๋ฌด ๋๋ ค์ ธ์ ๋ถ๋ด์ด ๋๋ ์ ์ ํ๊ฒ ์ฌ์ฉํ๋๋ก ํ์. (์ง์ ํ ์คํธ ๋ฐ์ดํฐ๋ฅผ ๋ฃ์ด์ ๊ฒ์ฆ, ํ ์คํธ์ Slow, Fast ํ๊ทธ๋ฅผ ๊ตฌ๋ถํด์ ํตํฉํ ์คํธ ๋น๋์ ์ค์ด๊ธฐ ๋ฑ)
@Autowired
AccountService accountService;
@Test
@Transactional
public void login_success() throws Exception{
String username = "grace";
String password = "123";
Account account = this.createUser(username, password);
// ์ฐธ๊ณ ๋ก ๋ณดํต account.getPassword() ๋ก ๋ฐ์ผ๋ฉด, ์ํธํ๋ ๋น๋ฐ๋ฒํธ์์ ์ฃผ์ํ์.
mockMvc.perform(formLogin().user(user.getUsername()).password(password))
.andExpect(authenticated());
}
@Test
@Transactional
public void login_fail() throws Exception{
String username = "grace";
String password = "123";
Account user = this.createUser(username, password);
mockMvc.perform(formLogin().user(user.getUsername()).password("1234"))
.andExpect(unauthenticated());
}
private Account createUser(String username, String password) {
Account account = new Account();
account.setUsername(username);
account.setPassword(password);
account.setRole("USER");
return accountService.createNew(account);
}
๋ง์ฝ UserDetails๋ฅผ ์ฌ์ฉํ์ง์๊ณ , ์ปค์คํ ํ๋ค๋ฉด (JWT๋ฑ) ๊ณต์๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์
'๐ฑ Spring Framework > Test (JUnit, SpringBootTest)' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
ํ ์คํธ ๋ฐ์ดํฐ๋๊ตฌ - Fixture Monkey (3) | 2022.04.02 |
---|---|
์ข์ ํ ์คํธ๋ฅผ ์์ฑํ๋ ๋ฐฉ๋ฒ (5) | 2022.03.08 |
Spring Boot Test์ ์ฌ๋ผ์ด์ค ํ ์คํธ (0) | 2021.09.08 |
์๋ฐ ํ ์คํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ - JUnit5, AssertJ (0) | 2021.09.07 |
๋ค์ํ ํ ์คํธ์ Test Double (+Mockist) (0) | 2021.09.07 |
๋ธ๋ก๊ทธ์ ์ ๋ณด
JiwonDev
JiwonDev