Como obter detalhes do usuário do usuário ativo

170

Nos meus controladores, quando preciso do usuário ativo (conectado), faço o seguinte para obter minha UserDetailsimplementação:

User activeUser = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
log.debug(activeUser.getSomeCustomField());

Funciona bem, mas eu acho que a Spring poderia facilitar a vida em um caso como esse. Existe uma maneira de UserDetailsconectar automaticamente o controlador ao método ou ao método?

Por exemplo, algo como:

public ModelAndView someRequestHandler(Principal principal) { ... }

Mas em vez de obter o UsernamePasswordAuthenticationToken, recebo um em UserDetailsvez disso?

Estou procurando uma solução elegante. Alguma ideia?

O Urso Awnry
fonte

Respostas:

226

Preâmbulo: Desde o Spring-Security 3.2, há uma boa anotação @AuthenticationPrincipaldescrita no final desta resposta. Este é o melhor caminho a seguir quando você usa Spring-Security> = 3.2.

Quando você:

  • use uma versão mais antiga do Spring-Security,
  • É necessário carregar seu objeto de usuário personalizado do banco de dados com algumas informações (como o login ou o ID) armazenadas no principal ou
  • deseja aprender como HandlerMethodArgumentResolverou WebArgumentResolverpode resolver isso de uma maneira elegante ou apenas deseja aprender os antecedentes @AuthenticationPrincipale AuthenticationPrincipalArgumentResolver(porque é baseado em HandlerMethodArgumentResolver)

depois continue lendo - senão use @AuthenticationPrincipale agradeça a Rob Winch (autor de @AuthenticationPrincipal) e Lukas Schmelzeisen (por sua resposta).

(BTW: Minha resposta é um pouco mais antiga (janeiro de 2012), por isso foi Lukas Schmelzeisen que surgiu como a primeira com a @AuthenticationPrincipalsolução de anotação baseada em Spring Security 3.2.)


Então você pode usar no seu controlador

public ModelAndView someRequestHandler(Principal principal) {
   User activeUser = (User) ((Authentication) principal).getPrincipal();
   ...
}

Tudo bem se você precisar uma vez. Mas se você precisar dele várias vezes, é feio porque polui seu controlador com detalhes de infraestrutura, que normalmente devem estar ocultos pela estrutura.

Então, o que você pode realmente querer é ter um controlador como este:

public ModelAndView someRequestHandler(@ActiveUser User activeUser) {
   ...
}

Portanto, você só precisa implementar a WebArgumentResolver. Tem um método

Object resolveArgument(MethodParameter methodParameter,
                   NativeWebRequest webRequest)
                   throws Exception

Isso obtém a solicitação da web (segundo parâmetro) e deve retornar o Userif se sentir responsável pelo argumento do método (o primeiro parâmetro).

Desde a Primavera 3.1, existe um novo conceito chamado HandlerMethodArgumentResolver. Se você usa o Spring 3.1+, deve usá-lo. (É descrito na próxima seção desta resposta))

public class CurrentUserWebArgumentResolver implements WebArgumentResolver{

   Object resolveArgument(MethodParameter methodParameter, NativeWebRequest webRequest) {
        if(methodParameter is for type User && methodParameter is annotated with @ActiveUser) {
           Principal principal = webRequest.getUserPrincipal();
           return (User) ((Authentication) principal).getPrincipal();
        } else {
           return WebArgumentResolver.UNRESOLVED;
        }
   }
}

É necessário definir a anotação personalizada - você pode ignorá-la se todas as instâncias do usuário sempre forem retiradas do contexto de segurança, mas nunca forem um objeto de comando.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ActiveUser {}

Na configuração, você só precisa adicionar isso:

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"
    id="applicationConversionService">
    <property name="customArgumentResolver">
        <bean class="CurrentUserWebArgumentResolver"/>
    </property>
</bean>

@Veja: Aprenda a personalizar argumentos do método Spring MVC @Controller

Observe que, se você estiver usando o Spring 3.1, eles recomendam HandlerMethodArgumentResolver sobre WebArgumentResolver. - ver comentário de Jay


O mesmo HandlerMethodArgumentResolverpara o Spring 3.1+

public class CurrentUserHandlerMethodArgumentResolver
                               implements HandlerMethodArgumentResolver {

     @Override
     public boolean supportsParameter(MethodParameter methodParameter) {
          return
              methodParameter.getParameterAnnotation(ActiveUser.class) != null
              && methodParameter.getParameterType().equals(User.class);
     }

     @Override
     public Object resolveArgument(MethodParameter methodParameter,
                         ModelAndViewContainer mavContainer,
                         NativeWebRequest webRequest,
                         WebDataBinderFactory binderFactory) throws Exception {

          if (this.supportsParameter(methodParameter)) {
              Principal principal = webRequest.getUserPrincipal();
              return (User) ((Authentication) principal).getPrincipal();
          } else {
              return WebArgumentResolver.UNRESOLVED;
          }
     }
}

