Ao usar o Spring Security, qual é a maneira correta de obter informações atuais de nome de usuário (por exemplo, SecurityContext) em um bean?

288

Eu tenho um aplicativo da Web Spring MVC que usa o Spring Security. Quero saber o nome de usuário do usuário conectado no momento. Estou usando o snippet de código fornecido abaixo. Essa é a maneira aceita?

Não gosto de chamar um método estático dentro deste controlador - que derrota todo o propósito do Spring, IMHO. Existe uma maneira de configurar o aplicativo para injetar o SecurityContext atual ou a autenticação atual?

  @RequestMapping(method = RequestMethod.GET)
  public ModelAndView showResults(final HttpServletRequest request...) {
    final String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
    ...
  }
Scott Bale
fonte
Por que você não tem um controlador (controlador seguro) como uma superclasse obtém o usuário do SecurityContext e o define como uma variável de instância dentro dessa classe? Dessa forma, quando você estender o controlador seguro, toda a sua classe terá acesso ao principal de Usuário do contexto atual.
Dehan de Croos

Respostas:

259

Se você estiver usando o Spring 3 , a maneira mais fácil é:

 @RequestMapping(method = RequestMethod.GET)   
 public ModelAndView showResults(final HttpServletRequest request, Principal principal) {

     final String currentUser = principal.getName();

 }
tsunade21
fonte
69

Muita coisa mudou no mundo da primavera desde que essa pergunta foi respondida. O Spring simplificou a obtenção do usuário atual em um controlador. Para outros beans, o Spring adotou as sugestões do autor e simplificou a injeção do 'SecurityContextHolder'. Mais detalhes estão nos comentários.


Essa é a solução que eu acabei usando. Em vez de usar SecurityContextHolderno meu controlador, quero injetar algo que seja usado SecurityContextHoldersob o capô, mas que abstraia a classe do tipo código do meu código. Não encontrei outra maneira de fazer isso além de rolar minha própria interface, assim:

public interface SecurityContextFacade {

  SecurityContext getContext();

  void setContext(SecurityContext securityContext);

}

Agora, meu controlador (ou qualquer outro POJO) ficaria assim:

public class FooController {

  private final SecurityContextFacade securityContextFacade;

  public FooController(SecurityContextFacade securityContextFacade) {
    this.securityContextFacade = securityContextFacade;
  }

  public void doSomething(){
    SecurityContext context = securityContextFacade.getContext();
    // do something w/ context
  }

}

E, devido à interface ser um ponto de dissociação, o teste de unidade é simples. Neste exemplo eu uso o Mockito:

public class FooControllerTest {

  private FooController controller;
  private SecurityContextFacade mockSecurityContextFacade;
  private SecurityContext mockSecurityContext;

  @Before
  public void setUp() throws Exception {
    mockSecurityContextFacade = mock(SecurityContextFacade.class);
    mockSecurityContext = mock(SecurityContext.class);
    stub(mockSecurityContextFacade.getContext()).toReturn(mockSecurityContext);
    controller = new FooController(mockSecurityContextFacade);
  }

  @Test
  public void testDoSomething() {
    controller.doSomething();
    verify(mockSecurityContextFacade).getContext();
  }

}

A implementação padrão da interface é assim:

public class SecurityContextHolderFacade implements SecurityContextFacade {

  public SecurityContext getContext() {
    return SecurityContextHolder.getContext();
  }

  public void setContext(SecurityContext securityContext) {
    SecurityContextHolder.setContext(securityContext);
  }

}

E, finalmente, a configuração do Spring de produção é assim:

<bean id="myController" class="com.foo.FooController">
     ...
  <constructor-arg index="1">
    <bean class="com.foo.SecurityContextHolderFacade">
  </constructor-arg>
</bean>

Parece um pouco tolo que o Spring, um recipiente de injeção de dependência de todas as coisas, não tenha fornecido uma maneira de injetar algo semelhante. Eu entendo que SecurityContextHolderfoi herdado do acegi, mas ainda assim. O problema é que eles estão muito próximos - se apenas SecurityContextHolderhouvesse um getter para obter a SecurityContextHolderStrategyinstância subjacente (que é uma interface), você poderia injetar isso. Na verdade, eu até abri um problema com o Jira nesse sentido.

Uma última coisa - eu mudei substancialmente a resposta que tinha aqui antes. Verifique o histórico se você estiver curioso, mas, como um colega de trabalho apontou para mim, minha resposta anterior não funcionaria em um ambiente multithread. O subjacente SecurityContextHolderStrategyusado por SecurityContextHolderé, por padrão, uma instância de ThreadLocalSecurityContextHolderStrategy, que armazena SecurityContexts em a ThreadLocal. Portanto, não é necessariamente uma boa idéia injetar o SecurityContextdiretamente em um bean no momento da inicialização - pode ser necessário recuperá-lo de ThreadLocalcada vez, em um ambiente multithread, para que o correto seja recuperado.

