Jak uzyskać szczegóły UserDetails aktywnego użytkownika

170

W moich kontrolerach, gdy potrzebuję aktywnego (zalogowanego) użytkownika, wykonuję następujące czynności, aby uzyskać moją UserDetailsimplementację:

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

Działa dobrze, ale myślę, że Wiosna może ułatwić życie w takim przypadku. Czy istnieje sposób na automatyczne UserDetailspołączenie z kontrolerem lub metodą?

Na przykład coś takiego:

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

Ale zamiast dostać UsernamePasswordAuthenticationToken, dostaję UserDetailszamiast tego?

Szukam eleganckiego rozwiązania. Jakieś pomysły?

Niedźwiedź Awnry
źródło

Odpowiedzi:

226

Preambuła: Od Spring-Security 3.2 @AuthenticationPrincipalna końcu tej odpowiedzi znajduje się ładna adnotacja . To najlepszy sposób, jeśli używasz Spring-Security> = 3.2.

Kiedy ty:

  • użyj starszej wersji Spring-Security,
  • trzeba załadować niestandardowy obiekt użytkownika z bazy danych za pomocą pewnych informacji (takich jak login lub identyfikator) przechowywanych w pliku głównym lub
  • chcesz dowiedzieć się, jak HandlerMethodArgumentResolverlub WebArgumentResolvermożna rozwiązać ten problem w elegancki sposób, lub po prostu chcesz poznać tło za @AuthenticationPrincipali AuthenticationPrincipalArgumentResolver(ponieważ opiera się na a HandlerMethodArgumentResolver)

następnie czytaj dalej - w przeciwnym razie użyj @AuthenticationPrincipali podziękuj Robowi Winchowi (autorowi @AuthenticationPrincipal) i Lukasowi Schmelzeisena (za odpowiedź).

(BTW: Moja odpowiedź jest nieco starsza (styczeń 2012), więc to Lukas Schmelzeisen jako pierwszy pojawił się w @AuthenticationPrincipalrozwiązaniu adnotacji opartym na Spring Security 3.2.)


Następnie możesz użyć w swoim kontrolerze

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

To jest w porządku, jeśli potrzebujesz tego raz. Ale jeśli potrzebujesz go kilka razy, jest brzydki, ponieważ zanieczyszcza kontroler szczegółami infrastruktury, które normalnie powinny być ukryte przez framework.

Więc to, czego naprawdę możesz chcieć, to mieć taki kontroler:

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

Dlatego wystarczy zaimplementować plik WebArgumentResolver. Ma metodę

Object resolveArgument(MethodParameter methodParameter,
                   NativeWebRequest webRequest)
                   throws Exception

Pobiera żądanie sieciowe (drugi parametr) i musi zwrócić, Userjeśli czuje się odpowiedzialny za argument metody (pierwszy parametr).

Od wiosny 3.1 pojawiła się nowa koncepcja o nazwie HandlerMethodArgumentResolver. Jeśli używasz Spring 3.1+, powinieneś go użyć. (Jest to opisane w następnej sekcji tej odpowiedzi))

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

Musisz zdefiniować adnotację niestandardową - możesz ją pominąć, jeśli każda instancja użytkownika powinna zawsze pochodzić z kontekstu zabezpieczeń, ale nigdy nie jest obiektem polecenia.

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

W konfiguracji wystarczy dodać to:

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

@ Zobacz : Naucz się dostosowywać argumenty metody Spring MVC @Controller

Należy zauważyć, że jeśli używasz Spring 3.1, zalecają HandlerMethodArgumentResolver zamiast WebArgumentResolver. - patrz komentarz Jay


To samo HandlerMethodArgumentResolverdotyczy wiosny 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;
          }
     }
}

W konfiguracji musisz to dodać

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

@See Wykorzystanie interfejsu Spring MVC 3.1 HandlerMethodArgumentResolver


Rozwiązanie Spring-Security 3.2

Spring Security 3.2 (nie mylić z Spring 3.2) ma własne rozwiązanie wbudowane: @AuthenticationPrincipal( org.springframework.security.web.bind.annotation.AuthenticationPrincipal). Ładnie to opisuje odpowiedź Lukasa Schmelzeisena

Po prostu pisze

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

Aby to działało, musisz zarejestrować AuthenticationPrincipalArgumentResolver( org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver): albo przez "aktywację", @EnableWebMvcSecurityalbo przez zarejestrowanie tego ziarna w mvc:argument-resolvers- tak samo, jak opisałem to z rozwiązaniem May Spring 3.1 powyżej.

