[spring] 스프링 테스트 및 보안 : 인증을 모의하는 방법?

내 컨트롤러의 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로 보내지 않고 기능 수준에서 서비스를 테스트 할 때만 도움이됩니다. 제 경우에는 “액세스 거부”예외가 발생했습니다.

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



답변

답을 구하기 위해 동시에 쉽고 유연하다는 것을 찾을 수 없었고 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필요한 유연성과 힘을 모 읍니다.

@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 경로에 대한 매핑 된 기능 이 있으며 주석을 사용하여 역할 기반 보안을 테스트 하고 있습니다. 두 개의 테스트를 만들어 보겠습니다. 하나는 유효한 사용자가이 경례 응답을 볼 수 있는지 확인하고 다른 하나는 실제로 금지되었는지 확인합니다.@Secured@PreAuthorize@PostAuthorize

@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테스트를 위해 사용자에게 제공하기 위해 가져 왔습니다 . 각각은 간단한 주석을 사용하여 해당 테스트 케이스에 사용되어 코드와 복잡성을 줄였습니다.

더 간단한 역할 기반 보안을 위해 @WithMockUser를 더 잘 사용하십시오.

보시다시피 @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 가 어떻게 작동 하는지 확인하는 것이 좋습니다 .


답변

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'


답변

그것은 밝혀졌다 SecurityContextPersistenceFilter봄 보안 필터 체인의 일부인이, 항상 내 재설정 SecurityContext, 내가 전화 설정 SecurityContextHolder.getContext().setAuthentication(principal)(또는 사용 .principal(principal)방법). 이 필터는 설정 SecurityContext에서 SecurityContextHolderA를을 SecurityContextA로부터 SecurityContextRepository 덮어 쓰기 앞서 설정 한합니다. 저장소는 HttpSessionSecurityContextRepository기본적으로입니다. HttpSessionSecurityContextRepository검사는 주어진 HttpRequest및 액세스 시도는 해당 HttpSession. 존재하는 경우,을 읽으려고합니다 SecurityContext으로부터 HttpSession. 이것이 실패하면 저장소는 빈 SecurityContext.

따라서 내 해결책은 HttpSession요청과 함께 전달하는 것입니다 SecurityContext.

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());
    }
}


답변

pom.xml에 추가하십시오.

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

org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors승인 요청에 사용 합니다. https://github.com/rwinch/spring-security-test-blog
( https://jira.spring.io/browse/SEC-2592 ) 에서 샘플 사용을 참조 하십시오 .

최신 정보:

4.0.0.RC2는 스프링 보안 3.x에서 작동합니다. spring-security 4 spring-security-test는 spring-security의 일부가됩니다 ( 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 .


답변

다음은 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>


답변

짧은 답변:

@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스프링 보안 테스트를 수행 한 후 각 요청은 로그인 한 사용자로 자동 호출됩니다.

긴 대답 :

이 솔루션을 확인하십시오 (답은 spring 4입니다) : Spring 3.2 new mvc testing으로 사용자를 로그인하는 방법


답변

테스트에서 SecurityContextHolder 사용을 피하는 옵션 :

  • 옵션 1 : mock SecurityContextHolder사용-일부 mock 라이브러리를 사용하여 mock 의미 – 예를 들어 EasyMock
  • 옵션 2 : SecurityContextHolder.get...일부 서비스의 코드에서 호출 을 래핑 합니다. 예를 들어 인터페이스 를 구현 SecurityServiceImpl하는 메서드 getCurrentPrincipal를 사용한 SecurityService다음 테스트에서 액세스없이 원하는 주체를 반환하는이 인터페이스의 모의 구현을 만들 수 있습니다 SecurityContextHolder.