programing tip

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

itbloger 2020. 7. 3. 20:21
반응형

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


우리 회사는 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객체 자체는 매우 취소 봄 같은 것 ...


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

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

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

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


일반적인 방법으로 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);

정적 메서드 호출은 종속성을 쉽게 조롱 할 수 없으므로 단위 테스트에 특히 문제가됩니다. 내가 당신에게 보여줄 것은 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);
}

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


인증 객체를 생성하고 주입하는 방법에 대한 질문에 답하지 않고 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 장에서 찾을 수 있습니다 (위의 예제가 복사 된 파일).


개인적으로 나는 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);
        ...
    }
}

Admittedly there is quite a bit of boiler plate code here i.e. mock an Authentication object, mock a SecurityContext to return the Authentication and finally mock the SecurityContextHolder to get the SecurityContext, however its very flexible and allows you to unit test for scenarios like null Authentication objects etc. without having to change your (non test) code


Using a static in this case is the best way to write secure code.

Yes, statics are generally bad - generally, but in this case, the static is what you want. Since the security context associates a Principal with the currently running thread, the most secure code would access the static from the thread as directly as possible. Hiding the access behind a wrapper class that is injected provides an attacker with more points to attack. They wouldn't need access to the code (which they would have a hard time changing if the jar was signed), they just need a way to override the configuration, which can be done at runtime or slipping some XML onto the classpath. Even using annotation injection would be overridable with external XML. Such XML could inject the running system with a rogue principal.


I asked the same question myself over here, and just posted an answer that I recently found. Short answer is: inject a SecurityContext, and refer to SecurityContextHolder only in your Spring config to obtain the SecurityContext


I would take a look at Spring's abstract test classes and mock objects which are talked about here. They provide a powerful way of auto-wiring your Spring managed objects making unit and integration testing easier.


General

In the meantime (since version 3.2, in the year 2013, thanks to SEC-2298) the authentication can be injected into MVC methods using the annotation @AuthenticationPrincipal:

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

Tests

In your unit test you can obviously call this Method directly. In integration tests using org.springframework.test.web.servlet.MockMvc you can use org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user() to inject the user like this:

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

This will however just directly fill the SecurityContext. If you want to make sure that the user is loaded from a session in your test, you can use this:

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

Authentication is a property of a thread in server environment in the same way as it is a property of a process in OS. Having a bean instance for accessing authentication information would be inconvenient configuration and wiring overhead without any benefit.

Regarding test authentication there are several ways how you can make your life easier. My favourite is to make a custom annotation @Authenticated and test execution listener, which manages it. Check DirtiesContextTestExecutionListener for inspiration.


After quite a lot of work I was able to reproduce the desired behavior. I had emulated the login through MockMvc. It is too heavy for most unit tests but helpful for integration tests.

Of course I am willing to see those new features in Spring Security 4.0 that will make our testing easier.

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 
}

참고URL : https://stackoverflow.com/questions/360520/unit-testing-with-spring-security

반응형