Como definir manualmente um usuário autenticado no Spring Security / SpringMVC

107

Depois que um novo usuário envia um formulário 'Nova conta', quero fazer o login manualmente desse usuário para que ele não precise fazer login na página seguinte.

A página de login do formulário normal passando pelo interceptor de segurança Spring funciona bem.

No controlador de formulário de nova conta, estou criando um UsernamePasswordAuthenticationToken e configurando-o no SecurityContext manualmente:

SecurityContextHolder.getContext().setAuthentication(authentication);

Na mesma página, verifico posteriormente se o usuário está conectado com:

SecurityContextHolder.getContext().getAuthentication().getAuthorities();

Isso retorna as autoridades que defini anteriormente na autenticação. Tudo está bem.

Mas quando esse mesmo código é chamado na página seguinte que carrego, o token de autenticação é apenas UserAnonymous.

Não estou claro por que ele não manteve a autenticação que defini na solicitação anterior. Alguma ideia?

  • Pode ter a ver com o ID de sessão não estar configurado corretamente?
  • Existe algo que possivelmente está substituindo minha autenticação de alguma forma?
  • Talvez eu precise de mais uma etapa para salvar a autenticação?
  • Ou há algo que preciso fazer para declarar a autenticação em toda a sessão, em vez de uma única solicitação de alguma forma?

Só estou procurando alguns pensamentos que possam me ajudar a ver o que está acontecendo aqui.

David Parks
fonte
1
Você pode seguir minha resposta para stackoverflow.com/questions/4824395/…
AlexK 01 de
2
Leitores, cuidado com as respostas para esta pergunta se eles dizem para você fazer: SecurityContextHolder.getContext().setAuthentication(authentication). Funciona e é comum, mas existem sérias deficiências de funcionalidade que você encontrará se simplesmente fizer isso. Para obter mais informações, veja minha pergunta e a resposta: stackoverflow.com/questions/47233187/…
goat

Respostas:

62

Eu tive o mesmo problema que você um tempo atrás. Não consigo me lembrar dos detalhes, mas o código a seguir fez as coisas funcionarem para mim. Este código é usado dentro de um fluxo Spring Webflow, portanto, as classes RequestContext e ExternalContext. Mas a parte mais relevante para você é o método doAutoLogin.

public String registerUser(UserRegistrationFormBean userRegistrationFormBean,
                           RequestContext requestContext,
                           ExternalContext externalContext) {

    try {
        Locale userLocale = requestContext.getExternalContext().getLocale();
        this.userService.createNewUser(userRegistrationFormBean, userLocale, Constants.SYSTEM_USER_ID);
        String emailAddress = userRegistrationFormBean.getChooseEmailAddressFormBean().getEmailAddress();
        String password = userRegistrationFormBean.getChoosePasswordFormBean().getPassword();
        doAutoLogin(emailAddress, password, (HttpServletRequest) externalContext.getNativeRequest());
        return "success";

    } catch (EmailAddressNotUniqueException e) {
        MessageResolver messageResolvable 
                = new MessageBuilder().error()
                                      .source(UserRegistrationFormBean.PROPERTYNAME_EMAIL_ADDRESS)
                                      .code("userRegistration.emailAddress.not.unique")
                                      .build();
        requestContext.getMessageContext().addMessage(messageResolvable);
        return "error";
    }

}


private void doAutoLogin(String username, String password, HttpServletRequest request) {

    try {
        // Must be called from request filtered by Spring Security, otherwise SecurityContextHolder is not updated
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
        token.setDetails(new WebAuthenticationDetails(request));
        Authentication authentication = this.authenticationProvider.authenticate(token);
        logger.debug("Logging in with [{}]", authentication.getPrincipal());
        SecurityContextHolder.getContext().setAuthentication(authentication);
    } catch (Exception e) {
        SecurityContextHolder.getContext().setAuthentication(null);
        logger.error("Failure in autoLogin", e);
    }

}
Kevin Stembridge
fonte
2
Obrigado, o código é muito útil para me ajudar a saber que estou solucionando problemas na área certa. Parece que eu tenho uma arma fumegante, está criando uma nova id de sessão após a autenticação manual, mas a id de sessão antiga ainda está sendo identificada no cookie. Tenho que descobrir o porquê agora, mas pelo menos estou claramente no caminho certo. Obrigado!
David Parks
4
Qualquer pessoa que siga esta orientação também deve ver este problema relacionado: stackoverflow.com/questions/4824395/…
David Parks
14
Você pode explicar como está obtendo o authenticationProvider
Vivex de
1
@ s1moner3d você deve conseguir injetá-lo via IoC -> \ @Autowired
Hartmut
1
@Configuration public class WebConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationProvider() throws Exception { return super.authenticationManagerBean(); } }
slisnychyi
66

Não consegui encontrar nenhuma outra solução completa, então pensei em postar a minha. Isso pode ser um pouco hack, mas resolveu o problema acima:

public void login(HttpServletRequest request, String userName, String password)
{

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userName, password);

    // Authenticate the user
    Authentication authentication = authenticationManager.authenticate(authRequest);
    SecurityContext securityContext = SecurityContextHolder.getContext();
    securityContext.setAuthentication(authentication);

    // Create a new session and add the security context.
    HttpSession session = request.getSession(true);
    session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
}
Stuart McIntyre
fonte
7
+1 - Isso me ajudou! Eu estava perdendo a atualização SPRING_SECURITY_CONTEXT. ... Mas quão "sujo" é isso?
13dx
12
de onde você authenticationManagervem?
Isaac
2
AuthenticationManager é autowired em sua classe como este @Autowired AuthenticationServiceImpl authenticationManager. E também deve haver uma injeção de bean em sua configuração xml, para que o Spring saiba o que injetar.
1
onde está a implementação de AuthenticationServiceImpl? O que esta classe contém?
Pra_A
3
Por que é necessário criar uma nova sessão? O SecurityContext não lida com isso?
Vlad Manuel Mureșan
17

