Pergunta

Minha empresa vem avaliando Spring MVC para determinar se devemos usá-lo em um dos nossos próximos projetos. Até agora eu amo o que eu vi, e agora eu estou tendo um olhar para o módulo Spring Security para determinar se é algo que pode / deve usar.

requisitos de segurança Nossos são bastante básico; um usuário só precisa ser capaz de fornecer um nome de usuário e senha para ser capaz de acessar certas partes do site (como para obter informações sobre a sua conta); e há um punhado de páginas no site (FAQs, apoio, etc) onde um usuário anônimo deve ser dado acesso.

No protótipo Fui criando, fui armazenando um "LoginCredentials" objeto (que apenas contém nome de usuário e senha) em sessão para um usuário autenticado; alguns dos controladores de verificar para ver se este objeto está em sessão para obter uma referência ao registado no nome de usuário, por exemplo. Eu estou olhando para substituir essa lógica home-grown com Spring Security em vez disso, o que teria o benefício agradável de remover qualquer tipo de "como é que nós rastreamos usuários logados?" e "como é que vamos autenticar usuários?" do meu código controlador / negócio.

Parece que Spring Security fornece uma (thread por) "contexto" objeto para ser capaz de acessar o nome de usuário / principal informações de qualquer lugar em seu aplicativo ...

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

... que parece muito un-Spring como como este objeto é um Singleton (global), de uma forma.

A minha pergunta é esta: se esta é a maneira padrão para acessar informações sobre o usuário autenticado em Spring Security, qual é a forma aceita para injetar um objeto de autenticação para o SecurityContext para que ele está disponível para os meus testes de unidade quando a unidade testes requerem um usuário autenticado?

Eu preciso ligar isso no método de inicialização de cada caso de teste?

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

Isto parece excessivamente detalhado. Existe uma maneira mais fácil?

O próprio objeto SecurityContextHolder parece muito un-Spring-like ...

Foi útil?

Solução

O problema é que Spring Security não faz o objeto de autenticação disponível como um feijão no recipiente, então não há nenhuma maneira de facilmente injetar ou autowire-lo para fora da caixa.

Antes de começarmos a utilizar Spring Security, gostaríamos de criar um bean com escopo de sessão no recipiente para armazenar o principal, injetar isso em um "AuthenticationService" (singleton) e depois injectar este feijão em outros serviços que conhecimento necessário do Diretor atual.

Se você estiver implementando o seu próprio serviço de autenticação, você pode fazer basicamente a mesma coisa: criar um bean com escopo de sessão com um "principal" propriedade, injetar isso em seu serviço de autenticação, ter o serviço auth definir a propriedade na auth sucesso e, em seguida, fazer o serviço de autenticação disponíveis para outros grãos que for necessário.

Eu não me sinto muito mal sobre o uso SecurityContextHolder. Apesar. Eu sei que é um static / Singleton e que desencoraja Primavera usando tais coisas, mas a sua implementação tem o cuidado de se comportar adequadamente dependendo do ambiente: no escopo da sessão em um recipiente Servlet, thread-com escopo em um teste JUnit, etc. O fator limitante reais de um Singleton é quando ele fornece uma implementação que é inflexível a diferentes ambientes.

Outras dicas

Apenas fazê-lo da forma habitual e, em seguida, inseri-lo usando SecurityContextHolder.setContext() em sua classe de teste, por exemplo:

Controlador:

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

Test:

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

Você tem toda a razão para se preocupar - chamadas de métodos estáticos são particularmente problemáticos para testes de unidade como você não pode facilmente zombar suas dependências. O que eu vou lhe mostrar é como deixar o contêiner do Spring IoC fazer o trabalho sujo para você, deixando-o com código puro, testável. SecurityContextHolder é uma classe quadro e, embora possa ser ok para o seu código de segurança de baixo nível para ser vinculados a ela, você provavelmente vai querer expor uma interface mais limpa aos seus componentes de interface do usuário (ou seja, controladores).

cliff.meyers mencionou uma maneira de contornar isso - criar seu próprio "principal" tipo e injetar uma instância para os consumidores. A Primavera << a href = "http://static.springframework.org/spring/docs/2.5.x/reference/beans.html#beans-factory-scopes-other-injection" rel = "noreferrer"> aop: escopo-proxy tag /> introduzido em 2.x combinado com uma definição de feijão pedido escopo, eo suporte de método de fábrica pode ser o bilhete para o código mais legível.

Ele poderia trabalhar como seguir:

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
}