Scott Bale
fonte
1
Gosto da sua solução - é um uso inteligente do suporte ao método de fábrica no Spring. Dito isto, isso está funcionando para você porque o objeto do controlador está no escopo da solicitação da web. Se você alterasse o escopo do bean do controlador da maneira errada, isso seria interrompido.
1913 Paul Morie
2
Os dois comentários anteriores se referem a uma resposta antiga e incorreta que acabei de substituir.
Scott Bale
12
Essa ainda é a solução recomendada com a versão atual do Spring? Não acredito que ele precise de muito código apenas para recuperar apenas o nome de usuário.
Ta Sas
6
Se você estiver usando o Spring Security 3.0.x, eles implementaram minha sugestão no problema do JIRA que registrei em jira.springsource.org/browse/SEC-1188, para que você possa agora injetar a instância SecurityContextHolderStrategy (do SecurityContextHolder) diretamente no seu bean via padrão Configuração de mola.
Scott Bale
4
Por favor, veja a resposta tsunade21. Spring 3 agora permite que você use java.security.Principal como um argumento de método em seu controlador
Patrick
22

Eu concordo que ter que consultar o SecurityContext para o usuário atual fede, parece uma maneira muito improvável de lidar com esse problema.

Eu escrevi uma classe estática "auxiliar" para lidar com esse problema; é sujo, pois é um método global e estático, mas imaginei que, se mudarmos algo relacionado à segurança, pelo menos só preciso alterar os detalhes em um só lugar:

/**
* Returns the domain User object for the currently logged in user, or null
* if no User is logged in.
* 
* @return User object for the currently logged in user, or null if no User
*         is logged in.
*/
public static User getCurrentUser() {

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

    if (principal instanceof MyUserDetails) return ((MyUserDetails) principal).getUser();

    // principal object is either null or represents anonymous user -
    // neither of which our domain User object can represent - so return null
    return null;
}


/**
 * Utility method to determine if the current user is logged in /
 * authenticated.
 * <p>
 * Equivalent of calling:
 * <p>
 * <code>getCurrentUser() != null</code>
 * 
 * @return if user is logged in
 */
public static boolean isLoggedIn() {
    return getCurrentUser() != null;
}
matt b
fonte
22
é contanto que SecurityContextHolder.getContext () e o último seja seguro para threads, pois mantém os detalhes de segurança em um threadLocal. Este código não mantém nenhum estado.
Matt b
22

Para que ele apareça nas suas páginas JSP, você pode usar o Spring Security Tag Lib:

http://static.springsource.org/spring-security/site/docs/3.0.x/reference/taglibs.html

Para usar qualquer uma das tags, você deve ter o taglib de segurança declarado em seu JSP:

<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>

Em uma página jsp, faça algo assim:

<security:authorize access="isAuthenticated()">
    logged in as <security:authentication property="principal.username" /> 
</security:authorize>

<security:authorize access="! isAuthenticated()">
    not logged in
</security:authorize>

NOTA: Conforme mencionado nos comentários de @ SBerg413, você precisará adicionar

use-expression = "true"

para a tag "http" na configuração security.xml para que isso funcione.

Brad Parks
fonte
Parece que provavelmente é a maneira aprovada pela Spring Security!
quer
3
Para que esse método funcione, é necessário adicionar use-expression = "true" à tag http na configuração security.xml.
SBerg413 01/03
Obrigado @ SBerg413, vou editar minha resposta e adicionar seu esclarecimento importante!
Brad Parks
14

Se você estiver usando o Spring Security ver> = 3.2, poderá usar a @AuthenticationPrincipalanotação:

@RequestMapping(method = RequestMethod.GET)
public ModelAndView showResults(@AuthenticationPrincipal CustomUser currentUser, HttpServletRequest request) {
    String currentUsername = currentUser.getUsername();
    // ...
}

Aqui CustomUserestá um objeto customizado que implementa UserDetailsque é retornado por um customizado UserDetailsService.

Mais informações podem ser encontradas no capítulo @AuthenticationPrincipal dos documentos de referência do Spring Security.

Matsev
fonte
13

Eu obtenho usuário autenticado por HttpServletRequest.getUserPrincipal ();

Exemplo:

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.support.RequestContext;

import foo.Form;

@Controller
@RequestMapping(value="/welcome")
public class IndexController {

