Lorsque vous utilisez Spring Security, quelle est la bonne façon d'obtenir des informations sur le nom d'utilisateur actuel (c'est-à-dire SecurityContext) dans un bean?

288

J'ai une application Web Spring MVC qui utilise Spring Security. Je veux connaître le nom d'utilisateur de l'utilisateur actuellement connecté. J'utilise l'extrait de code ci-dessous. Est-ce la voie acceptée?

Je n'aime pas avoir un appel à une méthode statique à l'intérieur de ce contrôleur - qui va à l'encontre du but de Spring, à mon humble avis. Existe-t-il un moyen de configurer l'application pour que le SecurityContext actuel ou l'authentification actuelle soit injecté à la place?

  @RequestMapping(method = RequestMethod.GET)
  public ModelAndView showResults(final HttpServletRequest request...) {
    final String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
    ...
  }
Scott Bale
la source
Pourquoi n'avez-vous pas un contrôleur (contrôleur sécurisé) en tant que super classe, obtenez l'utilisateur du SecurityContext et définissez-le comme variable d'instance dans cette classe? De cette façon, lorsque vous étendez le contrôleur sécurisé, toute votre classe aura accès au principal utilisateur du contexte actuel.
Dehan de Croos

Réponses:

259

Si vous utilisez Spring 3 , la manière la plus simple est:

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

     final String currentUser = principal.getName();

 }
tsunade21
la source
69

Beaucoup de choses ont changé dans le monde du printemps depuis la réponse à cette question. Spring a simplifié le placement de l'utilisateur actuel dans un contrôleur. Pour les autres beans, Spring a adopté les suggestions de l'auteur et simplifié l'injection de 'SecurityContextHolder'. Plus de détails sont dans les commentaires.


C'est la solution avec laquelle j'ai fini par aller. Au lieu d'utiliser SecurityContextHolderdans mon contrôleur, je veux injecter quelque chose qui utilise SecurityContextHoldersous le capot mais qui résume cette classe de type singleton de mon code. Je n'ai trouvé aucun autre moyen que de rouler ma propre interface, comme ceci:

public interface SecurityContextFacade {

  SecurityContext getContext();

  void setContext(SecurityContext securityContext);

}

Maintenant, mon contrôleur (ou n'importe quel POJO) ressemblerait à ceci:

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
  }

}

Et, comme l'interface est un point de découplage, le test unitaire est simple. Dans cet exemple, j'utilise 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();
  }

}

L'implémentation par défaut de l'interface ressemble à ceci:

public class SecurityContextHolderFacade implements SecurityContextFacade {

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

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

}

Et, enfin, la configuration de production de Spring ressemble à ceci:

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

Il semble plus qu'un peu stupide que Spring, un conteneur d'injection de dépendance de toutes choses, n'a pas fourni un moyen d'injecter quelque chose de similaire. Je comprends que a SecurityContextHolderété hérité d'Acegi, mais quand même. Le fait est qu'ils sont si proches - si seulement SecurityContextHolderun getter pouvait obtenir l' SecurityContextHolderStrategyinstance sous-jacente (qui est une interface), vous pourriez l'injecter. En fait, j'ai même ouvert un dossier Jira à cet effet.

Une dernière chose - je viens de changer considérablement la réponse que j'avais ici auparavant. Consultez l'historique si vous êtes curieux mais, comme un collègue me l'a fait remarquer, ma réponse précédente ne fonctionnerait pas dans un environnement multi-thread. Le sous-jacent SecurityContextHolderStrategyutilisé par SecurityContextHolderest, par défaut, une instance de ThreadLocalSecurityContextHolderStrategy, qui stocke SecurityContexts dans a ThreadLocal. Par conséquent, ce n'est pas nécessairement une bonne idée d'injecter le SecurityContextdirectement dans un bean au moment de l'initialisation - il peut être nécessaire de le récupérer à ThreadLocalchaque fois, dans un environnement multi-thread, afin que le bon soit récupéré.

Scott Bale
la source
1
J'aime votre solution - c'est une utilisation intelligente du support de méthode d'usine dans Spring. Cela dit, cela fonctionne pour vous car l'objet contrôleur est limité à la demande Web. Si vous modifiiez la portée du bean du contrôleur de manière incorrecte, cela se briserait.
Paul Morie
2
Les deux commentaires précédents font référence à une ancienne réponse incorrecte que je viens de remplacer.
Scott Bale
12
Est-ce toujours la solution recommandée avec la version Spring actuelle? Je ne peux pas croire qu'il ait besoin de tant de code juste pour récupérer uniquement le nom d'utilisateur.
Ta Sas
6
Si vous utilisez Spring Security 3.0.x, ils ont implémenté ma suggestion dans le problème JIRA que j'ai enregistré jira.springsource.org/browse/SEC-1188 afin que vous puissiez maintenant injecter l'instance SecurityContextHolderStrategy (depuis SecurityContextHolder) directement dans votre bean via la norme Configuration du ressort.
Scott Bale
4
Veuillez voir la réponse tsunade21. Spring 3 vous permet désormais d'utiliser java.security.Principal comme argument de méthode dans votre contrôleur
Patrick
22

