So erhalten Sie UserDetails für aktive Benutzer

170

Wenn ich in meinen Controllern den aktiven (angemeldeten) Benutzer benötige, gehe ich wie folgt vor, um meine UserDetailsImplementierung zu erhalten:

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

Es funktioniert gut, aber ich würde denken, dass der Frühling in einem solchen Fall das Leben leichter machen könnte. Gibt es eine Möglichkeit, das UserDetailsAutowire entweder in den Controller oder in die Methode zu integrieren?

Zum Beispiel so etwas wie:

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

Aber anstatt das zu bekommen UsernamePasswordAuthenticationToken, bekomme ich UserDetailsstattdessen ein?

Ich suche eine elegante Lösung. Irgendwelche Ideen?

Der Awnry Bär
quelle

Antworten:

226

Präambel: Seit Spring-Security 3.2 ist @AuthenticationPrincipalam Ende dieser Antwort eine nette Anmerkung beschrieben. Dies ist der beste Weg, wenn Sie Spring-Security> = 3.2 verwenden.

Wenn du:

  • Verwenden Sie eine ältere Version von Spring-Security.
  • Sie müssen Ihr benutzerdefiniertes Benutzerobjekt aus der Datenbank mit einigen Informationen (wie dem Login oder der ID) laden, die im Principal oder gespeichert sind
  • wollen lernen, wie ein HandlerMethodArgumentResolveroder WebArgumentResolverdies auf elegante Weise lösen kann, oder wollen einfach nur den Hintergrund hinter @AuthenticationPrincipalund lernen AuthenticationPrincipalArgumentResolver(weil es auf a basiert HandlerMethodArgumentResolver)

dann lesen Sie weiter - sonst verwenden Sie einfach @AuthenticationPrincipalund danken Sie Rob Winch (Autor von @AuthenticationPrincipal) und Lukas Schmelzeisen (für seine Antwort).

(Übrigens: Meine Antwort ist etwas älter (Januar 2012), daher war es Lukas Schmelzeisen , der als erster die @AuthenticationPrincipalAnnotation-Lösungsbasis auf Spring Security 3.2 aufstellte .)


Dann können Sie in Ihrem Controller verwenden

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

Das ist in Ordnung, wenn Sie es einmal brauchen. Wenn Sie es jedoch mehrmals benötigen, ist es hässlich, weil es Ihren Controller mit Infrastrukturdetails verschmutzt. Dies sollte normalerweise vom Framework ausgeblendet werden.

Was Sie also wirklich wollen, ist ein Controller wie dieser:

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

Daher müssen Sie nur a implementieren WebArgumentResolver. Es hat eine Methode

Object resolveArgument(MethodParameter methodParameter,
                   NativeWebRequest webRequest)
                   throws Exception

Das erhält die Webanforderung (zweiter Parameter) und muss die zurückgeben, Userwenn es sich für das Methodenargument (den ersten Parameter) verantwortlich fühlt.

Seit Frühjahr 3.1 gibt es ein neues Konzept namens HandlerMethodArgumentResolver. Wenn Sie Spring 3.1+ verwenden, sollten Sie es verwenden. (Es wird im nächsten Abschnitt dieser Antwort beschrieben))

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

Sie müssen die benutzerdefinierte Anmerkung definieren - Sie können sie überspringen, wenn jede Instanz des Benutzers immer aus dem Sicherheitskontext entnommen werden soll, aber niemals ein Befehlsobjekt ist.

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

In der Konfiguration müssen Sie nur Folgendes hinzufügen:

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

@See: Erfahren Sie, wie Sie die Argumente der Spring MVC @Controller-Methode anpassen

Wenn Sie Spring 3.1 verwenden, empfehlen sie HandlerMethodArgumentResolver gegenüber WebArgumentResolver. - siehe Kommentar von Jay


Gleiches gilt HandlerMethodArgumentResolverfür 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;
          }
     }
}

In der Konfiguration müssen Sie dies hinzufügen

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

@Siehe Nutzung der Spring MVC 3.1 HandlerMethodArgumentResolver-Schnittstelle


Spring-Security 3.2 Lösung

Spring Security 3.2 (nicht mit Spring 3.2 verwechseln) verfügt über eine eigene integrierte Lösung: @AuthenticationPrincipal( org.springframework.security.web.bind.annotation.AuthenticationPrincipal). Dies ist in Lukas Schmelzeisens Antwort gut beschrieben

Es schreibt nur

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

Damit dies funktioniert, müssen Sie AuthenticationPrincipalArgumentResolver( org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver) registrieren : entweder durch "Aktivieren" @EnableWebMvcSecurityoder durch Registrieren dieser Bean innerhalb mvc:argument-resolvers- so wie ich es oben mit der Lösung von may Spring 3.1 beschrieben habe.