    @RequestMapping(method=RequestMethod.GET)
    public String getCreateForm(Model model, HttpServletRequest request) {

        if(request.getUserPrincipal() != null) {
            String loginName = request.getUserPrincipal().getName();
            System.out.println("loginName : " + loginName );
        }

        model.addAttribute("form", new Form());
        return "welcome";
    }
}
digz6666
fonte
Eu gosto da sua solução. Para especialistas do Spring: Essa é uma solução segura e boa?
Marioosh 12/04
Não é uma boa solução. Você receberá nullse o usuário for autenticado anonimamente ( http> anonymouselementos no XML do Spring Security). SecurityContextHolderou SecurityContextHolderStrategyé o caminho correto.
Nowaker
1
Para que eu verifiquei se não é nulo request.getUserPrincipal ()! = Null.
digz6666
É nulo no filtro
Alex78191
9

Na primavera 3+, você tem as seguintes opções.

Opção 1 :

@RequestMapping(method = RequestMethod.GET)    
public String currentUserNameByPrincipal(Principal principal) {
    return principal.getName();
}

Opção 2 :

@RequestMapping(method = RequestMethod.GET)
public String currentUserNameByAuthentication(Authentication authentication) {
    return authentication.getName();
}

Opção 3:

@RequestMapping(method = RequestMethod.GET)    
public String currentUserByHTTPRequest(HttpServletRequest request) {
    return request.getUserPrincipal().getName();

}

Opção 4: Fantasia: verifique isso para obter mais detalhes

public ModelAndView someRequestHandler(@ActiveUser User activeUser) {
  ...
}
Fazenda
fonte
1
Desde o 3.2, o spring-security-web vem com o @CurrentUserque funciona como o costume @ActiveUserdo seu link.
Mike Partridge
@ MikePartridge, não parece encontrar o que você está dizendo, qualquer link ?? ou mais informações ??
21
1
Meu erro - eu entendi mal o javadoc do Spring Security AuthenticationPrincipalArgumentResolver . Um exemplo é mostrado nesse agrupamento @AuthenticationPrincipalcom uma @CurrentUseranotação personalizada . Desde o 3.2, não precisamos implementar um resolvedor de argumentos personalizado, como na resposta vinculada. Essa outra resposta tem mais detalhes.
Mike Partridge
5

Sim, as estatísticas geralmente são ruins - geralmente, mas nesse caso, a estática é o código mais seguro que você pode escrever. Como o contexto de segurança associa um Principal ao encadeamento atualmente em execução, o código mais seguro acessa a estática do encadeamento o mais diretamente possível. Ocultar o acesso atrás de uma classe de invólucro injetada fornece ao invasor mais pontos para atacar. Eles não precisariam acessar o código (que dificilmente mudariam se o jar fosse assinado), eles apenas precisariam de uma maneira de substituir a configuração, o que pode ser feito em tempo de execução ou colocar um pouco de XML no caminho de classe. Mesmo usando a injeção de anotação no código assinado seria substituível pelo XML externo. Esse XML pode injetar no sistema em execução um principal não autorizado.

Michael Bushe
fonte
5

Eu apenas faria isso:

request.getRemoteUser();
Dan
fonte
1
Isso pode funcionar, mas não de forma confiável. Do javadoc: "O envio do nome de usuário a cada solicitação subsequente depende do navegador e do tipo de autenticação." - download-llnw.oracle.com/javaee/6/api/javax/servlet/http/...
Scott Bale
3
Essa é realmente uma maneira válida e muito simples de obter o nome de usuário remoto em um aplicativo Web Spring Security. A cadeia de filtros padrão inclui uma SecurityContextHolderAwareRequestFilterque agrupa a solicitação e implementa essa chamada acessando o SecurityContextHolder.
Shaun the Sheep
4

Para o último aplicativo Spring MVC que escrevi, não injetei o titular SecurityContext, mas eu tinha um controlador básico que tinha dois métodos utilitários relacionados a isso ... isAuthenticated () & getUsername (). Internamente, eles fazem a chamada de método estático que você descreveu.

Pelo menos, é apenas um lugar, se você precisar refatorar mais tarde.

RichH
fonte
3

Você pode usar o Spring AOP. Por exemplo, se você tiver algum serviço, isso precisa conhecer o principal atual. Você pode introduzir anotações personalizadas, como @Principal, que indicam que este Serviço deve depender do principal.

public class SomeService {
    private String principal;
    @Principal
    public setPrincipal(String principal){
        this.principal=principal;
    }
}

Em seu conselho, que eu acho que precisa estender o MethodBeforeAdvice, verifique se o serviço específico tem anotação @Principal e injeta o nome Principal ou defina-o como 'ANÔNIMO'.

Pavel Rodionov
fonte
Preciso acessar o Principal dentro da classe de serviço. Você poderia postar um exemplo completo no github? Eu não sei Primavera AOP, daí o pedido.
Rakesh Waghela
2