Na configuração, você precisa adicionar este

<mvc:annotation-driven>
      <mvc:argument-resolvers>
           <bean class="CurrentUserHandlerMethodArgumentResolver"/>         
      </mvc:argument-resolvers>
 </mvc:annotation-driven>

@Veja Aproveitando a interface HandlerMethodArgumentResolver do Spring MVC 3.1


Solução Spring-Security 3.2

O Spring Security 3.2 (não confunda com o Spring 3.2) tem solução própria de compilação: @AuthenticationPrincipal( org.springframework.security.web.bind.annotation.AuthenticationPrincipal). Isso está bem descrito na resposta de Lukas Schmelzeisen

Está apenas escrevendo

ModelAndView someRequestHandler(@AuthenticationPrincipal User activeUser) {
    ...
 }

Para que isso funcione, é necessário registrar o AuthenticationPrincipalArgumentResolver( org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver): "ativando" @EnableWebMvcSecurityou registrando este bean mvc:argument-resolvers- da mesma forma que o descrevi na solução Spring 3.1 acima.

Consulte a Referência do Spring Security 3.2, Capítulo 11.2. @AuthenticationPrincipal


Solução Spring-Security 4.0

Funciona como a solução Spring 3.2, mas no Spring 4.0 o @AuthenticationPrincipale AuthenticationPrincipalArgumentResolverfoi "movido" para outro pacote:

(Mas as classes antigas em suas antigas embalagens ainda existem, portanto não as misture!)

Está apenas escrevendo

import org.springframework.security.core.annotation.AuthenticationPrincipal;
ModelAndView someRequestHandler(@AuthenticationPrincipal User activeUser) {
    ...
}

Para que isso funcione, é necessário registrar o ( org.springframework.security.web.method.annotation.) AuthenticationPrincipalArgumentResolver: "ativando" @EnableWebMvcSecurityou registrando esse bean mvc:argument-resolvers- da mesma forma que o descrevi na solução Spring 3.1 acima.

<mvc:annotation-driven>
    <mvc:argument-resolvers>
        <bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" />
    </mvc:argument-resolvers>
</mvc:annotation-driven>

@ Consulte Spring Security 5.0 Reference, Capítulo 39.3 @AuthenticationPrincipal

Ralph
fonte
Como alternativa, basta criar um bean que tenha o método getUserDetails () e o @Autowire no seu controlador.
precisa saber é o seguinte
Implementando esta solução, defino um ponto de interrupção na parte superior do resolveArgument (), mas meu aplicativo nunca entra no resolvedor de argumentos da Web. Sua configuração de mola no contexto do servlet não no contexto raiz, certo?
21412 Jay
@ Jay: Essa configuração faz parte do contexto raiz, não do contexto do servlet. - costuras que eu esqueça de especificar o id (id="applicationConversionService")no exemplo
Ralph
11
Observe que, se você estiver usando o Spring 3.1, eles recomendam HandlerMethodArgumentResolver sobre WebArgumentResolver. Coloquei HandlerMethodArgumentResolver para trabalhar configurando-o com <annotation-driven> no contexto do servlet. Fora isso, eu implementei a resposta, conforme publicado aqui, e tudo funciona muito bem #
272 Jay Jay
@sourcedelica Por que não criar apenas um método estático?
Alex78191
66

Enquanto o Ralphs Answer fornece uma solução elegante, com o Spring Security 3.2, você não precisa mais implementar o seu ArgumentResolver.

Se você possui uma UserDetailsimplementação CustomUser, basta fazer o seguinte:

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) {

    // .. find messages for this User and return them...
}

Consulte a documentação de segurança do Spring: @AuthenticationPrincipal

Lukas Schmelzeisen
fonte
2
para aqueles que não gostam de ler links fornecidos, este tem de ser activado quer por @EnableWebMvcSecurityou em XML:<mvc:annotation-driven> <mvc:argument-resolvers> <bean class="org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver" /> </mvc:argument-resolvers> </mvc:annotation-driven>
sodik
Como verificar se o customUseré nulo ou não?
Sajad 30/01/16
if (customUser != null) { ... }
precisa saber é o seguinte
27

O Spring Security se destina a trabalhar com outras estruturas que não são do Spring, portanto, não está totalmente integrado ao Spring MVC. O Spring Security retorna o Authenticationobjeto do HttpServletRequest.getUserPrincipal()método por padrão, e é isso que você obtém como principal. Você pode obter seu UserDetailsobjeto diretamente disso usando

UserDetails ud = ((Authentication)principal).getPrincipal()

Observe também que os tipos de objetos podem variar dependendo do mecanismo de autenticação usado (você pode não obter um UsernamePasswordAuthenticationToken, por exemplo) e Authenticationnão precisa estritamente conter um UserDetails. Pode ser uma string ou qualquer outro tipo.

