문제

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

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

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

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

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

...어떤 면에서는 이 개체가 (전역) 싱글톤이기 때문에 매우 봄이 아닌 것처럼 보입니다.

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

각 테스트 케이스의 초기화 메소드에서 이를 연결해야 합니까?

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

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

그만큼 SecurityContextHolder 객체 자체는 매우 봄 같지 않은 것 같습니다 ...

도움이 되었습니까?

해결책

문제는 Spring Security가 인증 객체를 컨테이너의 빈으로 사용 가능하게 만들지 않기 때문에 이를 상자 밖으로 쉽게 주입하거나 자동 연결할 수 있는 방법이 없다는 것입니다.

Spring Security를 ​​사용하기 전에는 Principal을 저장하기 위해 컨테이너에 세션 범위 Bean을 생성하고 이를 "AuthenticationService"(싱글톤)에 삽입한 다음 이 Bean을 현재 Principal에 대한 지식이 필요한 다른 서비스에 삽입했습니다.

자체 인증 서비스를 구현하는 경우 기본적으로 동일한 작업을 수행할 수 있습니다."principal" 속성을 사용하여 세션 범위 빈을 생성하고 이를 인증 서비스에 삽입하고 인증 서비스가 성공적인 인증에 대한 속성을 설정하도록 한 다음 필요할 때 다른 빈에서 인증 서비스를 사용할 수 있도록 합니다.

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

다른 팁

일반적인 방법으로 수행 한 다음 사용하여 삽입하십시오. 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);

당신은 걱정할 옳습니다 - 정적 메소드 호출은 종속성을 쉽게 조롱 할 수 없으므로 단위 테스트에 특히 문제가됩니다. 내가 당신에게 보여줄 것은 스프링 IOC 컨테이너가 더러운 일을하도록하는 방법입니다. SecurityContexTholder는 프레임 워크 클래스이며 저수준 보안 코드가 연결되어 있으면 괜찮을 수 있지만 UI 구성 요소 (IE 컨트롤러)에 깔끔한 인터페이스를 노출하려고합니다.

Cliff.meyers는 주변의 한 가지 방법을 언급했습니다. 자신의 "주요"유형을 만들고 인스턴스를 소비자에게 주입합니다. 봄AOP : 스코프 프록시/> 요청 범위 Bean 정의와 결합 된 2.x로 도입 된 태그는 공장에서 모드 지원이 가장 읽기 쉬운 코드의 티켓 일 수 있습니다.

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

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 컨텍스트에서 Principal을 보유하기 위해 요청 스코핑 된 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 태그의 마법 덕분에 정적 메소드 GetUserDetails는 새로운 HTTP 요청이 들어올 때마다 호출되며 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();
    ...
}

사용할 옵션도 있습니다 @WithUserDetails a UserDetails 에서 돌아 왔습니다 UserDetailsService, 예를 들어

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

자세한 내용은 @withmockuser 그리고 @WithUserDetails Spring Security Reference Docs의 장 (위의 예제가 복사 된 장)

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

여기에는 상당히 많은 보일러 플레이트 코드가 있습니다. 즉, 인증 객체를 조롱하고, 인증을 반환하고 SecurityContexTholder를 조롱하여 SecurityContolder를 조롱하기 위해 SecurityContext를 조롱하지만, 매우 유연하며 Null Authentication Objects와 같은 시나리오에 대한 시나리오에 대한 테스트를 장착 할 수 있습니다. (비 테스트) 코드를 변경하지 않고도

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

예, 정적은 일반적으로 나쁘지만 일반적으로 정적은 정적이 원하는 것입니다. 보안 컨텍스트는 현재 실행중인 스레드와 교장을 연결하므로 가장 안전한 코드는 가능한 한 직접 스레드의 정적에 액세스 할 수 있습니다. 주입 된 래퍼 클래스 뒤에 액세스를 숨기면 공격자에게 더 많은 공격을 할 수 있습니다. 코드에 액세스 할 필요가 없습니다 (JAR에 서명 한 경우 변경하기가 어려울 것입니다). 런타임에 수행하거나 일부 XML을 클래스 경로로 미끄러 뜨릴 수있는 구성을 무시하는 방법 만 있으면됩니다. 주석 주입을 사용하더라도 외부 XML에서는 우선 할 수 있습니다. 이러한 XML은 Rogue Principal과 함께 실행 시스템을 주입 할 수 있습니다.

나는 같은 질문을 스스로 물었다 여기, 그리고 방금 최근에 찾은 답변을 게시했습니다. 짧은 대답은 : 주사 a SecurityContext, 그리고 참조하십시오 SecurityContextHolder 스프링 구성에서만 가능합니다 SecurityContext

나는 Spring의 추상 테스트 클래스와 모의 객체에 대해 살펴보겠습니다. 여기.이는 Spring 관리 객체를 자동 연결하는 강력한 방법을 제공하여 단위 및 통합 테스트를 더 쉽게 만듭니다.

일반적인

그 동안 (버전 3.2 이후 2013 년에. SEC-2298) 주석을 사용하여 인증을 MVC 방법에 주입 할 수 있습니다. @authenticationprincipal:

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

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

테스트 인증과 관련하여 인생을 더 쉽게 만들 수있는 방법이 몇 가지 있습니다. 제가 가장 좋아하는 것은 사용자 정의 주석을 만드는 것입니다 @Authenticated 테스트 실행 리스너,이를 관리합니다. 확인하다 DirtiesContextTestExecutionListener 영감을 위해.

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

물론 스프링 보안 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 
}
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top