@ Siehe Spring Security 3.2 Referenz, Kapitel 11.2. @AuthenticationPrincipal


Spring-Security 4.0-Lösung

Es funktioniert wie die Spring 3.2-Lösung, aber in Spring 4.0 wurde das @AuthenticationPrincipalund AuthenticationPrincipalArgumentResolverin ein anderes Paket "verschoben":

(Aber die alten Klassen in ihren alten Paketen existieren noch, also mischen Sie sie nicht!)

Es schreibt nur

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

Damit dies funktioniert, müssen Sie ( org.springframework.security.web.method.annotation.) registrieren AuthenticationPrincipalArgumentResolver: entweder durch "Aktivieren" @EnableWebMvcSecurityoder durch Registrieren dieser Bean innerhalb mvc:argument-resolvers- so wie ich es oben mit der Lösung von may Spring 3.1 beschrieben habe.

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

@ Siehe Spring Security 5.0-Referenz, Kapitel 39.3 @AuthenticationPrincipal

Ralph
quelle
Alternativ können Sie einfach eine Bean mit einer getUserDetails () -Methode und @Autowire in Ihrem Controller erstellen.
Sourcedelica
Bei der Implementierung dieser Lösung habe ich einen Haltepunkt am oberen Rand von resolveArgument () festgelegt, aber meine App tritt niemals in den Webargument-Resolver ein. Ihre Federkonfiguration im Servlet-Kontext ist nicht der Root-Kontext, oder?
Jay
@ Jay: Diese Konfiguration ist Teil des Stammkontexts, nicht des Servlet-Kontexts. - Es scheint, dass ich vergessen habe, die ID (id="applicationConversionService")im Beispiel anzugeben
Ralph
11
Wenn Sie Spring 3.1 verwenden, empfehlen sie HandlerMethodArgumentResolver gegenüber WebArgumentResolver. Ich habe HandlerMethodArgumentResolver zum Arbeiten gebracht, indem ich es mit <annotation-driven> im Servlet-Kontext konfiguriert habe. Abgesehen davon habe ich die Antwort wie hier implementiert implementiert und alles funktioniert großartig
Jay
@sourcedelica Warum nicht einfach eine statische Methode erstellen?
Alex78191
66

Während Ralphs Answer eine elegante Lösung bietet, müssen Sie mit Spring Security 3.2 keine eigenen mehr implementieren ArgumentResolver.

Wenn Sie eine UserDetailsImplementierung haben CustomUser, können Sie dies einfach tun:

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

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

Siehe Spring Security-Dokumentation: @AuthenticationPrincipal

Lukas Schmelzeisen
quelle
2
Für diejenigen, die bereitgestellte Links nicht lesen @EnableWebMvcSecurity<mvc:annotation-driven> <mvc:argument-resolvers> <bean class="org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver" /> </mvc:argument-resolvers> </mvc:annotation-driven>
möchten
Wie überprüfe ich, ob das customUsernull ist oder nicht?
Sajad
if (customUser != null) { ... }
T3rm1
27

Spring Security soll mit anderen Nicht-Spring-Frameworks zusammenarbeiten und ist daher nicht eng in Spring MVC integriert. Spring Security gibt das AuthenticationObjekt HttpServletRequest.getUserPrincipal()standardmäßig von der Methode zurück, sodass Sie dies als Prinzipal erhalten. Sie können Ihr UserDetailsObjekt direkt daraus erhalten, indem Sie

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

Beachten Sie auch, dass die Objekttypen je nach verwendetem Authentifizierungsmechanismus variieren können ( UsernamePasswordAuthenticationTokenz. B. erhalten Sie möglicherweise kein ) und dass das Objekt nicht unbedingt ein Authenticationenthalten muss UserDetails. Es kann eine Zeichenfolge oder ein anderer Typ sein.

Wenn Sie nicht SecurityContextHolderdirekt anrufen möchten , besteht der eleganteste Ansatz (dem ich folgen würde) darin, eine eigene benutzerdefinierte Sicherheitskontext-Accessor-Oberfläche einzufügen, die an Ihre Anforderungen und Benutzerobjekttypen angepasst ist. Erstellen Sie eine Schnittstelle mit den entsprechenden Methoden, zum Beispiel:

interface MySecurityAccessor {

    MyUserDetails getCurrentUser();

    // Other methods
}

