Wie kann bei Verwendung von Spring Security der aktuelle Benutzername (dh SecurityContext) in einer Bean abgerufen werden?

288

Ich habe eine Spring MVC-Webanwendung, die Spring Security verwendet. Ich möchte den Benutzernamen des aktuell angemeldeten Benutzers wissen. Ich verwende das unten angegebene Code-Snippet. Ist das der akzeptierte Weg?

Ich mag es nicht, eine statische Methode in diesem Controller aufzurufen - das macht den ganzen Zweck von Spring zunichte, IMHO. Gibt es eine Möglichkeit, die App so zu konfigurieren, dass stattdessen der aktuelle SecurityContext oder die aktuelle Authentifizierung injiziert wird?

  @RequestMapping(method = RequestMethod.GET)
  public ModelAndView showResults(final HttpServletRequest request...) {
    final String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
    ...
  }
Scott Bale
quelle
Warum haben Sie keinen Controller (sicheren Controller) als Superklasse, um den Benutzer aus dem SecurityContext zu holen und ihn als Instanzvariable innerhalb dieser Klasse festzulegen? Auf diese Weise hat Ihre gesamte Klasse beim Erweitern des sicheren Controllers Zugriff auf das Benutzerprinzip des aktuellen Kontexts.
Dehan de Croos

Antworten:

259

Wenn Sie Spring 3 verwenden , ist der einfachste Weg:

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

     final String currentUser = principal.getName();

 }
Tsunade21
quelle
69

Seit der Beantwortung dieser Frage hat sich in der Frühlingswelt viel verändert. Spring hat es vereinfacht, den aktuellen Benutzer in eine Steuerung zu integrieren. Für andere Bohnen hat Spring die Vorschläge des Autors übernommen und die Injektion von 'SecurityContextHolder' vereinfacht. Weitere Details finden Sie in den Kommentaren.


Dies ist die Lösung, mit der ich mich am Ende entschieden habe. Anstatt SecurityContextHolderin meinem Controller zu verwenden, möchte ich etwas SecurityContextHoldereinfügen , das unter der Haube verwendet wird, aber diese singletonähnliche Klasse von meinem Code abstrahiert. Ich habe keine andere Möglichkeit gefunden, als meine eigene Benutzeroberfläche zu rollen:

public interface SecurityContextFacade {

  SecurityContext getContext();

  void setContext(SecurityContext securityContext);

}

Nun würde mein Controller (oder was auch immer POJO) so aussehen:

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
  }

}

Und da die Schnittstelle ein Entkopplungspunkt ist, ist das Testen von Einheiten unkompliziert. In diesem Beispiel verwende ich 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();
  }

}

Die Standardimplementierung der Schnittstelle sieht folgendermaßen aus:

public class SecurityContextHolderFacade implements SecurityContextFacade {

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

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

}

Und schließlich sieht die Produktions-Spring-Konfiguration folgendermaßen aus:

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

Es scheint mehr als ein bisschen albern, dass Spring, ausgerechnet ein Container für Abhängigkeitsinjektionen, keine Möglichkeit bietet, etwas Ähnliches zu injizieren. Ich verstehe, SecurityContextHolderwurde von Acegi geerbt, aber immer noch. Die Sache ist, dass sie so nah beieinander sind - wenn Sie nur SecurityContextHoldereinen Getter hätten, um die zugrunde liegende SecurityContextHolderStrategyInstanz (die eine Schnittstelle ist) zu erhalten, könnten Sie das injizieren. Tatsächlich habe ich sogar eine entsprechende Jira-Ausgabe eröffnet .

Eine letzte Sache - ich habe gerade die Antwort, die ich hier zuvor hatte, grundlegend geändert. Überprüfen Sie den Verlauf, wenn Sie neugierig sind, aber, wie ein Mitarbeiter mir sagte, würde meine vorherige Antwort in einer Multithread-Umgebung nicht funktionieren. Die zugrundeliegende SecurityContextHolderStrategygebrauchte SecurityContextHolderist standardmäßig eine Instanz ThreadLocalSecurityContextHolderStrategy, die speichert SecurityContexts in ein ThreadLocal. Daher ist es nicht unbedingt eine gute Idee, das SecurityContextBean zum Zeitpunkt der Initialisierung direkt in eine Bean zu injizieren. Möglicherweise muss es ThreadLocaljedes Mal in einer Umgebung mit mehreren Threads abgerufen werden, damit das richtige abgerufen wird.

