Spring Security를 ​​사용한 단위 테스트


140

우리 회사는 Spring MVC를 평가하여 다음 프로젝트 중 하나에서 사용해야하는지 결정했습니다. 지금까지 내가 본 것을 좋아하고 지금 당장 Spring Security 모듈을 살펴보고 사용할 수 있는지 여부를 결정하고 있습니다.

보안 요구 사항은 매우 기본적입니다. 사용자는 사이트의 특정 부분에 액세스 할 수 있도록 사용자 이름과 비밀번호를 제공하면됩니다 (예 : 계정 정보를 얻는 등). 익명의 사용자에게 액세스 권한을 부여해야하는 사이트 (FAQ, 지원 등)가 있습니다.

내가 만든 프로토 타입에서 인증 된 사용자를 위해 "LoginCredentials"개체 (사용자 이름과 암호 만 포함)를 세션에 저장했습니다. 예를 들어, 일부 컨트롤러는 로그인 한 사용자 이름에 대한 참조를 얻기 위해이 개체가 세션에 있는지 확인합니다. 대신이 자체 제작 논리를 Spring Security로 바꾸려고합니다. "로그인 한 사용자를 어떻게 추적합니까?"를 제거하면 큰 이점이 있습니다. "사용자를 어떻게 인증합니까?" 내 컨트롤러 / 비즈니스 코드에서.

Spring Security는 (스레드 당) "컨텍스트"객체를 제공하여 앱의 어느 곳에서나 사용자 이름 / 주요 정보에 액세스 할 수있는 것처럼 보입니다 ...

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

...이 객체는 어떤 방식으로 (전역) 싱글 톤이므로 매우 봄이 아닌 것처럼 보입니다.

내 질문은 이것입니다 : 이것이 Spring Security에서 인증 된 사용자에 대한 정보에 액세스하는 표준 방법이라면, 단위 테스트가 필요할 때 단위 테스트에 사용할 수 있도록 AuthenticationContext를 SecurityContext에 주입하는 허용되는 방법은 무엇입니까? 인증 된 사용자?

각 테스트 케이스의 초기화 방법에 이것을 연결해야합니까?

protected void setUp() throws Exception {
    ...
    SecurityContextHolder.getContext().setAuthentication(
        new UsernamePasswordAuthenticationToken(testUser.getLogin(), testUser.getPassword()));
    ...
}

지나치게 장황한 것 같습니다. 더 쉬운 방법이 있습니까?

SecurityContextHolder객체 자체는 매우 취소 봄 같은 것 ...

답변:


48

문제는 스프링 시큐리티가 인증 객체를 컨테이너에서 빈으로 사용 가능하게하지 않기 때문에 상자에서 쉽게 삽입하거나 자동 와이어 링 할 수있는 방법이 없다는 것입니다.

Spring Security를 ​​사용하기 전에 컨테이너에 세션 범위 Bean을 작성하여 프린시 펄을 저장하고이를 "AuthenticationService"(싱글 톤)에 주입 한 다음이 Bean을 현재 프린시 펄에 대한 지식이 필요한 다른 서비스에 주입합니다.

자체 인증 서비스를 구현하는 경우 기본적으로 동일한 작업을 수행 할 수 있습니다. "principal"특성을 사용하여 세션 범위 Bean을 작성하고이를 인증 서비스에 삽입 한 후 인증 서비스가 해당 특성을 성공적인 인증에 설정하도록하십시오. 필요에 따라 다른 Bean이 인증 서비스를 사용할 수 있도록하십시오.

SecurityContextHolder 사용에 대해 나쁘지 않을 것입니다. 그러나. 나는 정적 / 싱글 톤이며 Spring은 그러한 것들을 사용하지 않는 것을 알고 있지만 구현은 환경에 따라 적절하게 작동하도록주의를 기울입니다. 서블릿 컨테이너의 세션 범위, JUnit 테스트의 스레드 범위 등 실제 제한 요소 Singleton은 다른 환경에 융통성이없는 구현을 제공 할 때입니다.


