Cuando se usa Spring Security, ¿cuál es la forma correcta de obtener información de nombre de usuario actual (es decir, SecurityContext) en un bean?

288

Tengo una aplicación web Spring MVC que usa Spring Security. Quiero saber el nombre de usuario del usuario actualmente conectado. Estoy usando el fragmento de código que figura a continuación. ¿Es esta la forma aceptada?

No me gusta tener una llamada a un método estático dentro de este controlador, que anula todo el propósito de Spring, en mi humilde opinión. ¿Hay alguna forma de configurar la aplicación para que se inyecte el SecurityContext actual o la Autenticación actual?

  @RequestMapping(method = RequestMethod.GET)
  public ModelAndView showResults(final HttpServletRequest request...) {
    final String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
    ...
  }
Scott Bale
fuente
¿Por qué no tiene un controlador (controlador seguro) como una superclase para obtener el usuario del SecurityContext y establecerlo como una variable de instancia dentro de esa clase? De esta manera, cuando extienda el controlador seguro, toda su clase tendrá acceso al usuario principal del contexto actual.
Dehan de Croos

Respuestas:

259

Si está utilizando Spring 3 , la forma más fácil es:

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

     final String currentUser = principal.getName();

 }
tsunade21
fuente
69

Mucho ha cambiado en el mundo de la primavera desde que se respondió esta pregunta. Spring ha simplificado la obtención del usuario actual en un controlador. Para otros beans, Spring adoptó las sugerencias del autor y simplificó la inyección de 'SecurityContextHolder'. Más detalles están en los comentarios.


Esta es la solución con la que terminé yendo. En lugar de usar SecurityContextHolderen mi controlador, quiero inyectar algo que se usa SecurityContextHolderdebajo del capó pero abstrae esa clase de tipo singleton de mi código. No he encontrado otra manera de hacer esto que no sea rodar mi propia interfaz, así:

public interface SecurityContextFacade {

  SecurityContext getContext();

  void setContext(SecurityContext securityContext);

}

Ahora, mi controlador (o cualquier POJO) se vería así:

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
  }

}

Y, debido a que la interfaz es un punto de desacoplamiento, las pruebas unitarias son sencillas. En este ejemplo uso 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();
  }

}

La implementación predeterminada de la interfaz se ve así:

public class SecurityContextHolderFacade implements SecurityContextFacade {

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

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

}

Y, finalmente, la producción Spring config se ve así:

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

Parece más que un poco tonto que Spring, un contenedor de inyección de dependencia de todas las cosas, no haya proporcionado una forma de inyectar algo similar. Entiendo que SecurityContextHolderfue heredado de acegi, pero aún así. El hecho es que están tan cerca: si solo SecurityContextHoldertuviera un getter para obtener la SecurityContextHolderStrategyinstancia subyacente (que es una interfaz), podría inyectar eso. De hecho, incluso abrí un problema de Jira en ese sentido.

Una última cosa: acabo de cambiar sustancialmente la respuesta que tenía aquí antes. Verifique el historial si tiene curiosidad, pero, como me señaló un compañero de trabajo, mi respuesta anterior no funcionaría en un entorno de subprocesos múltiples. El subyacente SecurityContextHolderStrategyutilizado por SecurityContextHolderes, por defecto, una instancia de ThreadLocalSecurityContextHolderStrategy, que almacena SecurityContexts en a ThreadLocal. Por lo tanto, no es necesariamente una buena idea inyectar SecurityContextdirectamente en un bean en el momento de la inicialización; es posible que deba recuperarse de ThreadLocalcada vez, en un entorno de subprocesos múltiples, de modo que se recupere el correcto.

Scott Bale
fuente
1
Me gusta su solución: es un uso inteligente del soporte del método de fábrica en Spring. Dicho esto, esto funciona para usted porque el objeto del controlador está dentro del alcance de la solicitud web. Si cambia el alcance del bean controlador de forma incorrecta, esto se rompería.
Paul Morie
2
Los dos comentarios anteriores se refieren a una respuesta antigua e incorrecta que acabo de reemplazar.
Scott Bale
12
¿Sigue siendo la solución recomendada con la versión actual de Spring? No puedo creer que necesite tanto código solo para recuperar solo el nombre de usuario.
Ta Sas
66
Si está utilizando Spring Security 3.0.x, implementaron mi sugerencia en el problema JIRA que registré jira.springsource.org/browse/SEC-1188 para que ahora pueda inyectar la instancia de SecurityContextHolderStrategy (desde SecurityContextHolder) directamente en su bean a través del estándar Configuración de primavera.
Scott Bale
44
Por favor, vea la respuesta de tsunade21. Spring 3 ahora le permite usar java.security.Principal como argumento de método en su controlador
Patrick
22