Sie können dies dann implementieren, indem Sie auf das SecurityContextHolderin Ihrer Standardimplementierung zugreifen und so Ihren Code vollständig von Spring Security entkoppeln. Fügen Sie dies dann in die Controller ein, die Zugriff auf Sicherheitsinformationen oder Informationen zum aktuellen Benutzer benötigen.

Der andere Hauptvorteil besteht darin, dass es einfach ist, einfache Implementierungen mit festen Daten zum Testen durchzuführen, ohne sich um das Auffüllen von Thread-Locals usw. kümmern zu müssen.

Shaun das Schaf
quelle
Ich habe über diesen Ansatz nachgedacht, war mir aber nicht sicher, a) wie ich es richtig machen soll (tm) und b) ob es Threading-Probleme geben würde. Sind Sie sicher, dass es dort keine Probleme geben würde? Ich gehe mit der oben beschriebenen Annotationsmethode vor, aber ich denke, dies ist immer noch eine anständige Vorgehensweise. Danke fürs Schreiben. :)
Der Awnry Bär
4
Dies entspricht technisch dem direkten Zugriff auf den SecurityContextHolder von Ihrem Controller aus, sodass keine Threading-Probleme auftreten sollten. Es hält den Anruf nur an einem einzigen Ort und ermöglicht es Ihnen, Alternativen zum Testen einfach zu injizieren. Sie können denselben Ansatz auch in anderen Nicht-Webklassen wiederverwenden, die Zugriff auf Sicherheitsinformationen benötigen.
Shaun das Schaf
Gotcha ... das habe ich mir gedacht. Dies ist eine gute Lösung, wenn ich Probleme mit Unit-Tests mit der Annotation-Methode habe oder wenn ich die Kopplung mit Spring verringern möchte.
Der Awnry Bär
@LukeTaylor, können Sie diesen Ansatz bitte näher erläutern? Ich bin ein bisschen neu in Bezug auf Frühling und Frühlingssicherheit, daher verstehe ich nicht ganz, wie ich dies implementieren soll. Wo implementiere ich das, damit darauf zugegriffen werden kann? zu meinem UserServiceImpl?
Cu7l4ss
9

Implementieren Sie die HandlerInterceptorSchnittstelle und fügen Sie sie dann UserDetailswie folgt in jede Anforderung mit einem Modell ein:

@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());
        }
}
ein Zug
quelle
1
Danke @atrain, das ist nützlich und elegant. Außerdem musste ich die <mvc:interceptors>in meiner Anwendungskonfigurationsdatei hinzufügen .
Eric
Es ist besser, autorisierten Benutzer in Vorlage zu bekommen über spring-security-taglibs: stackoverflow.com/a/44373331/548473
Grigory Kislin
9

Ab Spring Security Version 3.2 ist die benutzerdefinierte Funktionalität, die von einigen älteren Antworten implementiert wurde, sofort in Form der @AuthenticationPrincipalAnmerkung vorhanden, die von unterstützt wird AuthenticationPrincipalArgumentResolver.

Ein einfaches Beispiel für seine Verwendung ist:

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

CustomUser muss von zuweisbar sein authentication.getPrincipal()

Hier sind die entsprechenden Javadocs von AuthenticationPrincipal und AuthenticationPrincipalArgumentResolver

geoand
quelle
1
@nbro Als ich die versionenspezifische Lösung hinzufügte, wurde keine der anderen Lösungen aktualisiert, um diese Lösung zu berücksichtigen
geoand
AuthenticationPrincipalArgumentResolver ist jetzt veraltet
Igor Donin
5
@Controller
public abstract class AbstractController {
    @ModelAttribute("loggedUser")
    public User getLoggedUser() {
        return (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
}
Genosse
quelle
0

Und wenn Sie autorisierte Benutzer in Vorlagen (zB JSP) benötigen, verwenden Sie

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

zusammen mit

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

Sie können dies versuchen: Mithilfe des Authentifizierungsobjekts von Spring können wir Benutzerdetails in der Controller-Methode abrufen. Im Folgenden finden Sie ein Beispiel für die Übergabe des Authentifizierungsobjekts in der Controller-Methode zusammen mit dem Argument. Sobald der Benutzer authentifiziert ist, werden die Details im Authentifizierungsobjekt ausgefüllt.

@GetMapping(value = "/mappingEndPoint") <ReturnType> methodName(Authentication auth) {
   String userName = auth.getName(); 
   return <ReturnType>;
}
Mirza Shujathullah
quelle
Bitte erläutern Sie Ihre Antwort.
Nikolai Shevchenko
Ich habe meine Antwort bearbeitet. Wir könnten ein Authentifizierungsobjekt verwenden, das Benutzerdetails des Benutzers enthält, der sich bereits authentifiziert hat.
Mirza Shujathullah