감사합니다. 이것은 유용한 조언입니다. 지금까지 한 일은 기본적으로 SecurityContextHolder.getContext () 호출을 진행하는 것입니다 (내 자신의 몇 가지 래퍼 메서드를 통해 적어도 하나의 클래스에서만 호출됩니다).
matt b

2
한 가지 참고 사항-ServletContextHolder에는 HttpSession 개념이나 웹 서버 환경에서 작동하는지 알 수있는 방법이 없다고 생각합니다. 다른 것을 사용하도록 구성하지 않으면 ThreadLocal을 사용합니다 (다른 두 가지 내장 모드는 InheritableThreadLocal and Global)
matt b

Spring에서 세션 / 요청 범위 Bean을 사용하는 유일한 단점은 JUnit 테스트에서 실패한다는 것입니다. 가능한 경우 세션 / 요청을 사용하고 스레드로 폴백해야하는 사용자 지정 범위를 구현하는 것입니다. 내 생각 엔 ... 봄 보안 비슷한 일을하고 있다는 것입니다
cliff.meyers

내 목표는 세션없이 휴식 API를 구축하는 것입니다. 아마도 갱신 가능한 토큰 일 것입니다. 이것이 내 질문에 대답하지는 않았지만 도움이되었습니다. 감사합니다
Pomagranite

166

일반적인 방법으로 SecurityContextHolder.setContext()테스트 클래스에 다음과 같이 삽입하십시오 .

제어 장치:

Authentication a = SecurityContextHolder.getContext().getAuthentication();

테스트:

Authentication authentication = Mockito.mock(Authentication.class);
// Mockito.whens() for your authorization object
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
Mockito.when(securityContext.getAuthentication()).thenReturn(authentication);
SecurityContextHolder.setContext(securityContext);

2
@ 레오나르도 Authentication a컨트롤러에서 어디에 추가 해야 합니까? 각 메소드 호출에서 이해할 수 있듯이? 주사 대신에 "봄의 길"을 추가해도 괜찮습니까?
Oleg Kuts

그러나 SecurityContextHolder가 로컬 스레드 변수를 보유하므로 테스트
간에이

그것을 수행 @BeforeEach(JUnit5) 또는 @Before(JUnit 4에서는). 좋고 간단합니다.
WesternGun

30

인증 객체를 생성하고 주입하는 방법에 대한 질문에 답하지 않고 Spring Security 4.0은 테스트와 관련하여 몇 가지 환영하는 대안을 제공합니다. @WithMockUser개발자 는 주석을 사용하여 모의 사용자 (선택적 권한, 사용자 이름, 비밀번호 및 역할 포함)를 깔끔하게 지정할 수 있습니다.

@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
    String message = messageService.getMessage();
    ...
}

사용하는 옵션도 있습니다 @WithUserDetailsA는 에뮬레이션 UserDetails으로부터 반환 UserDetailsService, 예를 들면

@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
    String message = messageService.getMessage();
    ...
}

자세한 내용은 Spring Security 참조 문서 의 @WithMockUser@WithUserDetails 장에서 찾을 수 있습니다 (위의 예제가 복사 됨)


29

정적 메서드 호출은 종속성을 쉽게 조롱 할 수 없으므로 단위 테스트에 특히 문제가됩니다. 내가 당신에게 보여줄 것은 Spring IoC 컨테이너가 당신을 위해 더티 작업을 수행하도록 깔끔하게 테스트 가능한 코드를 남기는 방법입니다. SecurityContextHolder는 프레임 워크 클래스이며, 저수준 보안 코드를 연결하는 것이 좋을 수도 있지만 UI 구성 요소 (예 : 컨트롤러)에 더 깔끔한 인터페이스를 노출하고 싶을 것입니다.

cliff.meyers는 한 가지 방법으로 언급했습니다. 자신 만의 "기본"유형을 작성하고 인스턴스를 소비자에게 주입하십시오. 2.x에서 도입 된 Spring < aop : scoped-proxy /> 태그는 요청 범위 Bean 정의와 결합되었으며 팩토리 메소드 지원은 가장 읽기 쉬운 코드에 대한 티켓입니다.

다음과 같이 작동 할 수 있습니다.

public class MyUserDetails implements UserDetails {
    // this is your custom UserDetails implementation to serve as a principal
    // implement the Spring methods and add your own methods as appropriate
}

