Cómo obtener detalles de usuario del usuario activo

170

En mis controladores, cuando necesito el usuario activo (conectado), estoy haciendo lo siguiente para obtener mi UserDetailsimplementación:

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

Funciona bien, pero creo que Spring podría hacer la vida más fácil en un caso como este. ¿Hay alguna manera de tener el UserDetailsautoconectado en el controlador o en el método?

Por ejemplo, algo como:

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

Pero en lugar de obtener el UsernamePasswordAuthenticationToken, me sale un UserDetailslugar?

Estoy buscando una solución elegante. ¿Algunas ideas?

El oso de Awnry
fuente

Respuestas:

226

Preámbulo: desde Spring-Security 3.2 hay una buena anotación @AuthenticationPrincipaldescrita al final de esta respuesta. Esta es la mejor manera de usar Spring-Security> = 3.2.

Cuando usted:

  • use una versión anterior de Spring-Security,
  • necesita cargar su objeto de usuario personalizado de la base de datos mediante alguna información (como el inicio de sesión o la identificación) almacenada en el principal o
  • desee aprender una HandlerMethodArgumentResolvero WebArgumentResolverpuede solucionar esto de una manera elegante, o simplemente quieren aprender un segundo plano detrás @AuthenticationPrincipaly AuthenticationPrincipalArgumentResolver(debido a que se basa en una HandlerMethodArgumentResolver)

luego siga leyendo; de lo contrario, solo use @AuthenticationPrincipaly agradezca a Rob Winch (Autor de @AuthenticationPrincipal) y Lukas Schmelzeisen (por su respuesta).

(Por cierto: mi respuesta es un poco más antigua (enero de 2012), por lo que fue Lukas Schmelzeisen quien surgió como el primero con la @AuthenticationPrincipalsolución de anotación basada en Spring Security 3.2.)


Entonces puedes usar en tu controlador

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

Eso está bien si lo necesitas una vez. Pero si lo necesita varias veces es feo porque contamina su controlador con detalles de infraestructura, eso normalmente debería estar oculto por el marco.

Entonces, lo que realmente puede desear es tener un controlador como este:

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

Por lo tanto, solo necesita implementar a WebArgumentResolver. Tiene un metodo

Object resolveArgument(MethodParameter methodParameter,
                   NativeWebRequest webRequest)
                   throws Exception

Eso obtiene la solicitud web (segundo parámetro) y debe devolver el Usersi se siente responsable del argumento del método (el primer parámetro).

Desde la primavera 3.1 hay un nuevo concepto llamado HandlerMethodArgumentResolver. Si usa Spring 3.1+, entonces debe usarlo. (Se describe en la siguiente sección de esta respuesta))

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

Debe definir la anotación personalizada: puede omitirla si cada instancia de usuario siempre debe tomarse del contexto de seguridad, pero nunca es un objeto de comando.

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

En la configuración solo necesita agregar esto:

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

@Ver: aprenda a personalizar los argumentos del método Spring MVC @Controller

Cabe señalar que si está utilizando Spring 3.1, recomiendan HandlerMethodArgumentResolver sobre WebArgumentResolver. - ver comentario de Jay


Lo mismo con HandlerMethodArgumentResolverSpring 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;
          }
     }
}

En la configuración, debe agregar esto

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

@Ver Aprovechar la interfaz Spring MVC 3.1 HandlerMethodArgumentResolver


Solución Spring-Security 3.2

Spring Security 3.2 (no confundir con Spring 3.2) tiene su propia solución integrada: @AuthenticationPrincipal( org.springframework.security.web.bind.annotation.AuthenticationPrincipal). Esto se describe muy bien en la respuesta de Lukas Schmelzeisen

Es solo escribir

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

Para que esto funcione, debe registrar el AuthenticationPrincipalArgumentResolver( org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver): ya sea "activando" @EnableWebMvcSecurityo registrando este bean dentro mvc:argument-resolvers, de la misma manera que lo describí con la solución Spring 3.1 anterior.