Nada complicado até agora, certo? Na verdade, você provavelmente teve que fazer a maior parte isso já. Em seguida, no seu bean contexto definir um bean com escopo de solicitação para manter o principal:

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

Graças à magia do aop: tag do proxy, o método getUserDetails estáticos será chamado cada vez que um novo pedido HTTP chega e quaisquer referências à propriedade currentUser será resolvido corretamente. Agora o teste de unidade se torna trivial:

protected void setUp() {
    // existing init code

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

Espero que isso ajude!

Sem responder à pergunta sobre como criar e injetar Authentication objetos, Spring Security 4.0 oferece algumas alternativas bem-vindos quando se trata de testes. A anotação @WithMockUser permite ao desenvolvedor especificar um usuário simulada (com as autoridades opcionais, nome de usuário, senha e funções) em uma maneira pura:

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

Há também a opção de usar @WithUserDetails para emular um UserDetails retornado do UserDetailsService, por exemplo.

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

Mais detalhes podem ser encontrados no @ WithMockUser e @ WithUserDetails capítulos nos documentos de referência de segurança da mola (a partir dos quais os exemplos acima foram copiados)

Pessoalmente, eu iria usar apenas PowerMock juntamente com Mockito ou EasyMock para zombar a estática SecurityContextHolder.getSecurityContext () no seu teste de unidade / integração por exemplo.

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

É verdade que há um pouco de código de placa de caldeira aqui isto é zombar um objeto de autenticação, zombar um SecurityContext para retornar a autenticação e, finalmente, zombar do SecurityContextHolder para obter o SecurityContext, no entanto é muito flexível e permite que você teste de unidade para cenários como Autenticação nula objetos etc., sem ter que mudar seu (não teste) código

Usando um estático, neste caso, é a melhor maneira de escrever código seguro.

Sim, estática são geralmente ruim - geralmente, mas neste caso, a estática é o que você quer. Desde o contexto de segurança associados um principal com o segmento em execução no momento, o código mais seguro seria acessar a estática do fio o mais diretamente possível. Escondendo o acesso por trás de uma classe de invólucro que é injectado fornece um intruso com mais pontos de ataque. Eles não precisam de ter acesso ao código (que eles teriam dificuldade em mudar se o frasco foi assinado), eles só precisam de uma maneira para substituir a configuração, que pode ser feito em tempo de execução ou escorregar alguns XML para o classpath. Mesmo usando injeção de anotação seria substituível com XML externo. Tal XML pode injetar o sistema funcionando com um principal desonestos.

Eu fiz a mesma pergunta me sobre aqui , e só postou uma resposta que eu encontrei recentemente. A resposta curta é: injetar um SecurityContext, e referem-se a SecurityContextHolder apenas na sua configuração da Primavera para obter o SecurityContext

Gostaria de dar uma olhada em classes de teste abstratas de Primavera e objetos mock que se fala aqui . Eles fornecem uma maneira poderosa de-fiação auto seu Primavera gerido objetos que fazem unidade e integração testar mais fácil.

Geral

Nesse meio tempo (desde a versão 3.2, no ano de 2013, graças a SEC-2298 ) a autenticação pode ser injectado no MVC métodos que utilizam a anotação @ AuthenticationPrincipal :

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

testes

Em seu teste de unidade pode obviamente chamar esse método diretamente. Em testes de integração usando org.springframework.test.web.servlet.MockMvc você pode usar org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user() para injetar o usuário assim:

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

Este será, no entanto apenas preencher diretamente o SecurityContext. Se você quiser ter certeza de que o usuário é carregado a partir de uma sessão no seu teste, você pode usar isto:

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

A autenticação é uma propriedade de um segmento em ambiente de servidor da mesma forma como é uma propriedade de um processo no OS. Ter um exemplo de feijão para acessar informações de autenticação seria configuração inconveniente e sobrecarga de fiação sem qualquer benefício.

Em relação a autenticação de teste existem várias maneiras como você pode tornar sua vida mais fácil. O meu favorito é fazer uma @Authenticated anotação personalizado e execução do teste ouvinte, que o administra. Verifique DirtiesContextTestExecutionListener para a inspiração.

Depois de um monte de trabalho que foi capaz de reproduzir o comportamento desejado. Eu tinha imitado o login através MockMvc. É muito pesado para a maioria dos testes de unidade, mas útil para testes de integração.

É claro que eu estou disposto a ver esses novos recursos do Spring Security 4.0 que vai fazer o nosso teste mais fácil.

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 
}
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top