Se você não deseja ligar SecurityContextHolderdiretamente, a abordagem mais elegante (que eu seguiria) é injetar sua própria interface acessora de contexto de segurança personalizada, personalizada para atender às suas necessidades e tipos de objeto do usuário. Crie uma interface, com os métodos relevantes, por exemplo:

interface MySecurityAccessor {

    MyUserDetails getCurrentUser();

    // Other methods
}

Em seguida, você pode implementar isso acessando o SecurityContextHolderem sua implementação padrão, dissociando completamente seu código do Spring Security. Em seguida, injete isso nos controladores que precisam acessar informações de segurança ou informações sobre o usuário atual.

O outro principal benefício é que é fácil fazer implementações simples com dados fixos para teste, sem ter que se preocupar com o preenchimento de locais de encadeamento e assim por diante.

Shaun the Sheep
fonte
Eu estava considerando essa abordagem, mas não tinha certeza a) como exatamente fazê-lo da maneira certa (tm) eb) se houvesse algum problema de encadeamento. Você tem certeza de que não haveria problemas lá? Vou seguir o método de anotação postado acima, mas acho que ainda é uma maneira decente de fazer isso. Obrigado por publicar. :)
The Awnry Bear
4
É tecnicamente o mesmo que acessar o SecurityContextHolder diretamente do seu controlador, portanto, não deve haver problemas de encadeamento. Apenas mantém a ligação em um único local e permite que você injete facilmente alternativas para testes. Você também pode reutilizar a mesma abordagem em outras classes que não são da Web que precisam acessar informações de segurança.
Shaun the Sheep
Entendi ... era isso que eu estava pensando. Será uma boa idéia para voltar se eu tiver problemas com o teste de unidade com o método de anotação ou se quiser diminuir o acoplamento com o Spring.
The Bear Awnry
@LukeTaylor, por favor, explique melhor essa abordagem, sou um pouco novo em segurança de primavera e primavera, então não entendo muito bem como implementar isso. Onde eu implemento isso para que ele seja acessado? para o meu UserServiceImpl?
Cu7l4ss
9

Implemente a HandlerInterceptorinterface e injete a UserDetailsem cada solicitação que possui um Modelo, da seguinte maneira:

@Component 
public class UserInterceptor implements HandlerInterceptor {
    ....other methods not shown....
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if(modelAndView != null){
            modelAndView.addObject("user", (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal());
        }
}
um trem
fonte
1
Obrigado @atrain, é útil e elegante. Além disso, eu tive que adicionar o <mvc:interceptors>arquivo de configuração do meu aplicativo.
Eric
É melhor obter autorização do usuário no modelo de via spring-security-taglibs: stackoverflow.com/a/44373331/548473
Grigory Kislin
9

Começando com o Spring Security versão 3.2, a funcionalidade personalizada que foi implementada por algumas das respostas mais antigas existe imediatamente, na forma da @AuthenticationPrincipalanotação suportada AuthenticationPrincipalArgumentResolver.

Um exemplo simples de seu uso é:

@Controller
public class MyController {
   @RequestMapping("/user/current/show")
   public String show(@AuthenticationPrincipal CustomUser customUser) {
        // do something with CustomUser
       return "view";
   }
}

O CustomUser precisa ser atribuído a partir de authentication.getPrincipal()

Aqui estão os Javadocs correspondentes de AuthenticationPrincipal e AuthenticationPrincipalArgumentResolver

geo e
fonte
1
@nbro Quando adicionei a solução específica da versão, nenhuma das outras soluções foi atualizada para levar em conta essa solução
geoand
O AuthenticationPrincipalArgumentResolver agora está obsoleto
Igor Donin
5
@Controller
public abstract class AbstractController {
    @ModelAttribute("loggedUser")
    public User getLoggedUser() {
        return (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
}
Camarada
fonte
0

E se você precisar de um usuário autorizado em modelos (por exemplo, JSP), use

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<sec:authentication property="principal.yourCustomField"/>

junto com

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-taglibs</artifactId>
        <version>${spring-security.version}</version>
    </dependency>
Grigory Kislin
fonte
0

Você pode tentar o seguinte: Usando o Authentication Object da Spring, podemos obter detalhes do usuário no método do controlador. Abaixo está o exemplo, passando o objeto de autenticação no método do controlador junto com o argumento. Uma vez que o usuário é autenticado, os detalhes são preenchidos no objeto de autenticação.

@GetMapping(value = "/mappingEndPoint") <ReturnType> methodName(Authentication auth) {
   String userName = auth.getName(); 
   return <ReturnType>;
}
Mirza Shujathullah
fonte
Por favor, elabore sua resposta.
Nikolai Shevchenko
Eu editei minha resposta, poderíamos usar o objeto de autenticação que possui detalhes do usuário que já se autenticou.
Mirza Shujathullah 27/09/19