public class MyUserHolder {
    public static MyUserDetails getUserDetails() {
        Authentication a = SecurityContextHolder.getContext().getAuthentication();
        if (a == null) {
            return null;
        } else {
            return (MyUserDetails) a.getPrincipal();
        }
    }
}

public class MyUserAwareController {        
    MyUserDetails currentUser;

    public void setCurrentUser(MyUserDetails currentUser) { 
        this.currentUser = currentUser;
    }

    // controller code
}

지금까지 복잡한 것이 없습니까? 실제로 당신은 아마이 대부분을 이미해야했습니다. 다음으로 Bean 컨텍스트에서 프린시 펄을 보유 할 요청 범위 Bean을 정의하십시오.

<bean id="userDetails" class="MyUserHolder" factory-method="getUserDetails" scope="request">
    <aop:scoped-proxy/>
</bean>

<bean id="controller" class="MyUserAwareController">
    <property name="currentUser" ref="userDetails"/>
    <!-- other props -->
</bean>

aop : scoped-proxy 태그의 마술 덕분에 새 HTTP 요청이 올 때마다 정적 메소드 getUserDetails가 호출되고 currentUser 특성에 대한 참조가 올바르게 분석됩니다. 이제 단위 테스트가 간단 해집니다.

protected void setUp() {
    // existing init code

    MyUserDetails user = new MyUserDetails();
    // set up user as you wish
    controller.setCurrentUser(user);
}

도움이 되었기를 바랍니다!


9

개인적으로 나는 Mockito 또는 Easymock과 함께 Powermock을 사용하여 단위 / 통합 테스트에서 정적 SecurityContextHolder.getSecurityContext ()를 조롱합니다.

@RunWith(PowerMockRunner.class)
@PrepareForTest(SecurityContextHolder.class)
public class YourTestCase {

    @Mock SecurityContext mockSecurityContext;

    @Test
    public void testMethodThatCallsStaticMethod() {
        // Set mock behaviour/expectations on the mockSecurityContext
        when(mockSecurityContext.getAuthentication()).thenReturn(...)
        ...
        // Tell mockito to use Powermock to mock the SecurityContextHolder
        PowerMockito.mockStatic(SecurityContextHolder.class);

        // use Mockito to set up your expectation on SecurityContextHolder.getSecurityContext()
        Mockito.when(SecurityContextHolder.getSecurityContext()).thenReturn(mockSecurityContext);
        ...
    }
}

분명히 여기에 약간의 보일러 플레이트 코드가 있습니다. 즉, 인증 객체를 조롱하고, SecurityContext를 조롱하여 인증을 반환하고, 마지막으로 SecurityContextHolder를 조롱하여 SecurityContext를 얻습니다. (비 테스트) 코드를 변경하지 않고도


7

이 경우 정적을 사용하는 것이 보안 코드를 작성하는 가장 좋은 방법입니다.

예, 정적은 일반적으로 좋지 않습니다. 일반적으로이 경우 정적은 원하는 것입니다. 보안 컨텍스트는 Principal을 현재 실행중인 스레드와 연결하므로 가장 안전한 코드는 가능한 한 스레드에서 직접 정적에 액세스합니다. 주입 된 랩퍼 클래스 뒤에 액세스를 숨기면 공격자에게 더 많은 공격 포인트가 제공됩니다. 코드에 액세스 할 필요가 없으며 (jar이 서명 된 경우 변경하기가 어려울 수 있음) 런타임에 수행하거나 일부 XML을 클래스 경로에 넣을 수있는 구성을 재정의하는 방법이 필요합니다. 주석 삽입을 사용하더라도 외부 XML로 재정의 할 수 있습니다. 이러한 XML은 실행중인 시스템에 불량 사용자를 주입 할 수 있습니다.


4

나는 여기에 같은 질문을 직접했고 최근에 찾은 답변을 게시했습니다. 짧은 대답은을 주입하고 Spring 구성에서만 SecurityContext참조 SecurityContextHolder하여SecurityContext


3

일반

그 사이에 (2013 년 SEC-2298 덕분에 버전 3.2부터 ) @AuthenticationPrincipal 주석을 사용하여 인증을 MVC 메소드에 삽입 할 수 있습니다 .

