[java] 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 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 장에서 찾을 수 있습니다 (위의 예제가 복사 됨)


답변

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

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


답변

개인적으로 나는 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를 얻습니다. (비 테스트) 코드를 변경하지 않고도


답변

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

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


답변

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