Por fim, descobri a raiz do problema.

Quando eu crio o contexto de segurança manualmente, nenhum objeto de sessão é criado. Somente quando a solicitação termina o processamento, o mecanismo Spring Security percebe que o objeto de sessão é nulo (quando ele tenta armazenar o contexto de segurança para a sessão depois que a solicitação foi processada).

No final da solicitação, o Spring Security cria um novo objeto de sessão e ID de sessão. No entanto, este novo ID de sessão nunca chega ao navegador porque ocorre no final da solicitação, após a resposta ao navegador ter sido feita. Isso faz com que o novo ID de sessão (e, portanto, o contexto de Segurança contendo meu usuário conectado manualmente) seja perdido quando a próxima solicitação contém o ID de sessão anterior.

David Parks
fonte
4
Honestamente, isso parece mais uma falha de projeto na segurança de molas. Existem muitos frameworks escritos em outras linguagens que não teriam nenhum problema com isso, mas Spring Security simplesmente falha.
chubbsondubs
3
e a solução é?
s1moner3d
2
e qual é a solução?
Thiago
6

Ative o registro de depuração para obter uma imagem melhor do que está acontecendo.

Você pode saber se os cookies de sessão estão sendo configurados usando um depurador do lado do navegador para examinar os cabeçalhos retornados nas respostas HTTP. (Existem outras maneiras também.)

Uma possibilidade é que SpringSecurity esteja configurando cookies de sessão segura, e sua próxima página solicitada tenha uma URL "http" em vez de uma URL "https". (O navegador não enviará um cookie seguro para um URL "http".)

Stephen C
fonte
Obrigado, essas sugestões foram muito úteis e relevantes!
David Parks
5

O novo recurso de filtragem no Servlet 2.4 basicamente alivia a restrição de que os filtros só podem operar no fluxo de solicitação antes e depois do processamento da solicitação real pelo servidor de aplicativos. Em vez disso, os filtros do Servlet 2.4 agora podem interagir com o despachante de solicitação em cada ponto de despacho. Isso significa que quando um recurso da Web encaminha uma solicitação para outro recurso (por exemplo, um servlet encaminhando a solicitação para uma página JSP no mesmo aplicativo), um filtro pode estar operando antes que a solicitação seja tratada pelo recurso de destino. Isso também significa que, se um recurso da Web incluir a saída ou função de outros recursos da Web (por exemplo, uma página JSP incluindo a saída de várias outras páginas JSP), os filtros do Servlet 2.4 podem funcionar antes e depois de cada um dos recursos incluídos. .

Para ativar esse recurso, você precisa:

web.xml

<filter>   
    <filter-name>springSecurityFilterChain</filter-name>   
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 
</filter>  
<filter-mapping>   
    <filter-name>springSecurityFilterChain</filter-name>   
    <url-pattern>/<strike>*</strike></url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>

RegistrationController

return "forward:/login?j_username=" + registrationModel.getUserEmail()
        + "&j_password=" + registrationModel.getPassword();
AlexK
fonte
Boas informações, mas colocar o nome de usuário e a senha no url é ruim. 1) nenhum escape é feito, portanto, um nome de usuário ou uma senha com um caractere especial provavelmente serão corrompidos ou, pior ainda, serão usados ​​como um vetor de exploração de segurança. 2) as senhas em urls são ruins porque urls geralmente são registradas no disco, o que é muito ruim para a segurança - todas as suas senhas em texto simples ficam lá.
cabra
1

Eu estava tentando testar um aplicativo extjs e depois de definir com sucesso um testingAuthenticationToken, isso de repente parou de funcionar sem uma causa óbvia.

Não consegui fazer com que as respostas acima funcionassem, então minha solução foi pular essa parte da primavera no ambiente de teste. Eu introduzi uma costura na primavera como esta:

public class SpringUserAccessor implements UserAccessor
{
    @Override
    public User getUser()
    {
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();
        return (User) authentication.getPrincipal();
    }
}

O usuário é um tipo personalizado aqui.

Em seguida, estou envolvendo-o em uma classe que tem apenas uma opção para o código de teste alternar para fora.

public class CurrentUserAccessor
{
    private static UserAccessor _accessor;

    public CurrentUserAccessor()
    {
        _accessor = new SpringUserAccessor();
    }

    public User getUser()
    {
        return _accessor.getUser();
    }

    public static void UseTestingAccessor(User user)
    {
        _accessor = new TestUserAccessor(user);
    }
}

A versão de teste tem a seguinte aparência:

public class TestUserAccessor implements UserAccessor
{
    private static User _user;

    public TestUserAccessor(User user)
    {
        _user = user;
    }

    @Override
    public User getUser()
    {
        return _user;
    }
}

No código de chamada, ainda estou usando um usuário apropriado carregado do banco de dados:

    User user = (User) _userService.loadUserByUsername(username);
    CurrentUserAccessor.UseTestingAccessor(user);

Obviamente, isso não será adequado se você realmente precisar usar a segurança, mas estou executando uma configuração sem segurança para a implantação de teste. Achei que outra pessoa pudesse passar por uma situação semelhante. Este é um padrão que usei para simular dependências estáticas antes. A outra alternativa é que você pode manter a estática da classe de invólucro, mas eu prefiro esta porque as dependências do código são mais explícitas, já que você precisa passar CurrentUserAccessor para as classes onde é necessário.

JonnyRaa
fonte