Estoy de acuerdo en que tener que consultar el SecurityContext para el usuario actual apesta, parece una forma muy poco flexible de manejar este problema.

Escribí una clase estática de "ayuda" para tratar este problema; está sucio porque es un método global y estático, pero pensé de esta manera si cambiamos algo relacionado con la Seguridad, al menos solo tengo que cambiar los detalles en un solo 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;
}
mate b
fuente
22
es tan largo como SecurityContextHolder.getContext (), y este último es seguro ya que mantiene los detalles de seguridad en un threadLocal. Este código no mantiene estado.
mate b
22

Para que se muestre en sus páginas JSP, puede usar Spring Security Tag Lib:

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

Para usar cualquiera de las etiquetas, debe tener el taglib de seguridad declarado en su JSP:

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

Luego, en una página jsp, haga algo como esto:

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

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

NOTA: Como se menciona en los comentarios de @ SBerg413, deberá agregar

use-expressions = "true"

a la etiqueta "http" en la configuración de security.xml para que esto funcione.

Brad Parks
fuente
¡Esto parece ser probablemente la forma aprobada por Spring Security!
Nick Spacek
3
Para que este método funcione, debe agregar use-expressions = "true" a la etiqueta http en la configuración de security.xml.
SBerg413
¡Gracias @ SBerg413, editaré mi respuesta y agregaré su aclaración importante!
Brad Parks
14

Si está utilizando Spring Security ver> = 3.2, puede usar la @AuthenticationPrincipalanotación:

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

Aquí, CustomUserhay un objeto personalizado que implementa UserDetailsque es devuelto por una costumbre UserDetailsService.

Puede encontrar más información en el capítulo @AuthenticationPrincipal de los documentos de referencia de Spring Security.

matsev
fuente
13

HttpServletRequest.getUserPrincipal () obtiene un usuario autenticado;

Ejemplo:

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
fuente
Me gusta tu solución. Para los especialistas de Spring: ¿es una buena solución segura?
marioosh
No es una buena solución. Obtendrá nullsi el usuario se autentica de forma anónima ( http> anonymouselementos en Spring Security XML). SecurityContextHoldero SecurityContextHolderStrategyes la forma correcta.
Nowaker
1
Así que he comprobado si no es nulo request.getUserPrincipal ()! = Null.
digz6666
Es nulo en el filtro
Alex78191
9

En Spring 3+ tienes las siguientes opciones.

Opción 1 :

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

Opcion 2 :

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

Opcion 3:

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

}

Opción 4: Fantasía: mira esto para más detalles

public ModelAndView someRequestHandler(@ActiveUser User activeUser) {
  ...
}
Granja
fuente
1
Desde 3.2, viene spring-security-web, @CurrentUserque funciona como la costumbre @ActiveUserde su enlace.
Mike Partridge
@ MikePartridge, parece que no encuentro lo que estás diciendo, ¿algún enlace? o mas informacion ??
azerafati
1
Mi error: no entendí el Spring Security AuthenticationPrincipalArgumentResolver javadoc. Allí se muestra un ejemplo @AuthenticationPrincipalcon una @CurrentUseranotación personalizada . Desde 3.2 no necesitamos implementar un solucionador de argumentos personalizado como en la respuesta vinculada. Esta otra respuesta tiene más detalles.
Mike Partridge
5

Sí, las estadísticas son generalmente malas, generalmente, pero en este caso, la estática es el código más seguro que puede escribir. Dado que el contexto de seguridad asocia un Principal con el subproceso que se está ejecutando actualmente, el código más seguro accedería a la estática del subproceso lo más directamente posible. Ocultar el acceso detrás de una clase de contenedor que se inyecta proporciona al atacante más puntos para atacar. No necesitarían acceso al código (que les costaría mucho cambiar si se firmara el jar), solo necesitan una forma de anular la configuración, que se puede hacer en tiempo de ejecución o deslizar algún XML en el classpath. Incluso el uso de la inyección de anotaciones en el código firmado sería reemplazable con XML externo. Tal XML podría inyectar al sistema en ejecución con un principal falso.

Michael Bushe
fuente
5

Yo solo haría esto:

request.getRemoteUser();
Dan
fuente
1
Eso podría funcionar, pero no de manera confiable. De javadoc: "Si el nombre de usuario se envía con cada solicitud posterior depende del navegador y el tipo de autenticación". - download-llnw.oracle.com/javaee/6/api/javax/servlet/http/…
Scott Bale
3
Esta es en realidad una forma válida y muy simple de obtener el nombre de usuario remoto en una aplicación web Spring Security. La cadena de filtros estándar incluye una SecurityContextHolderAwareRequestFilterque envuelve la solicitud e implementa esta llamada accediendo a SecurityContextHolder.
Shaun the Sheep
4

Para la última aplicación Spring MVC que escribí, no inyecté el soporte de SecurityContext, pero tenía un controlador base que tenía dos métodos de utilidad relacionados con esto ... isAuthenticated () & getUsername (). Internamente hacen la llamada al método estático que describió.

Al menos, solo está en el lugar una vez si necesita refactorizar más tarde.

RichH
fuente
3

Podrías usar el enfoque Spring AOP. Por ejemplo, si tiene algún servicio, necesita saber el director actual. Puede introducir anotaciones personalizadas, es decir, @Principal, que indican que este Servicio debe depender del director.

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

Luego, en su consejo, que creo que necesita extender MethodBeforeAdvice, verifique que el servicio en particular tenga una anotación @Principal e inyecte el nombre Principal, o configúrelo en 'ANÓNIMO' en su lugar.

Pavel Rodionov
fuente
Necesito acceder a Principal dentro de la clase de servicio. ¿Podría publicar un ejemplo completo en github? No sé Spring AOP, de ahí la solicitud.
Rakesh Waghela
2

El único problema es que incluso después de autenticarse con Spring Security, el bean de usuario / principal no existe en el contenedor, por lo que la inyección de dependencias será difícil. Antes de usar Spring Security, creábamos un bean de ámbito de sesión que tenía el Principal actual, lo inyectaba en un "AuthService" y luego inyectaba ese Servicio en la mayoría de los otros servicios de la Aplicación. Entonces, esos Servicios simplemente llamarían a authService.getCurrentUser () para obtener el objeto. Si tiene un lugar en su código donde obtiene una referencia al mismo Principal en la sesión, simplemente puede establecerlo como una propiedad en su bean de ámbito de sesión.

acantilado
fuente
1

Prueba esto

Autenticación autenticación = SecurityContextHolder.getContext (). GetAuthentication ();
Cadena userName = autenticacion.getName ();

heredar
fuente
3
Llamar al método estático SecurityContextHolder.getContext () es exactamente de lo que me estaba quejando en la pregunta original. No has respondido nada.
Scott Bale
2
Sin embargo, es exactamente lo que recomienda la documentación: static.springsource.org/spring-security/site/docs/3.0.x/… Entonces, ¿qué está logrando al evitarlo? Estás buscando una solución complicada para un problema simple. En el mejor de los casos, obtienes el mismo comportamiento. En el peor de los casos, obtienes un error o un agujero de seguridad.
Bob Kerns el
2
@BobKerns Para las pruebas, es más limpio poder inyectar la autenticación en lugar de colocarla en un hilo local.
1

La mejor solución si está utilizando Spring 3 y necesita el principal autenticado en su controlador es hacer algo como esto:

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
fuente
1
¿Por qué está haciendo esa instancia de comprobación de Nombre de usuario Contraseña Autenticación Tomado cuando el parámetro ya es de tipo Nombre de usuario Contraseña Autenticación?
Scott Bale
(authToken instanceof UsernamePasswordAuthenticationToken) es equivalente funcional de if (authToken! = null). Este último podría ser un poco más limpio, pero de lo contrario no hay diferencia.
Mark
1

Estoy usando la @AuthenticationPrincipalanotación en las @Controllerclases, así como en las @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;
    }
}

¿Dónde UserActiveestá la clase que uso para los servicios de usuarios registrados y se extiende desde 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
}

Realmente fácil.

EliuX
fuente
0

Defínalo Principalcomo una dependencia en su método de controlador y spring inyectará al usuario autenticado actual en su método en la invocación.

Imrank
fuente
-2

Me gusta compartir mi forma de apoyar los detalles del usuario en la página de Freemarker. ¡Todo es muy simple y funciona perfectamente!

Solo tiene que colocar la solicitud de autenticación en default-target-url(página después del inicio de sesión del formulario) Este es mi método Controler para esa 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");
}

Y este es mi código ftl:

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

Y eso es todo, el nombre de usuario aparecerá en cada página después de la autorización.

Sarga
fuente
Gracias por intentar responder, pero el uso del método estático SecurityContextHolder.getContext () es exactamente lo que quería evitar, y la razón por la que hice esta pregunta en primer lugar.
Scott Bale