복붙노트

[SPRING] Spring Test & Security : 인증 방법 모의?

SPRING

Spring Test & Security : 인증 방법 모의?

내 컨트롤러의 URL이 제대로 보호되어 있는지 테스트하는 방법을 알아 내려고했습니다. 누군가가 주변 환경을 변경하고 실수로 보안 설정을 제거하는 경우를 대비하여.

내 컨트롤러 메서드는 다음과 같습니다.

@RequestMapping("/api/v1/resource/test") 
@Secured("ROLE_USER")
public @ResonseBody String test() {
    return "test";
}

필자는 다음과 같이 WebTestEnvironment를 설정했습니다.

import javax.annotation.Resource;
import javax.naming.NamingException;
import javax.sql.DataSource;

import org.junit.Before;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({ 
        "file:src/main/webapp/WEB-INF/spring/security.xml",
        "file:src/main/webapp/WEB-INF/spring/applicationContext.xml",
        "file:src/main/webapp/WEB-INF/spring/servlet-context.xml" })
public class WebappTestEnvironment2 {

    @Resource
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    @Qualifier("databaseUserService")
    protected UserDetailsService userDetailsService;

    @Autowired
    private WebApplicationContext wac;

    @Autowired
    protected DataSource dataSource;

    protected MockMvc mockMvc;

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    protected UsernamePasswordAuthenticationToken getPrincipal(String username) {

        UserDetails user = this.userDetailsService.loadUserByUsername(username);

        UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(
                        user, 
                        user.getPassword(), 
                        user.getAuthorities());

        return authentication;
    }

    @Before
    public void setupMockMvc() throws NamingException {

        // setup mock MVC
        this.mockMvc = MockMvcBuilders
                .webAppContextSetup(this.wac)
                .addFilters(this.springSecurityFilterChain)
                .build();
    }
}

내 실제 테스트에서 나는 다음과 같은 것을 시도했다.

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;

import eu.ubicon.webapp.test.WebappTestEnvironment;

public class CopyOfClaimTest extends WebappTestEnvironment {

    @Test
    public void signedIn() throws Exception {

        UsernamePasswordAuthenticationToken principal = 
                this.getPrincipal("test1");

        SecurityContextHolder.getContext().setAuthentication(principal);        

        super.mockMvc
            .perform(
                    get("/api/v1/resource/test")
//                    .principal(principal)
                    .session(session))
            .andExpect(status().isOk());
    }

}

나는 이것을 여기에서 골랐다.

그러나이 기능을 면밀히 살펴보면 실제 요청을 URL로 보내지 않을 때만 도움이되지만 기능 수준에서 서비스를 테스트 할 때만 도움이됩니다. 내 경우에는 "액세스 거부"예외가 throw되었습니다.

org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:83) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:206) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:60) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) ~[spring-aop-3.2.1.RELEASE.jar:3.2.1.RELEASE]
        ...

다음 두 로그 메시지는 기본적으로 Principal 설정이 작동하지 않거나 덮어 쓰기되었음을 나타내는 사용자가 인증되지 않았 음을 의미합니다.

14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Secure object: ReflectiveMethodInvocation: public java.util.List test.TestController.test(); target is of class [test.TestController]; Attributes: [ROLE_USER]
14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS

해결법

  1. ==============================

    1.스프링 보안 필터 체인의 일부인 SecurityContextPersistenceFilter는 항상 SecurityContextHolder.getContext (). setAuthentication (principal) (또는 .principal (principal) 메서드 사용)을 설정하는 SecurityContext를 재설정합니다. 이 필터는 이전에 설정 한 SecurityContextRepository를 오버라이드 (override) 한 SecurityContext를 사용하여 SecurityContextHolder의 SecurityContext를 SecurityContext로 설정합니다. 저장소는 기본적으로 HttpSessionSecurityContextRepository입니다. HttpSessionSecurityContextRepository는 주어진 HttpRequest를 검사하고 해당 HttpSession에 액세스를 시도합니다. 존재하는 경우 HttpSession에서 SecurityContext를 읽으려고 시도합니다. 이것이 실패하면 저장소는 빈 SecurityContext를 생성합니다.

    스프링 보안 필터 체인의 일부인 SecurityContextPersistenceFilter는 항상 SecurityContextHolder.getContext (). setAuthentication (principal) (또는 .principal (principal) 메서드 사용)을 설정하는 SecurityContext를 재설정합니다. 이 필터는 이전에 설정 한 SecurityContextRepository를 오버라이드 (override) 한 SecurityContext를 사용하여 SecurityContextHolder의 SecurityContext를 SecurityContext로 설정합니다. 저장소는 기본적으로 HttpSessionSecurityContextRepository입니다. HttpSessionSecurityContextRepository는 주어진 HttpRequest를 검사하고 해당 HttpSession에 액세스를 시도합니다. 존재하는 경우 HttpSession에서 SecurityContext를 읽으려고 시도합니다. 이것이 실패하면 저장소는 빈 SecurityContext를 생성합니다.

    따라서, 내 솔루션은 SecurityContext를 보유하고있는 요청과 함께 HttpSession을 전달하는 것입니다.

    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    
    import org.junit.Test;
    import org.springframework.mock.web.MockHttpSession;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
    
    import eu.ubicon.webapp.test.WebappTestEnvironment;
    
    public class Test extends WebappTestEnvironment {
    
        public static class MockSecurityContext implements SecurityContext {
    
            private static final long serialVersionUID = -1386535243513362694L;
    
            private Authentication authentication;
    
            public MockSecurityContext(Authentication authentication) {
                this.authentication = authentication;
            }
    
            @Override
            public Authentication getAuthentication() {
                return this.authentication;
            }
    
            @Override
            public void setAuthentication(Authentication authentication) {
                this.authentication = authentication;
            }
        }
    
        @Test
        public void signedIn() throws Exception {
    
            UsernamePasswordAuthenticationToken principal = 
                    this.getPrincipal("test1");
    
            MockHttpSession session = new MockHttpSession();
            session.setAttribute(
                    HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, 
                    new MockSecurityContext(principal));
    
    
            super.mockMvc
                .perform(
                        get("/api/v1/resource/test")
                        .session(session))
                .andExpect(status().isOk());
        }
    }
    
  2. ==============================

    2.응답을위한 Seaching 나는 동시에 쉽고 유연한 프로그램을 찾지 못했고 Spring Security Reference를 찾았습니다. 완벽한 솔루션이 거의 존재 함을 깨달았습니다. AOP 솔루션은 종종 테스트를위한 가장 훌륭한 솔루션이며, Spring은이 아티팩트에서 @WithMockUser, @WithUserDetails 및 @WithSecurityContext를 제공한다.

    응답을위한 Seaching 나는 동시에 쉽고 유연한 프로그램을 찾지 못했고 Spring Security Reference를 찾았습니다. 완벽한 솔루션이 거의 존재 함을 깨달았습니다. AOP 솔루션은 종종 테스트를위한 가장 훌륭한 솔루션이며, Spring은이 아티팩트에서 @WithMockUser, @WithUserDetails 및 @WithSecurityContext를 제공한다.

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <version>4.2.2.RELEASE</version>
        <scope>test</scope>
    </dependency>
    

    대부분의 경우 @WithUserDetails는 내가 필요로하는 유연성과 힘을 모은다.

    기본적으로 테스트하려는 모든 가능한 사용자 프로필로 사용자 정의 UserDetailsService를 만들어야합니다. 예 :

    @TestConfiguration
    public class SpringSecurityWebAuxTestConfig {
    
        @Bean
        @Primary
        public UserDetailsService userDetailsService() {
            User basicUser = new UserImpl("Basic User", "user@company.com", "password");
            UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList(
                    new SimpleGrantedAuthority("ROLE_USER"),
                    new SimpleGrantedAuthority("PERM_FOO_READ")
            ));
    
            User managerUser = new UserImpl("Manager User", "manager@company.com", "password");
            UserActive managerActiveUser = new UserActive(managerUser, Arrays.asList(
                    new SimpleGrantedAuthority("ROLE_MANAGER"),
                    new SimpleGrantedAuthority("PERM_FOO_READ"),
                    new SimpleGrantedAuthority("PERM_FOO_WRITE"),
                    new SimpleGrantedAuthority("PERM_FOO_MANAGE")
            ));
    
            return new InMemoryUserDetailsManager(Arrays.asList(
                    basicActiveUser, managerActiveUser
            ));
        }
    }
    

    이제 사용자를 준비 시켰습니다. 따라서이 컨트롤러 기능에 대한 액세스 제어를 테스트하고 싶다고 상상해보십시오.

    @RestController
    @RequestMapping("/foo")
    public class FooController {
    
        @Secured("ROLE_MANAGER")
        @GetMapping("/salute")
        public String saluteYourManager(@AuthenticationPrincipal User activeUser)
        {
            return String.format("Hi %s. Foo salutes you!", activeUser.getUsername());
        }
    }
    

    여기서는 / foo / salute 라우트에 매핑 된 함수를 가져오고 @PreAuthorize 및 @PostAuthorize도 테스트 할 수 있지만 @Secured 주석으로 역할 기반 보안을 테스트하고 있습니다. 유효한 사용자가이 경례 응답을 볼 수 있는지 확인하고 다른 응답자가 실제로 금지되어 있는지 확인하는 두 가지 테스트를 작성해 보겠습니다.

    @RunWith(SpringRunner.class)
    @SpringBootTest(
            webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
            classes = SpringSecurityWebAuxTestConfig.class
    )
    @AutoConfigureMockMvc
    public class WebApplicationSecurityTest {
    
        @Autowired
        private MockMvc mockMvc;
    
        @Test
        @WithUserDetails("manager@company.com")
        public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
        {
            mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                    .accept(MediaType.ALL))
                    .andExpect(status().isOk())
                    .andExpect(content().string(containsString("manager@company.com")));
        }
    
        @Test
        @WithUserDetails("user@company.com")
        public void givenBasicUser_whenGetFooSalute_thenForbidden() throws Exception
        {
            mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                    .accept(MediaType.ALL))
                    .andExpect(status().isForbidden());
        }
    }
    

    보시다시피 SpringSecurityWebAuxTestConfig를 가져와 테스트를 위해 사용자를 제공합니다. 각 코드 케이스는 간단한 주석을 사용하여 코드와 복잡성을 줄임으로써 해당 테스트 케이스에서 사용되었습니다.

    @WithUserDetails는 대부분의 응용 프로그램에 필요한 모든 유연성을 제공합니다. 역할이나 권한과 같은 GrantedAuthority가있는 사용자 지정 사용자를 사용할 수 있습니다. 그러나 만약 당신이 단지 역할로 작업한다면, 테스트는 더욱 쉬울 수 있으며 커스텀 UserDetailsService를 생성하는 것을 피할 수 있습니다. 이 경우 @WithMockUser와 사용자, 비밀번호 및 역할의 간단한 조합을 지정하십시오.

    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    @WithSecurityContext(
        factory = WithMockUserSecurityContextFactory.class
    )
    public @interface WithMockUser {
        String value() default "user";
    
        String username() default "";
    
        String[] roles() default {"USER"};
    
        String password() default "password";
    }
    

    주석은 매우 기본적인 사용자의 기본값을 정의합니다. 우리의 경우와 같이 우리가 테스트하는 라우트는 인증 된 사용자를 관리자로 요구하며 SpringSecurityWebAuxTestConfig를 사용하여 종료 할 수 있습니다.

    @Test
    @WithMockUser(roles = "MANAGER")
    public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
    {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
                .accept(MediaType.ALL))
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("user")));
    }
    

    이제 manager@company.com이라는 사용자 대신 @WithMockUser : user가 제공하는 기본값을 얻습니다. 그러나 우리가 정말로 관심을 갖는 것이 그의 역할 ROLE_MANAGER이기 때문에 중요하지 않습니다.

    @WithUserDetails 및 @WithMockUser와 같은 특수 효과를 통해 알 수 있듯이 간단한 테스트 만하기 위해 아키텍처에서 소외된 클래스를 만들지 않고도 인증 된 다른 사용자 시나리오로 전환 할 수 있습니다. 또한 @WithSecurityContext가 어떻게 더 유연하게 작동하는지 보도록 권장했습니다.

  3. ==============================

    3.pom.xml에 추가 :

    pom.xml에 추가 :

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <version>4.0.0.RC2</version>
        </dependency>
    

    Authorization 요청을 위해 org.springframework.security.test.web.servlet.request.SecurityMockCreationPostProcessors를 사용한다. 샘플 사용법은 https://github.com/rwinch/spring-security-test-blog에서 확인하십시오. (https://jira.spring.io/browse/SEC-2592).

    최신 정보:

    4.0.0.RC2는 스프링 보안 3.x에서 작동합니다. 스프링 보안의 경우 4 스프링 보안 테스트는 스프링 보안의 일부가됩니다 (http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test, 버전은 동일 함). ).

    설정이 변경되었습니다. http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc

    public void setup() {
        mvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity())  
                .build();
    }
    

    기본 인증 샘플 : http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication.

  4. ==============================

    4.Spring 4.0+ 이후, 가장 좋은 해결책은 @WithMockUser로 테스트 메소드에 주석을 달아주는 것이다.

    Spring 4.0+ 이후, 가장 좋은 해결책은 @WithMockUser로 테스트 메소드에 주석을 달아주는 것이다.

    @Test
    @WithMockUser(username = "user1", password = "pwd", roles = "USER")
    public void mytest1() throws Exception {
        mockMvc.perform(get("/someApi"))
            .andExpect(status().isOk());
    }
    

    프로젝트에 다음 종속성을 추가하는 것을 잊지 마십시오.

    'org.springframework.security:spring-security-test:4.2.3.RELEASE'
    
  5. ==============================

    5.다음은 Base64 기본 인증을 사용하여 Spring MockMvc Security Config를 테스트하려는 사람들을위한 예제이다.

    다음은 Base64 기본 인증을 사용하여 Spring MockMvc Security Config를 테스트하려는 사람들을위한 예제이다.

    String basicDigestHeaderValue = "Basic " + new String(Base64.encodeBase64(("<username>:<password>").getBytes()));
    this.mockMvc.perform(get("</get/url>").header("Authorization", basicDigestHeaderValue).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());
    

    Maven 종속성

        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.3</version>
        </dependency>
    
  6. ==============================

    6.짧은 답변:

    짧은 답변:

    @Autowired
    private WebApplicationContext webApplicationContext;
    
    @Autowired
    private Filter springSecurityFilterChain;
    
    @Before
    public void setUp() throws Exception {
        final MockHttpServletRequestBuilder defaultRequestBuilder = get("/dummy-path");
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
                .defaultRequest(defaultRequestBuilder)
                .alwaysDo(result -> setSessionBackOnRequestBuilder(defaultRequestBuilder, result.getRequest()))
                .apply(springSecurity(springSecurityFilterChain))
                .build();
    }
    
    private MockHttpServletRequest setSessionBackOnRequestBuilder(final MockHttpServletRequestBuilder requestBuilder,
                                                                 final MockHttpServletRequest request) {
        requestBuilder.session((MockHttpSession) request.getSession());
        return request;
    }
    

    봄 보안 테스트에서 formLogin을 수행 한 후 각 요청은 자동으로 로그인 한 사용자로 호출됩니다.

    긴 대답 :

    이 해결책을 확인하십시오. (답은 봄 4입니다.) 봄 3.2 사용자에 로그인하는 방법

  7. ==============================

    7.테스트에서 SecurityContextHolder 사용을 피하기위한 옵션 :

    테스트에서 SecurityContextHolder 사용을 피하기위한 옵션 :

  8. from https://stackoverflow.com/questions/15203485/spring-test-security-how-to-mock-authentication by cc-by-sa and MIT license