O único problema é que, mesmo após a autenticação com o Spring Security, o usuário / bean principal não existe no contêiner, portanto, a injeção de dependência será difícil. Antes de usarmos o Spring Security, criaríamos um bean com escopo de sessão com o Principal atual, injetar isso em um "AuthService" e injetar esse Serviço na maioria dos outros serviços no Aplicativo. Portanto, esses serviços simplesmente chamariam authService.getCurrentUser () para obter o objeto. Se você tiver um lugar no seu código onde obtém uma referência ao mesmo Principal na sessão, basta configurá-lo como uma propriedade no seu bean com escopo de sessão.

cliff.meyers
fonte
1

Tente isto

Autenticação de autenticação = SecurityContextHolder.getContext (). GetAuthentication ();
String userName = authentication.getName ();

cherit
fonte
3
Chamar o método estático SecurityContextHolder.getContext () é exatamente o que eu estava reclamando na pergunta original. Você não respondeu nada.
Scott Bale
2
No entanto, é exatamente o que a documentação recomenda: static.springsource.org/spring-security/site/docs/3.0.x/… Então, o que você está conseguindo ao evitá-lo? Você está procurando uma solução complicada para um problema simples. Na melhor das hipóteses - você obtém o mesmo comportamento. Na pior das hipóteses, você recebe um bug ou falha de segurança.
Bob Kerns
2
@BobKerns Para testes, é mais fácil injetar a autenticação, em vez de colocá-la em um local de threads.
1

A melhor solução se você estiver usando o Spring 3 e precisar do principal autenticado em seu controlador é fazer algo assim:

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;

    @Controller
    public class KnoteController {
        @RequestMapping(method = RequestMethod.GET)
        public java.lang.String list(Model uiModel, UsernamePasswordAuthenticationToken authToken) {

            if (authToken instanceof UsernamePasswordAuthenticationToken) {
                user = (User) authToken.getPrincipal();
            }
            ...

    }
Marca
fonte
1
Por que você está executando a instância da verificação UsernamePasswordAuthenticationToken quando o parâmetro já é do tipo UsernamePasswordAuthenticationToken?
Scott Bale
(a instância authToken do nome de usuário da senha de autenticação) é equivalente funcional a if (authToken! = null). O último pode ser um pouco mais limpo, mas, caso contrário, não há diferença.
Mark
1

Estou usando a @AuthenticationPrincipalanotação nas @Controllerclasses e também nas @ControllerAdviceranotadas. Ex.:

@ControllerAdvice
public class ControllerAdvicer
{
    private static final Logger LOGGER = LoggerFactory.getLogger(ControllerAdvicer.class);


    @ModelAttribute("userActive")
    public UserActive currentUser(@AuthenticationPrincipal UserActive currentUser)
    {
        return currentUser;
    }
}

Onde UserActiveé a classe que eu uso para serviços de usuários registrados e se estende de org.springframework.security.core.userdetails.User. Algo como:

public class UserActive extends org.springframework.security.core.userdetails.User
{

    private final User user;

    public UserActive(User user)
    {
        super(user.getUsername(), user.getPasswordHash(), user.getGrantedAuthorities());
        this.user = user;
    }

     //More functions
}

Muito fácil.

EliuX
fonte
0

Defina Principalcomo uma dependência no seu método do controlador e o spring injetará o usuário autenticado atual no seu método na chamada.

Imrank
fonte
-2

Eu gosto de compartilhar minha maneira de dar suporte aos detalhes do usuário na página do freemarker. Tudo é muito simples e funciona perfeitamente!

Você só precisa colocar a solicitação de autenticação novamente default-target-url(página após o login do formulário) Este é o meu método Controler para essa página:

@RequestMapping(value = "/monitoring", method = RequestMethod.GET)
public ModelAndView getMonitoringPage(Model model, final HttpServletRequest request) {
    showRequestLog("monitoring");


    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    String userName = authentication.getName();
    //create a new session
    HttpSession session = request.getSession(true);
    session.setAttribute("username", userName);

    return new ModelAndView(catalogPath + "monitoring");
}

E este é o meu código ftl:

<@security.authorize ifAnyGranted="ROLE_ADMIN, ROLE_USER">
<p style="padding-right: 20px;">Logged in as ${username!"Anonymous" }</p>
</@security.authorize> 

E é isso, o nome de usuário aparecerá em todas as páginas após a autorização.

Sarja
fonte
Obrigado por tentar responder, mas o uso do método estático SecurityContextHolder.getContext () é exatamente o que eu queria evitar, e o motivo pelo qual fiz essa pergunta em primeiro lugar.
Scott Bale