@Controller
class Controller {
  @RequestMapping("/somewhere")
  public void doStuff(@AuthenticationPrincipal UserDetails myUser) {
  }
}

테스트

단위 테스트에서이 메소드를 직접 호출 할 수 있습니다. 통합 테스트에서 org.springframework.test.web.servlet.MockMvc다음 org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user()과 같이 사용자를 주입하는 데 사용할 수 있습니다 .

mockMvc.perform(get("/somewhere").with(user(myUserDetails)));

그러나 이것은 SecurityContext를 직접 채울 것입니다. 테스트에서 사용자가 세션에서로드되었는지 확인하려면 다음을 사용할 수 있습니다.

mockMvc.perform(get("/somewhere").with(sessionUser(myUserDetails)));
/* ... */
private static RequestPostProcessor sessionUser(final UserDetails userDetails) {
    return new RequestPostProcessor() {
        @Override
        public MockHttpServletRequest postProcessRequest(final MockHttpServletRequest request) {
            final SecurityContext securityContext = new SecurityContextImpl();
            securityContext.setAuthentication(
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities())
            );
            request.getSession().setAttribute(
                HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext
            );
            return request;
        }
    };
}

2

Spring의 추상 테스트 클래스와 여기 에서 다루는 모의 객체를 살펴 보겠습니다 . Spring 관리 객체를 자동 배선하는 강력한 방법을 제공하여 유닛 및 통합 테스트를 더 쉽게 만듭니다.


해당 테스트 수업이 도움이되지만 여기에 적용되는지 확실하지 않습니다. 내 테스트에는 ApplicationContext에 대한 개념이 없으므로 필요하지 않습니다. 테스트 메소드가 실행되기 전에 SecurityContext가 채워져 있는지 확인하는 것만으로도 ThreadContext에 먼저 설정해야하는 것이 더럽습니다.
matt b

1

인증은 OS의 프로세스 속성과 같은 방식으로 서버 환경의 스레드 속성입니다. 인증 정보에 액세스하기위한 Bean 인스턴스가 있으면 아무런 구성없이 불편한 구성 및 배선 오버 헤드가 발생합니다.

테스트 인증과 관련하여 인생을 편하게 만드는 방법에는 여러 가지가 있습니다. 내가 가장 좋아하는 것은 사용자 지정 주석을 만들고이를 @Authenticated실행하는 테스트 실행 리스너를 만드는 것입니다. DirtiesContextTestExecutionListener영감을 확인하십시오 .


0

꽤 많은 작업을 한 후 원하는 행동을 재현 할 수있었습니다. MockMvc를 통해 로그인을 에뮬레이션했습니다. 대부분의 단위 테스트에는 너무 무겁지만 통합 테스트에는 도움이됩니다.

물론 Spring Security 4.0의 새로운 기능을 통해 테스트를보다 쉽게 ​​수행 할 수 있습니다.

package [myPackage]

import static org.junit.Assert.*;

import javax.inject.Inject;
import javax.servlet.http.HttpSession;

import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
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;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;

@ContextConfiguration(locations={[my config file locations]})
@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public static class getUserConfigurationTester{

    private MockMvc mockMvc;

    @Autowired
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    private MockHttpServletRequest request;

    @Autowired
    private WebApplicationContext webappContext;

    @Before  
    public void init() {  
        mockMvc = MockMvcBuilders.webAppContextSetup(webappContext)
                    .addFilters(springSecurityFilterChain)
                    .build();
    }  


    @Test
    public void testTwoReads() throws Exception{                        

    HttpSession session  = mockMvc.perform(post("/j_spring_security_check")
                        .param("j_username", "admin_001")
                        .param("j_password", "secret007"))
                        .andDo(print())
                        .andExpect(status().isMovedTemporarily())
                        .andExpect(redirectedUrl("/index"))
                        .andReturn()
                        .getRequest()
                        .getSession();

    request.setSession(session);

    SecurityContext securityContext = (SecurityContext)   session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);

    SecurityContextHolder.setContext(securityContext);

        // Your test goes here. User is logged with 
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.