@Patrz Spring Security 3.2 Reference, rozdział 11.2. @AuthenticationPrincipal


Rozwiązanie Spring-Security 4.0

Działa jak rozwiązanie Spring 3.2, ale w wersji Spring 4.0 @AuthenticationPrincipali AuthenticationPrincipalArgumentResolverzostał „przeniesiony” do innego pakietu:

(Ale stare klasy w starych paczkach nadal istnieją, więc nie mieszaj ich!)

Po prostu pisze

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

Aby to działało, musisz zarejestrować ( org.springframework.security.web.method.annotation.) AuthenticationPrincipalArgumentResolver: albo przez "aktywację", @EnableWebMvcSecurityalbo przez zarejestrowanie tego ziarna w mvc:argument-resolvers- tak samo, jak opisałem to z rozwiązaniem May Spring 3.1 powyżej.

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

@Patrz Spring Security 5.0 Reference, rozdział 39.3 @AuthenticationPrincipal

Ralph
źródło
Alternatywnie, po prostu utwórz fasolę, która ma metodę getUserDetails () i @Autowire, która jest umieszczana w kontrolerze.
sourcedelica
Wdrażając to rozwiązanie, ustawiłem punkt przerwania w górnej części metody replaceArgument (), ale moja aplikacja nigdy nie przechodzi do mechanizmu rozpoznawania argumentów sieci Web. Twoja konfiguracja sprężynowa w kontekście serwletu, a nie w kontekście głównym, prawda?
Jay
@Jay: Ta konfiguracja jest częścią kontekstu głównego, a nie kontekstu serwletu. - wydaje się, że zapomniałem podać id (id="applicationConversionService")w przykładzie
Ralph
11
Należy zauważyć, że jeśli używasz Spring 3.1, zalecają HandlerMethodArgumentResolver zamiast WebArgumentResolver. Mam HandlerMethodArgumentResolver do pracy, konfigurując go z <annotation-driven> w kontekście serwletu. Poza tym zaimplementowałem odpowiedź zgodnie z zamieszczoną tutaj i wszystko działa świetnie
Jay
@sourcedelica Dlaczego nie stworzyć metody statycznej?
Alex78191
66

Chociaż Ralphs Answer zapewnia eleganckie rozwiązanie, dzięki Spring Security 3.2 nie musisz już wdrażać własnego ArgumentResolver.

Jeśli masz UserDetailsimplementację CustomUser, możesz po prostu zrobić to:

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

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

Zobacz dokumentację zabezpieczeń Spring: @AuthenticationPrincipal

Lukas Schmelzeisen
źródło
2
dla tych, którzy nie lubią czytać podanych linków, musi to być włączone przez @EnableWebMvcSecurityXML lub w formacie XML:<mvc:annotation-driven> <mvc:argument-resolvers> <bean class="org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver" /> </mvc:argument-resolvers> </mvc:annotation-driven>
sodik
Jak sprawdzić, czy customUserjest null, czy nie?
Sajad
if (customUser != null) { ... }
T3rm1
27

Spring Security jest przeznaczony do współpracy z innymi frameworkami innymi niż Spring, dlatego nie jest ściśle zintegrowany z Spring MVC. Spring Security domyślnie zwraca Authenticationobiekt z HttpServletRequest.getUserPrincipal()metody, więc otrzymujesz to jako podmiot. Możesz uzyskać swój UserDetailsobiekt bezpośrednio z tego za pomocą

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

Zauważ również, że typy obiektów mogą się różnić w zależności od używanego mechanizmu uwierzytelniania (możesz UsernamePasswordAuthenticationTokenna przykład nie dostać ) i Authenticationnie musi ściśle zawierać pliku UserDetails. Może to być ciąg lub dowolny inny typ.

Jeśli nie chcesz dzwonić SecurityContextHolderbezpośrednio, najbardziej eleganckim podejściem (które bym zastosował) jest wstrzyknięcie własnego niestandardowego interfejsu akcesora kontekstu bezpieczeństwa, który jest dostosowany do twoich potrzeb i typów obiektów użytkownika. Utwórz interfejs z odpowiednimi metodami, na przykład:

interface MySecurityAccessor {

    MyUserDetails getCurrentUser();

    // Other methods
}

Następnie możesz to zaimplementować, uzyskując dostęp do SecurityContextHolderstandardowej implementacji, w ten sposób całkowicie oddzielając swój kod od Spring Security. Następnie wstrzyknij to do kontrolerów, które potrzebują dostępu do informacji bezpieczeństwa lub informacji o aktualnym użytkowniku.