Je suis d'accord que devoir interroger le SecurityContext pour l'utilisateur actuel pue, cela semble être un moyen très anti-Spring pour gérer ce problème.

J'ai écrit une classe "d'aide" statique pour résoudre ce problème; c'est sale dans la mesure où c'est une méthode globale et statique, mais j'ai pensé de cette façon si nous changeons quelque chose lié à la sécurité, au moins je n'ai qu'à changer les détails en un seul endroit:

/**
* 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;
}
mat b
la source
22
il est aussi long que SecurityContextHolder.getContext (), et ce dernier est threadsafe car il conserve les détails de sécurité dans un threadLocal. Ce code ne maintient aucun état.
mat b
22

Pour le faire apparaître simplement dans vos pages JSP, vous pouvez utiliser la Librairie de balises de sécurité Spring:

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

Pour utiliser l'une des balises, vous devez déclarer la balise de sécurité dans votre JSP:

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

Ensuite, dans une page jsp, faites quelque chose comme ceci:

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

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

REMARQUE: Comme mentionné dans les commentaires de @ SBerg413, vous devrez ajouter

use-expressions = "true"

à la balise "http" dans la configuration security.xml pour que cela fonctionne.

Brad Parks
la source
Il semble que ce soit probablement la méthode approuvée par Spring Security!
Nick Spacek
3
Pour que cette méthode fonctionne, vous devez ajouter use-expressions = "true" à la balise http dans la configuration security.xml.
SBerg413
Merci @ SBerg413, je vais modifier ma réponse et ajouter votre clarification importante!
Brad Parks
14

Si vous utilisez Spring Security ver> = 3.2, vous pouvez utiliser l' @AuthenticationPrincipalannotation:

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

Voici CustomUserun objet personnalisé qui implémente UserDetailsqui est retourné par une coutume UserDetailsService.

Plus d'informations peuvent être trouvées dans le chapitre @AuthenticationPrincipal des documents de référence de Spring Security.

matsev
la source
13

J'obtiens l'utilisateur authentifié par HttpServletRequest.getUserPrincipal ();

Exemple:

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
la source
J'aime votre solution. Aux spécialistes Spring: est-ce une bonne solution sûre?
marioosh
Pas une bonne solution. Vous obtiendrez nullsi l'utilisateur est authentifié de manière anonyme ( http> anonymouséléments dans Spring Security XML). SecurityContextHolderou SecurityContextHolderStrategyest la bonne façon.
Nowaker
1
Donc, j'ai vérifié sinon null request.getUserPrincipal ()! = Null.
digz6666
Est nul dans le filtre
Alex78191
9

Au printemps 3+, vous avez les options suivantes.

Option 1 :

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

Option 2 :

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

Option 3:

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

}

Option 4: Fantaisie: consultez cette option pour plus de détails

public ModelAndView someRequestHandler(@ActiveUser User activeUser) {
  ...
}
Ferme
la source
1
Depuis la version 3.2, spring-security-web est fourni avec @CurrentUserce qui fonctionne comme la coutume @ActiveUserde votre lien.
Mike Partridge
@MikePartridge, je ne semble pas trouver ce que vous dites, aucun lien ?? ou plus d'informations ??
azerafati
1
Mon erreur - j'ai mal compris le javadoc Spring Security AuthenticationPrincipalArgumentResolver . Un exemple y est présenté @AuthenticationPrincipalavec une @CurrentUserannotation personnalisée . Depuis 3.2, nous n'avons pas besoin d'implémenter un résolveur d'arguments personnalisé comme dans la réponse liée. Cette autre réponse a plus de détails.
Mike Partridge
5

Oui, la statique est généralement mauvaise - généralement, mais dans ce cas, la statique est le code le plus sécurisé que vous puissiez écrire. Étant donné que le contexte de sécurité associe un principal au thread en cours d'exécution, le code le plus sécurisé accéderait au statique à partir du thread aussi directement que possible. Masquer l'accès derrière une classe wrapper qui est injectée fournit à un attaquant plus de points à attaquer. Ils n'auraient pas besoin d'accéder au code (qu'ils auraient du mal à changer si le pot était signé), ils ont juste besoin d'un moyen de remplacer la configuration, ce qui peut être fait au moment de l'exécution ou en glissant du XML sur le chemin de classe. Même l'utilisation de l'injection d'annotations dans le code signé serait remplaçable avec du XML externe. Un tel XML pourrait injecter le système en cours d'exécution avec un principal escroc.

Michael Bushe
la source
5

Je voudrais juste faire ceci:

request.getRemoteUser();
Dan
la source
1
Cela pourrait fonctionner, mais pas de manière fiable. De javadoc: "Le fait que le nom d'utilisateur soit envoyé avec chaque demande ultérieure dépend du navigateur et du type d'authentification." - download-llnw.oracle.com/javaee/6/api/javax/servlet/http/…
Scott Bale
3
Il s'agit en fait d'un moyen valide et très simple d'obtenir le nom d'utilisateur distant dans une application Web Spring Security. La chaîne de filtrage standard comprend un SecurityContextHolderAwareRequestFilterqui enveloppe la demande et implémente cet appel en accédant à SecurityContextHolder.
Shaun the Sheep du
4

Pour la dernière application Spring MVC que j'ai écrite, je n'ai pas injecté le support SecurityContext, mais j'avais un contrôleur de base sur lequel j'avais deux méthodes utilitaires liées à cela ... isAuthenticated () & getUsername (). En interne, ils font l'appel de méthode statique que vous avez décrit.

Au moins, il n'est en place qu'une seule fois si vous avez besoin d'un remaniement ultérieur.

RichH
la source
3

Vous pouvez utiliser l'approche Spring AOP. Par exemple, si vous avez un service, il doit connaître le principal actuel. Vous pouvez introduire une annotation personnalisée, c'est-à-dire @Principal, qui indique que ce service doit être dépendant du principal.

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

Ensuite, dans votre conseil, qui, je pense, doit étendre MethodBeforeAdvice, vérifiez que le service particulier a une annotation @Principal et injectez le nom principal, ou définissez-le sur 'ANONYMOUS' à la place.

Pavel Rodionov
la source
J'ai besoin d'accéder à Principal dans la classe de service. Pourriez-vous publier un exemple complet sur github? Je ne connais pas le printemps AOP, d'où la demande.
Rakesh Waghela
2

Le seul problème est que même après l'authentification avec Spring Security, le bean utilisateur / principal n'existe pas dans le conteneur, donc l'injection de dépendances sera difficile. Avant d'utiliser Spring Security, nous créions un bean à portée de session qui avait le principal actuel, l'injections dans un "AuthService", puis injections ce service dans la plupart des autres services de l'application. Donc, ces services appellent simplement authService.getCurrentUser () pour obtenir l'objet. Si vous avez un endroit dans votre code où vous obtenez une référence au même principal dans la session, vous pouvez simplement le définir comme une propriété sur votre bean à portée de session.

cliff.meyers
la source
1

Essaye ça

Authentification authentification = SecurityContextHolder.getContext (). GetAuthentication ();
String userName = authentication.getName ();

cherit
la source
3
Appeler la méthode statique SecurityContextHolder.getContext () est exactement ce dont je me plaignais dans la question d'origine. Tu n'as rien répondu.
Scott Bale
2
C'est cependant exactement ce que la documentation recommande: static.springsource.org/spring-security/site/docs/3.0.x/… Alors, que faites-vous en l'évitant? Vous cherchez une solution compliquée à un problème simple. Au mieux - vous obtenez le même comportement. Au pire, vous obtenez un bug ou une faille de sécurité.
Bob Kerns
2
@BobKerns Pour les tests, il est plus propre de pouvoir injecter l'authentification plutôt que de la mettre sur un thread local.
1

La meilleure solution si vous utilisez Spring 3 et avez besoin du principal authentifié dans votre contrôleur est de faire quelque chose comme ceci:

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();
            }
            ...

    }
marque
la source
1
Pourquoi effectuez-vous cette instance de vérification de UsernamePasswordAuthenticationToken lorsque le paramètre est déjà de type UsernamePasswordAuthenticationToken?
Scott Bale
(authToken instanceof UsernamePasswordAuthenticationToken) est un équivalent fonctionnel de if (authToken! = null). Ce dernier est peut-être un peu plus propre mais sinon il n'y a pas de différence.
Mark
1

J'utilise l' @AuthenticationPrincipalannotation dans les @Controllerclasses ainsi que dans les classes @ControllerAdvicerannotées. Ex.:

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


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

UserActiveest la classe que j'utilise pour les services des utilisateurs connectés, et s'étend de org.springframework.security.core.userdetails.User. Quelque chose comme:

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
}

Vraiment facile.

EliuX
la source
0

Définissez Principalcomme dépendance dans votre méthode de contrôleur et Spring injectera l'utilisateur authentifié actuel dans votre méthode lors de l'invocation.

Imrank
la source
-2

J'aime partager ma façon de prendre en charge les détails de l'utilisateur sur la page de marque de commerce Tout est très simple et fonctionne parfaitement!

Il vous suffit de placer une nouvelle demande d'authentification sur default-target-url(page après la connexion au formulaire). Voici ma méthode Controler pour cette page:

@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");
}

Et voici mon code ftl:

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

Et c'est tout, le nom d'utilisateur apparaîtra sur chaque page après autorisation.

Serge
la source
Merci d'avoir essayé de répondre, mais l'utilisation de la méthode statique SecurityContextHolder.getContext () est exactement ce que je voulais éviter, et la raison pour laquelle j'ai posé cette question en premier lieu.
Scott Bale