JiwonDev

spring security ํ…Œ์ŠคํŠธ์šฉ ์„ค์ • ๋ฐฉ๋ฒ•

by JiwonDev

๊ณต์‹๋ฌธ์„œ ํ•œ๊ธ€๋ฒˆ์—ญ

 

Testing

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ๊ณต์‹ ๋ฌธ์„œ์— ์žˆ๋Š” “Testing” ์ฑ•ํ„ฐ๋ฅผ ํ•œ๊ธ€๋กœ ๋ฒˆ์—ญํ•œ ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค.

godekdls.github.io

# ๋‹จ์œ„ํ…Œ์ŠคํŠธ

@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๋“ฑ) ๊ณต์‹๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜์ž

๋ธ”๋กœ๊ทธ์˜ ์ •๋ณด

JiwonDev

JiwonDev

ํ™œ๋™ํ•˜๊ธฐ