Inną główną korzyścią jest to, że łatwo jest tworzyć proste implementacje ze stałymi danymi do testowania, bez martwienia się o zapełnianie lokalnych wątków i tak dalej.

Baranek Shaun
źródło
Rozważałem to podejście, ale nie byłem pewien a) jak dokładnie to zrobić we właściwy sposób (tm) ib) jeśli wystąpią problemy z gwintowaniem. Czy na pewno nie byłoby tam problemów? Idę z metodą adnotacji zamieszczoną powyżej, ale myślę, że jest to nadal przyzwoity sposób na zrobienie tego. Dzięki za wysłanie wiadomości. :)
The Awnry Bear
4
Z technicznego punktu widzenia jest to to samo, co uzyskiwanie dostępu do SecurityContextHolder bezpośrednio z kontrolera, więc nie powinno być żadnych problemów z wątkami. Po prostu utrzymuje połączenie w jednym miejscu i pozwala łatwo wstrzyknąć alternatywy do testów. Możesz również użyć tego samego podejścia w innych klasach innych niż internetowe, które wymagają dostępu do informacji bezpieczeństwa.
Shaun the Sheep
Mam cię ... właśnie o tym myślałem. Będzie to dobry temat, do którego warto wrócić, jeśli mam problemy z testowaniem jednostkowym z metodą adnotacji lub jeśli chcę zmniejszyć sprzężenie ze Springiem.
The Awnry Bear
@LukeTaylor, czy możesz dalej wyjaśnić to podejście, jestem trochę nowy w zabezpieczeniach wiosennych i wiosennych, więc nie bardzo wiem, jak to zaimplementować. Gdzie mam to zaimplementować, aby uzyskać do niego dostęp? do mojego UserServiceImpl?
Cu7l4ss
9

Zaimplementuj HandlerInterceptorinterfejs, a następnie wstrzyknij UserDetailsdo każdego żądania, które ma model, w następujący sposób:

@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());
        }
}
Pociąg
źródło
1
Dzięki @atrain, to przydatne i eleganckie. Musiałem również dodać <mvc:interceptors>w pliku konfiguracyjnym mojej aplikacji.
Eric,
Lepiej jest uzyskać autoryzowanego użytkownika w szablonie przez spring-security-taglibs: stackoverflow.com/a/44373331/548473
Grigory Kislin
9

Począwszy od wersji Spring Security 3.2, niestandardowa funkcjonalność, która została zaimplementowana w niektórych starszych odpowiedziach, istnieje po wyjęciu z pudełka w postaci @AuthenticationPrincipaladnotacji, która jest obsługiwana przez AuthenticationPrincipalArgumentResolver.

Prostym przykładem jego użycia jest:

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

CustomUser musi być przypisywany z authentication.getPrincipal()

Oto odpowiednie dokumenty Javadocs dotyczące AuthenticationPrincipal i AuthenticationPrincipalArgumentResolver

geoand
źródło
1
@nbro Kiedy dodałem rozwiązanie specyficzne dla wersji, żadne inne rozwiązanie nie zostało zaktualizowane, aby uwzględnić to rozwiązanie
geo i
AuthenticationPrincipalArgumentResolver jest teraz przestarzały
Igor Donin
5
@Controller
public abstract class AbstractController {
    @ModelAttribute("loggedUser")
    public User getLoggedUser() {
        return (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
}
Towarzysz
źródło
0

A jeśli potrzebujesz autoryzowanego użytkownika w szablonach (np. JSP) użyj

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

razem z

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-taglibs</artifactId>
        <version>${spring-security.version}</version>
    </dependency>
Grigorij Kislin
źródło
0

Możesz spróbować tego: Używając obiektu uwierzytelniania Springa możemy uzyskać z niego dane użytkownika w metodzie kontrolera. Poniżej znajduje się przykład, przez przekazanie obiektu Authentication w metodzie kontrolera wraz z argumentem. Gdy użytkownik zostanie uwierzytelniony, szczegóły zostaną wprowadzone do obiektu uwierzytelniania.

@GetMapping(value = "/mappingEndPoint") <ReturnType> methodName(Authentication auth) {
   String userName = auth.getName(); 
   return <ReturnType>;
}
Mirza Shujathullah
źródło
Opisz swoją odpowiedź.
Nikołaj Szewczenko
Zmieniłem moją odpowiedź, moglibyśmy użyć obiektu uwierzytelniania, który zawiera dane użytkownika, który już został uwierzytelniony.
Mirza Shujathullah