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


124

내 컨트롤러의 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

회사 이름 인 eu.ubicon이 가져 오기에 표시됩니다. 보안 상 위험하지 않습니까?
Kyle Bridenstine

2
안녕하세요, 댓글 주셔서 감사합니다! 그래도 이유를 알 수 없습니다. 어쨌든 오픈 소스 소프트웨어입니다. 관심이 있으시면 bitbucket.org/ubicon/ubicon (또는 최신 포크에 대한 bitbucket.org/dmir_wue/everyaware) 을 참조하십시오 . 내가 뭔가를 놓친 경우 알려주세요.
Martin Becker

이 솔루션을 확인하십시오 (답은 봄 4) : stackoverflow.com/questions/14308341/…
Nagy Attila

답변:


101

답을 구하기 위해 동시에 쉽고 유연하다는 것을 찾을 수 없었고 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 가 어떻게 작동 하는지 확인하는 것이 좋습니다 .


여러 사용자 를 조롱하는 방법 ? 예를 들어 첫 번째 요청은에서 보내고 tom두 번째 요청은 jerry?
ch271828n

테스트가 tom과 함께있는 함수를 만들고 동일한 논리로 다른 테스트를 만들고 Jerry로 테스트 할 수 있습니다. 각 테스트에 대한 특정 결과가 있으므로 다른 어설 션이 있으며 테스트가 실패하면 어떤 사용자 / 역할이 작동하지 않았는지 이름으로 알려줍니다. 요청에서 사용자는 하나만 될 수 있으므로 요청에 여러 사용자를 지정하는 것은 의미가 없습니다.
EliuX

죄송합니다. 예제 시나리오를 의미합니다. 우리는 tom이 비밀 기사를 만든 다음 jerry가 그것을 읽으려고 시도하고 jerry는 그것을 보지 말아야합니다 (비밀이기 때문에). 따라서이 경우에는 하나의 단위 테스트입니다 ...
ch271828n

답변에 제공된 BasicUserManager User시나리오 와 매우 유사합니다 . 핵심 개념은 사용자를 돌보는 대신 실제로 사용자의 역할에 관심이 있지만 동일한 단위 테스트에있는 각 테스트는 실제로 다른 쿼리를 나타냅니다. 다른 사용자 (다른 역할)가 동일한 엔드 포인트에 대해 수행합니다.
EliuX

61

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'

1
봄은 놀랍습니다. 감사합니다
TuGordoBello 2010 년

좋은 대답입니다. 또한-mockMvc를 사용할 필요는 없지만 springframework.data에서 예를 들어 PagingAndSortingRepository를 사용하는 경우 저장소에서 직접 메소드를 호출 할 수 있습니다 (EL @PreAuthorize (......)로 주석 처리됨).
supertramp

50

그것은 밝혀졌다 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());
    }
}

2
아직 Spring Security에 대한 공식 지원을 추가하지 않았습니다. jira.springsource.org/browse/SEC-2015를 참조하십시오. 어떻게 보일지에 대한 개요는 github.com/SpringSource/spring-test-mvc/blob/master/src/test/…에
Rob Winch

인증 개체를 만들고 해당 속성으로 세션을 추가하는 것이 그렇게 나쁘다고 생각하지 않습니다. 이것이 유효한 "해결책"이라고 생각하십니까? 반면에 직접적인 지원은 물론 좋을 것입니다. 꽤 깔끔해 보입니다. 링크 주셔서 감사합니다!
Martin Becker

훌륭한 솔루션. 나를 위해 일했다! getPrincipal()내 의견으로는 약간 오해의 소지가있는 보호 된 방법의 이름 지정에 대한 사소한 문제입니다 . 이상적으로는 이름이 지정되어야합니다 getAuthentication(). 마찬가지로 signedIn()테스트에서 지역 변수 이름을 지정 auth하거나 authentication대신principal
Tanvir

"해, getPrincipal 무엇입니까 ("TEST1 ") ¿ ?? 그것을 어디 있는지 expline 수 있다는 것을 미리 감사드립니다?
user2992476

@ user2992476 아마도 UsernamePasswordAuthenticationToken 유형의 개체를 반환합니다. 또는 GrantedAuthority를 ​​만들고이 개체를 생성합니다.
bluelurker

31

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 .


이것은 또한 로그인 보안 필터를 통해 로그인하려고 할 때 404가 발생하는 문제를 해결했습니다. 감사!
Ian Newland

안녕하세요, GKislin에서 언급 한대로 테스트하는 동안. "인증 실패 UserDetailsService가 null을 반환했습니다. 이는 인터페이스 계약 위반입니다."라는 오류가 발생합니다. 어떤 제안이라도 부탁드립니다. 최종 AuthenticationRequest auth = new AuthenticationRequest (); auth.setUsername (userId); auth.setPassword (암호); mockMvc.perform (post ( "/ api / auth /"). content (json (auth)). contentType (MediaType.APPLICATION_JSON));
Sanjeev

7

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

3

짧은 답변:

@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으로 사용자를 로그인하는 방법


2

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

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

Mh, 아마도 전체 그림을 얻지 못할 수도 있습니다. 내 문제는 SecurityContextPersistenceFilter가 HttpSessionSecurityContextRepository의 SecurityContext를 사용하여 SecurityContext를 대체하고, 차례로 해당 HttpSession에서 SecurityContext를 읽는다는 것입니다. 따라서 세션을 사용하는 솔루션입니다. SecurityContextHolder에 대한 호출과 관련하여 : 더 이상 SecurityContextHolder에 대한 호출을 사용하지 않도록 내 대답을 편집했습니다. 그러나 래핑이나 추가 모의 라이브러리를 도입하지 않아도됩니다. 이것이 더 나은 해결책이라고 생각하십니까?
Martin Becker

죄송합니다. 정확히 무엇을 찾고 있는지 이해하지 못했고 귀하가 제시 한 솔루션보다 더 나은 답변을 제공 할 수 없습니다. 좋은 옵션 인 것 같습니다.
Pavla Nováková 2013 년

좋아, 고마워. 나는 지금 내 제안을 해결책으로 받아 들일 것입니다.
Martin Becker
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.