@ Consulte Spring Security 3.2 Reference, Capítulo 11.2. @AuthenticationPrincipal


Solución Spring-Security 4.0

Funciona como la solución Spring 3.2, pero en Spring 4.0 el @AuthenticationPrincipaly AuthenticationPrincipalArgumentResolverfue "movido" a otro paquete:

(Pero las clases antiguas en sus paquetes anteriores todavía existen, ¡así que no las mezcle!)

Es solo escribir

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

Para que esto funcione, debe registrar el ( org.springframework.security.web.method.annotation.) AuthenticationPrincipalArgumentResolver: ya sea "activando" @EnableWebMvcSecurityo registrando este bean dentro mvc:argument-resolvers, de la misma manera que lo describí con la solución Spring 3.1 anterior.

<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
fuente
Alternativamente, solo cree un bean que tenga un método getUserDetails () y @Autowire en su controlador.
sourcedelica
Al implementar esta solución, establecí un punto de interrupción en la parte superior de resolveArgument () pero mi aplicación nunca entra en el solucionador de argumentos web. Su configuración de primavera en el contexto del servlet no es el contexto raíz, ¿verdad?
Jay
@ Jay: esta configuración es parte del contexto raíz, no del contexto de servlet. - parece que me he olvidado de especificar la identificación (id="applicationConversionService")en el ejemplo
Ralph
11
Cabe señalar que si está utilizando Spring 3.1, recomiendan HandlerMethodArgumentResolver sobre WebArgumentResolver. Obtuve HandlerMethodArgumentResolver para trabajar configurándolo con <annotation-driven> en el contexto del servlet. Aparte de eso, implementé la respuesta publicada aquí y todo funciona muy bien
Jay
@sourcedelica ¿Por qué no simplemente crear un método estático?
Alex78191
66

Si bien Ralphs Answer ofrece una solución elegante, con Spring Security 3.2 ya no necesita implementar la suya ArgumentResolver.

Si tiene una UserDetailsimplementación CustomUser, simplemente puede hacer esto:

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

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

Consulte la documentación de seguridad de Spring: @AuthenticationPrincipal

Lukas Schmelzeisen
fuente
2
para aquellos a quienes no les gusta leer los enlaces provistos, esto tiene que ser habilitado por @EnableWebMvcSecurityXML o en XML:<mvc:annotation-driven> <mvc:argument-resolvers> <bean class="org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver" /> </mvc:argument-resolvers> </mvc:annotation-driven>
sodik
¿Cómo verificar si el customUseres nulo o no?
Sajad
if (customUser != null) { ... }
T3rm1
27

Spring Security está diseñado para funcionar con otros frameworks que no sean Spring, por lo tanto, no está estrechamente integrado con Spring MVC. Spring Security devuelve el Authenticationobjeto del HttpServletRequest.getUserPrincipal()método por defecto, así que eso es lo que obtienes como principal. Puede obtener su UserDetailsobjeto directamente de esto usando

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

Tenga en cuenta también que los tipos de objeto pueden variar según el mecanismo de autenticación utilizado (puede que no obtenga un UsernamePasswordAuthenticationToken, por ejemplo) y el Authenticationestrictamente no tiene que contener un UserDetails. Puede ser una cadena o cualquier otro tipo.

Si no desea llamar SecurityContextHolderdirectamente, el enfoque más elegante (que seguiría) es inyectar su propia interfaz de acceso al contexto de seguridad personalizada que se personaliza para satisfacer sus necesidades y tipos de objetos de usuario. Cree una interfaz, con los métodos relevantes, por ejemplo:

interface MySecurityAccessor {

    MyUserDetails getCurrentUser();

    // Other methods
}