Scott Bale
quelle
1
Ich mag Ihre Lösung - es ist eine clevere Verwendung der Factory-Method-Unterstützung im Frühjahr. Dies funktioniert jedoch für Sie, da das Controller-Objekt auf die Webanforderung beschränkt ist. Wenn Sie den Bereich der Controller-Bean falsch geändert haben, würde dies nicht funktionieren.
Paul Morie
2
Die beiden vorherigen Kommentare beziehen sich auf eine alte, falsche Antwort, die ich gerade ersetzt habe.
Scott Bale
12
Ist dies mit der aktuellen Spring-Version immer noch die empfohlene Lösung? Ich kann nicht glauben, dass es so viel Code braucht, um nur den Benutzernamen abzurufen.
Ta Sas
6
Wenn Sie Spring Security 3.0.x verwenden, haben sie meinen Vorschlag in der JIRA-Ausgabe implementiert, die ich mit jira.springsource.org/browse/SEC-1188 protokolliert habe, damit Sie jetzt die SecurityContextHolderStrategy-Instanz (von SecurityContextHolder) über Standard direkt in Ihre Bean einfügen können Federkonfiguration.
Scott Bale
4
Bitte siehe Tsunade21 Antwort. In Spring 3 können Sie jetzt java.security.Principal als Methodenargument in Ihrem Controller verwenden
Patrick
22

Ich bin damit einverstanden, dass das Abfragen des SecurityContext nach dem aktuellen Benutzer stinkt. Es scheint eine sehr unfruchtbare Möglichkeit zu sein, dieses Problem zu lösen.

Ich habe eine statische "Helfer" -Klasse geschrieben, um dieses Problem zu lösen. Es ist insofern schmutzig, als es eine globale und statische Methode ist, aber ich dachte mir, wenn wir irgendetwas im Zusammenhang mit Sicherheit ändern, muss ich zumindest die Details nur an einer Stelle ändern:

/**
* 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
quelle
22
Es ist so lang wie SecurityContextHolder.getContext () und letzteres ist threadsicher, da die Sicherheitsdetails in einem threadLocal gespeichert werden. Dieser Code behält keinen Status bei.
Matt B
22

Damit es nur auf Ihren JSP-Seiten angezeigt wird, können Sie das Spring Security Tag Lib verwenden:

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

Um eines der Tags verwenden zu können, muss die Sicherheits-Taglib in Ihrer JSP deklariert sein:

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

Dann machen Sie auf einer JSP-Seite so etwas:

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

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

HINWEIS: Wie in den Kommentaren von @ SBerg413 erwähnt, müssen Sie hinzufügen

use-expression = "true"

auf das "http" -Tag in der security.xml-Konfiguration, damit dies funktioniert.

Brad Parks
quelle
Dies scheint wahrscheinlich der von Spring Security genehmigte Weg zu sein!
Nick Spacek
3
Damit diese Methode funktioniert, müssen Sie dem http-Tag in der Konfiguration security.xml use-expression = "true" hinzufügen.
SBerg413
Danke @ SBerg413, ich werde meine Antwort bearbeiten und Ihre wichtige Klarstellung hinzufügen!
Brad Parks
14

Wenn Sie Spring Security Version> = 3.2 verwenden, können Sie die @AuthenticationPrincipalAnmerkung verwenden:

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

Hier CustomUserist ein benutzerdefiniertes Objekt, das implementiert und UserDetailsvon einem benutzerdefinierten Objekt zurückgegeben wird UserDetailsService.

Weitere Informationen finden Sie im Kapitel @AuthenticationPrincipal der Spring Security-Referenzdokumente.

matsev
quelle
13

Ich erhalte einen authentifizierten Benutzer durch HttpServletRequest.getUserPrincipal ();

Beispiel:

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
quelle
Ich mag deine Lösung. An Spring-Spezialisten: Ist das eine sichere und gute Lösung?
Marioosh
Keine gute Lösung. Sie erhalten, nullwenn der Benutzer anonym authentifiziert ist ( http> anonymousElemente in Spring Security XML). SecurityContextHolderoder SecurityContextHolderStrategyist der richtige Weg.
Nowaker
1
Damit ich überprüft habe, ob nicht null request.getUserPrincipal ()! = Null.
digz6666
Ist null in Filter
Alex78191
9

In Spring 3+ haben Sie folgende Möglichkeiten.

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: Lust auf eins: Weitere Informationen finden Sie hier

public ModelAndView someRequestHandler(@ActiveUser User activeUser) {
  ...
}
Bauernhof
quelle
1
Seit 3.2 wird Spring-Security-Web mitgeliefert, @CurrentUserdas wie der Brauch @ActiveUseraus Ihrem Link funktioniert .
Mike Partridge
@ MikePartridge, ich scheine nicht zu finden, was du sagst, irgendeinen Link? oder mehr info ??
Azerafati
1
Mein Fehler - Ich habe das Spring Security AuthenticationPrincipalArgumentResolver- Javadoc falsch verstanden . Dort wird ein Beispiel gezeigt, das @AuthenticationPrincipalmit einer benutzerdefinierten @CurrentUserAnmerkung umbrochen wird. Seit 3.2 müssen wir keinen benutzerdefinierten Argumentauflöser wie in der verknüpften Antwort implementieren. Diese andere Antwort enthält mehr Details.
Mike Partridge
5

Ja, Statik ist im Allgemeinen schlecht - im Allgemeinen, aber in diesem Fall ist die Statik der sicherste Code, den Sie schreiben können. Da der Sicherheitskontext einen Principal mit dem aktuell ausgeführten Thread verknüpft, würde der sicherste Code so direkt wie möglich vom Thread auf die Statik zugreifen. Durch das Ausblenden des Zugriffs hinter einer injizierten Wrapper-Klasse erhält ein Angreifer mehr Angriffspunkte. Sie benötigen keinen Zugriff auf den Code (den sie nur schwer ändern können, wenn das JAR signiert wird). Sie benötigen lediglich eine Möglichkeit, die Konfiguration zu überschreiben. Dies kann zur Laufzeit erfolgen oder indem XML in den Klassenpfad verschoben wird. Selbst die Verwendung der Anmerkungsinjektion im signierten Code wäre mit externem XML überschreibbar. Solches XML könnte dem laufenden System einen Rogue-Principal hinzufügen.

Michael Bushe
quelle
5

Ich würde einfach das tun:

request.getRemoteUser();
Dan
quelle
1
Das könnte funktionieren, aber nicht zuverlässig. Von javadoc: "Ob der Benutzername bei jeder nachfolgenden Anforderung gesendet wird, hängt vom Browser und der Art der Authentifizierung ab." - download-llnw.oracle.com/javaee/6/api/javax/servlet/http/…
Scott Bale
3
Dies ist eine gültige und sehr einfache Möglichkeit, den Remote-Benutzernamen in einer Spring Security-Webanwendung abzurufen. Die Standardfilterkette enthält eine, SecurityContextHolderAwareRequestFilterdie die Anforderung umschließt und diesen Aufruf durch Zugriff auf die implementiert SecurityContextHolder.
Shaun das Schaf
4

Für die letzte Spring MVC-App, die ich geschrieben habe, habe ich den SecurityContext-Halter nicht eingefügt, aber ich hatte einen Basis-Controller, für den ich zwei Dienstprogrammmethoden hatte ... isAuthenticated () & getUsername (). Intern führen sie den von Ihnen beschriebenen statischen Methodenaufruf aus.

Zumindest ist es dann nur an einer Stelle, wenn Sie später umgestalten müssen.

RichH
quelle
3

Sie könnten Spring AOP Ansatz verwenden. Wenn Sie beispielsweise einen Dienst haben, muss dieser den aktuellen Principal kennen. Sie können eine benutzerdefinierte Anmerkung einführen, z. B. @Principal, die angibt, dass dieser Service vom Prinzipal abhängig sein sollte.

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

Überprüfen Sie dann in Ihrem Rat, der meiner Meinung nach MethodBeforeAdvice erweitern muss, ob ein bestimmter Dienst über eine @ Principal-Annotation verfügt, und fügen Sie den Principal-Namen ein, oder setzen Sie ihn stattdessen auf 'ANONYMOUS'.

Pavel Rodionov
quelle
Ich muss innerhalb der Serviceklasse auf Principal zugreifen. Können Sie ein vollständiges Beispiel auf github veröffentlichen? Ich kenne Spring AOP nicht, daher die Anfrage.
Rakesh Waghela
2

Das einzige Problem besteht darin, dass die Benutzer- / Principal-Bean auch nach der Authentifizierung bei Spring Security nicht im Container vorhanden ist. Daher ist es schwierig, Abhängigkeiten zu injizieren. Bevor wir Spring Security verwendet haben, haben wir eine Sitzungs-Bean mit dem aktuellen Principal erstellt, diese in einen "AuthService" eingefügt und diesen Service dann in die meisten anderen Dienste in der Anwendung eingefügt. Diese Dienste würden also einfach authService.getCurrentUser () aufrufen, um das Objekt abzurufen. Wenn Sie in Ihrem Code eine Stelle haben, an der Sie in der Sitzung einen Verweis auf denselben Principal erhalten, können Sie ihn einfach als Eigenschaft für Ihre Bean mit Sitzungsbereich festlegen.

cliff.meyers
quelle
1

Versuche dies

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

Cherit
quelle
3
Das Aufrufen der statischen Methode SecurityContextHolder.getContext () ist genau das, worüber ich mich in der ursprünglichen Frage beschwert habe. Du hast nichts beantwortet.
Scott Bale
2
Es ist jedoch genau das, was in der Dokumentation empfohlen wird: static.springsource.org/spring-security/site/docs/3.0.x/… Was erreichen Sie also, indem Sie es vermeiden? Sie suchen nach einer komplizierten Lösung für ein einfaches Problem. Bestenfalls - Sie bekommen das gleiche Verhalten. Im schlimmsten Fall erhalten Sie einen Fehler oder eine Sicherheitslücke.
Bob Kerns
2
@BobKerns Zum Testen ist es sauberer, die Authentifizierung injizieren zu können, als sie in einen lokalen Thread zu stellen.
1

Die beste Lösung, wenn Sie Spring 3 verwenden und den authentifizierten Principal in Ihrem Controller benötigen, ist Folgendes:

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

    }
Kennzeichen
quelle
1
Warum führen Sie diese Instanz der Überprüfung von UsernamePasswordAuthenticationToken durch, wenn der Parameter bereits vom Typ UsernamePasswordAuthenticationToken ist?
Scott Bale
(authToken-Instanz von UsernamePasswordAuthenticationToken) entspricht funktional if (authToken! = null). Letzteres mag etwas sauberer sein, aber sonst gibt es keinen Unterschied.
Mark
1

Ich verwende die @AuthenticationPrincipalAnnotation sowohl in @ControllerKlassen als auch in @ControllerAdvicerannotierten. Ex.:

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


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

Wo UserActiveist die Klasse i für angemeldete Benutzer Dienste nutzen, und erstreckt sich von org.springframework.security.core.userdetails.User. Etwas wie:

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
}

Wirklich einfach.

EliuX
quelle
0

Definieren Sie Principalals Abhängigkeit in Ihrer Controller-Methode, und Spring fügt beim Aufruf den aktuell authentifizierten Benutzer in Ihre Methode ein.

Imrank
quelle
-2

Ich teile gerne meine Art, Benutzerdetails auf der Freemarker-Seite zu unterstützen. Alles ist sehr einfach und funktioniert perfekt!

Sie müssen nur eine erneute Authentifizierungsanforderung auf default-target-url(Seite nach der Formularanmeldung) stellen. Dies ist meine Controler-Methode für diese Seite:

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

Und das ist mein ftl-Code:

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

Und das war's, der Benutzername wird nach der Autorisierung auf jeder Seite angezeigt.

Serge
quelle
Vielen Dank, dass Sie versucht haben zu antworten, aber die Verwendung der statischen Methode SecurityContextHolder.getContext () ist genau das, was ich vermeiden wollte, und der Grund, warum ich diese Frage überhaupt gestellt habe.
Scott Bale