Luego puede implementar esto accediendo a SecurityContextHolderen su implementación estándar, desacoplando por completo su código de Spring Security. Luego inyecte esto en los controladores que necesitan acceso a información de seguridad o información sobre el usuario actual.

El otro beneficio principal es que es fácil realizar implementaciones simples con datos fijos para la prueba, sin tener que preocuparse por poblar locales de subprocesos, etc.

Shaun la oveja
fuente
Estaba considerando este enfoque, pero no estaba seguro de a) cómo hacerlo exactamente de la manera correcta (tm) yb) si habría algún problema de subprocesamiento. ¿Estás seguro de que no habría problemas allí? Voy con el método de anotación publicado anteriormente, pero creo que esta sigue siendo una forma decente de hacerlo. Gracias por publicar. :)
The Awnry Bear
44
Es técnicamente lo mismo que acceder al SecurityContextHolder directamente desde su controlador, por lo que no debería haber problemas de subprocesos. Simplemente mantiene la llamada en un solo lugar y le permite inyectar fácilmente alternativas para la prueba. También puede reutilizar el mismo enfoque en otras clases no web que necesitan acceso a información de seguridad.
Shaun the Sheep
Te tengo ... eso es lo que estaba pensando. Este será un buen momento para volver si tengo problemas con las pruebas unitarias con el método de anotación, o si quiero disminuir el acoplamiento con Spring.
The Awnry Bear
@LukeTaylor, ¿podría explicar mejor este enfoque? Soy un poco nuevo en la primavera y la seguridad de la primavera, así que no entiendo cómo implementar esto. ¿Dónde implemento esto para que se pueda acceder? a mi UserServiceImpl?
Cu7l4ss
9

Implemente la HandlerInterceptorinterfaz y luego inyecte UserDetailsen cada solicitud que tenga un Modelo, de la siguiente manera:

@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());
        }
}
un tren
fuente
1
Gracias @atrain, eso es útil y elegante. Además, tuve que agregar el <mvc:interceptors>archivo de configuración de mi aplicación.
Eric
Es mejor obtener un usuario autorizado en la plantilla a través de spring-security-taglibs: stackoverflow.com/a/44373331/548473
Grigory Kislin
9

A partir de Spring Security versión 3.2, la funcionalidad personalizada que ha sido implementada por algunas de las respuestas anteriores existe de forma inmediata en forma de @AuthenticationPrincipalanotación respaldada por AuthenticationPrincipalArgumentResolver.

Un ejemplo simple de su uso es:

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

CustomUser debe ser asignable desde authentication.getPrincipal()

Aquí están los correspondientes Javadocs de AuthenticationPrincipal y AuthenticationPrincipalArgumentResolver

geoand
fuente
1
@nbro Cuando agregué la solución específica de la versión, ninguna de las otras soluciones se había actualizado para tener en cuenta esta solución
y
AuthenticationPrincipalArgumentResolver ahora está en desuso
Igor Donin el
5
@Controller
public abstract class AbstractController {
    @ModelAttribute("loggedUser")
    public User getLoggedUser() {
        return (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
}
Camarada
fuente
0

Y si necesita un usuario autorizado en plantillas (por ejemplo, JSP) use

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

Juntos con

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

Puede intentar esto: Al usar el Objeto de autenticación de Spring podemos obtener detalles del usuario en el método del controlador. A continuación se muestra el ejemplo, pasando el objeto de autenticación en el método del controlador junto con el argumento. Una vez que el usuario se autentica, los detalles se rellenan en el objeto de autenticación.

@GetMapping(value = "/mappingEndPoint") <ReturnType> methodName(Authentication auth) {
   String userName = auth.getName(); 
   return <ReturnType>;
}
Mirza Shujathullah
fuente
Por favor, elabore su respuesta.
Nikolai Shevchenko
He editado mi respuesta, podríamos usar el Objeto de autenticación que tiene detalles de usuario del usuario que ya se ha autenticado.
Mirza Shujathullah