Kiedy korzystasz z Spring Security, jaki jest właściwy sposób uzyskania bieżącej informacji o nazwie użytkownika (tj. SecurityContext) w fasoli?


288

Mam aplikację internetową Spring MVC, która korzysta z Spring Security. Chcę poznać nazwę użytkownika aktualnie zalogowanego. Używam fragmentu kodu podanego poniżej. Czy to jest akceptowany sposób?

Nie podoba mi się wywołanie metody statycznej w tym kontrolerze - co przeczy całemu celowi Springa, IMHO. Czy istnieje sposób, aby skonfigurować aplikację tak, aby zamiast niej podawać bieżący kontekst zabezpieczeń lub bieżące uwierzytelnianie?

  @RequestMapping(method = RequestMethod.GET)
  public ModelAndView showResults(final HttpServletRequest request...) {
    final String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
    ...
  }

Dlaczego nie masz kontrolera (kontrolera bezpiecznego) jako superklasy, aby pobrać użytkownika z obiektu SecurityContext i ustawić go jako zmienną instancji w tej klasie? W ten sposób, gdy rozszerzysz bezpieczny kontroler, cała klasa będzie miała dostęp do nazwy użytkownika bieżącego kontekstu.
Dehan de Croos,

Odpowiedzi:


259

Jeśli używasz Spring 3 , najprostszym sposobem jest:

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

     final String currentUser = principal.getName();

 }

69

W świecie wiosennym wiele się zmieniło od czasu odpowiedzi na to pytanie. Wiosna uprościła wprowadzanie bieżącego użytkownika do kontrolera. W przypadku innych ziaren Spring przyjął sugestie autora i uprościł zastrzyk „SecurityContextHolder”. Więcej szczegółów znajduje się w komentarzach.


To jest rozwiązanie, z którym skończyłem. Zamiast używać SecurityContextHolderw moim kontrolerze, chcę wstrzyknąć coś, co używa SecurityContextHolderpod maską, ale abstrahuje od mojego kodu klasę podobną do singletona. Nie znalazłem innego sposobu niż zrobienie tego z własnym interfejsem:

public interface SecurityContextFacade {

  SecurityContext getContext();

  void setContext(SecurityContext securityContext);

}

Teraz mój kontroler (lub cokolwiek POJO) wyglądałby tak:

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
  }

}

A ponieważ interfejs jest punktem odsprzęgania, testowanie jednostkowe jest proste. W tym przykładzie używam 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();
  }

}

Domyślna implementacja interfejsu wygląda następująco:

public class SecurityContextHolderFacade implements SecurityContextFacade {

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

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

}

I wreszcie produkcyjna konfiguracja wiosenna wygląda następująco:

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

Wydaje się bardziej niż trochę głupie, że Spring, pojemnik do wstrzykiwania zależności wszystkich rzeczy, nie dostarczył sposobu na wstrzyknięcie czegoś podobnego. Rozumiem, że SecurityContextHolderzostał odziedziczony z acegi, ale nadal. Chodzi o to, że są tak blisko - gdyby tylko SecurityContextHoldermieli getter, aby uzyskać bazową SecurityContextHolderStrategyinstancję (która jest interfejsem), moglibyście to wstrzyknąć. W rzeczywistości nawet otworzyłem w tym celu kwestię Jiry .

I ostatnia rzecz - zasadniczo zmieniłem odpowiedź, którą tu wcześniej miałem. Sprawdź historię, jeśli jesteś ciekawy, ale, jak wskazał mi współpracownik, moja poprzednia odpowiedź nie zadziałałaby w środowisku wielowątkowym. Bazą SecurityContextHolderStrategyużywaną przez SecurityContextHolderjest domyślnie instancja ThreadLocalSecurityContextHolderStrategy, która przechowuje SecurityContexts w pliku ThreadLocal. Dlatego niekoniecznie dobrym pomysłem jest wstrzyknięcie SecurityContextbezpośrednio do komponentu bean w czasie inicjalizacji - może za ThreadLocalkażdym razem być konieczne pobranie go w środowisku wielowątkowym, aby pobrać właściwy.


1
Podoba mi się twoje rozwiązanie - to sprytne wykorzystanie wsparcia metodami fabrycznymi na wiosnę. To powiedziawszy, działa to dla ciebie, ponieważ obiekt kontrolera jest objęty zakresem żądania sieci. Niepoprawna zmiana zakresu komponentu bean kontrolera może spowodować uszkodzenie.
Paul Morie

2
Poprzednie dwa komentarze odnoszą się do starej, niepoprawnej odpowiedzi, którą właśnie zastąpiłem.
Scott Bale,

12
Czy nadal jest to zalecane rozwiązanie w bieżącej wersji Spring? Nie mogę uwierzyć, że potrzeba tak dużo kodu, aby tylko pobrać nazwę użytkownika.
Ta Sas

6
Jeśli korzystasz z Spring Security 3.0.x, zaimplementowali moją sugestię w problemie JIRA. Zalogowałem jira.springsource.org/browse/SEC-1188, dzięki czemu możesz teraz wstrzyknąć instancję SecurityContextHolderStrategy (z SecurityContextHolder) bezpośrednio do komponentu bean za pomocą standardowego Konfiguracja wiosenna.
Scott Bale,

4
Zobacz odpowiedź tsunade21. Wiosna 3 pozwala teraz używać java.security.Principal jako argumentu metody w kontrolerze
Patrick

22

Zgadzam się, że zapytanie o SecurityContext dla obecnego użytkownika śmierdzi, wydaje się to bardzo nie-wiosenny sposób poradzenia sobie z tym problemem.

Napisałem statyczną klasę „pomocnika”, aby poradzić sobie z tym problemem; jest brudny, ponieważ jest to metoda globalna i statyczna, ale pomyślałem w ten sposób, jeśli zmienimy coś związanego z bezpieczeństwem, przynajmniej muszę zmienić szczegóły tylko w jednym miejscu:

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

22
jest tak długo, jak jest SecurityContextHolder.getContext (), a ta druga jest wątkowo bezpieczna, ponieważ przechowuje szczegóły bezpieczeństwa w ThreadLocal. Ten kod nie utrzymuje stanu.
matt b

22

Aby po prostu pojawił się na stronach JSP, możesz użyć Spring Security Tag Lib:

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

Aby użyć dowolnego ze znaczników, musisz zadeklarować taglib bezpieczeństwa na swojej stronie JSP:

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

Następnie na stronie jsp wykonaj coś takiego:

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

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

UWAGA: Jak wspomniano w komentarzach @ SBerg413, musisz dodać

use-expressions = "true"

do znacznika „http” w konfiguracji security.xml, aby to działało.


Wygląda na to, że jest to prawdopodobnie sposób zatwierdzony przez Spring Security!
Nick Spacek

3
Aby ta metoda działała, musisz dodać use-expressions = "true" do znacznika http w konfiguracji security.xml.
SBerg413

Dzięki @ SBerg413, zredaguję moją odpowiedź i dodam wasze ważne wyjaśnienie!
Brad Parks

14

Jeśli używasz Spring Security ver> = 3.2, możesz użyć @AuthenticationPrincipaladnotacji:

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

Tutaj CustomUserjest niestandardowy obiekt, który implementuje, UserDetailsktóry jest zwracany przez niestandardowy UserDetailsService.

Więcej informacji można znaleźć w rozdziale @AuthenticationPrincipal dokumentów referencyjnych Spring Security.


13

Otrzymuję uwierzytelnionego użytkownika przez HttpServletRequest.getUserPrincipal ();

Przykład:

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

Lubię twoje rozwiązanie. Dla specjalistów Spring: czy to bezpieczne, dobre rozwiązanie?
marioosh,

Niezbyt dobre rozwiązanie. Otrzymasz, nulljeśli użytkownik zostanie uwierzytelniony anonimowo ( http> anonymouselementy w Spring Security XML). SecurityContextHolderlub SecurityContextHolderStrategyjest właściwy sposób.
Nowaker

1
Tak, że sprawdziłem, jeśli nie null request.getUserPrincipal ()! = Null.
digz6666

Jest pusty w
filtrze

9

Wiosną 3+ masz następujące opcje.

Opcja 1 :

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

Opcja 2 :

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

Opcja 3:

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

}

Opcja 4: Fancy one: Sprawdź to, aby uzyskać więcej informacji

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

1
Od wersji 3.2 dostępna jest wiosenna strona bezpieczeństwa, @CurrentUserktóra działa jak niestandardowy @ActiveUserlink.
Mike Partridge

@ MikePartridge, chyba nie znajduję tego, co mówisz, żadnego linku? lub więcej informacji?
azerafati,

1
Mój błąd - źle zrozumiałem Spring Security AuthenticationPrincipalArgumentResolver javadoc. Pokazano tam przykład zawijania @AuthenticationPrincipalz niestandardową @CurrentUseradnotacją. Od wersji 3.2 nie musimy implementować niestandardowego narzędzia do rozwiązywania argumentów, jak w połączonej odpowiedzi. Ta inna odpowiedź ma więcej szczegółów.
Mike Partridge

5

Tak, statyka jest ogólnie zła - ogólnie, ale w tym przypadku statyczność jest najbezpieczniejszym kodem, jaki możesz napisać. Ponieważ kontekst bezpieczeństwa wiąże Principal z aktualnie działającym wątkiem, najbezpieczniejszy kod uzyskałby dostęp do statycznego z wątku tak bezpośrednio, jak to możliwe. Ukrywanie dostępu za wstrzykiwaną klasą opakowania zapewnia atakującemu więcej punktów do ataku. Nie potrzebowaliby dostępu do kodu (który byłby trudny do zmiany, gdyby jar był podpisany), po prostu potrzebują sposobu na zastąpienie konfiguracji, co można zrobić w czasie wykonywania lub wstawić trochę XML na ścieżkę klasy. Nawet użycie wstrzyknięcia adnotacji w podpisanym kodzie byłoby możliwe do zastąpienia za pomocą zewnętrznego XML. Taki XML mógłby wstrzyknąć działającemu systemowi nieuczciwą zasadę.


5

Po prostu zrobiłbym to:

request.getRemoteUser();

1
To może działać, ale nie niezawodnie. Z javadoc: „To, czy nazwa użytkownika jest wysyłana przy każdym kolejnym żądaniu, zależy od przeglądarki i rodzaju uwierzytelnienia.” - download-llnw.oracle.com/javaee/6/api/javax/servlet/http/…
Scott Bale

3
Jest to właściwie prawidłowy i bardzo prosty sposób na uzyskanie zdalnej nazwy użytkownika w aplikacji internetowej Spring Security. Standardowy łańcuch filtrów obejmuje SecurityContextHolderAwareRequestFilterzapytanie, które otacza żądanie i realizuje to wywołanie, uzyskując dostęp do SecurityContextHolder.
Shaun the Sheep

4

W ostatniej aplikacji Spring MVC, którą napisałem, nie wstrzyknąłem uchwytu SecurityContext, ale miałem podstawowy kontroler, który miałem dwie metody narzędziowe związane z tym ... isAuthenticated () i getUsername (). Wewnętrznie wykonują opisane przez ciebie wywołanie metody statycznej.

Przynajmniej wtedy jest tylko w jednym miejscu, jeśli chcesz później refaktoryzować.


3

Możesz użyć metody Spring AOP. Na przykład, jeśli masz jakąś usługę, musi znać aktualny zleceniodawca. Możesz wprowadzić niestandardową adnotację, tj. @Principal, która wskazuje, że ta usługa powinna być zależna od użytkownika.

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

Następnie w swojej radzie, którą moim zdaniem należy rozszerzyć MethodBeforeAdvice, sprawdź, czy dana usługa ma adnotację @Principal i wstrzyknij nazwę główną, lub ustaw ją zamiast tego na „ANONIMOWY”.


Muszę uzyskać dostęp do Principal w klasie usług. Czy możesz zamieścić pełny przykład na github? Nie znam wiosny AOP, stąd prośba.
Rakesh Waghela

2

Jedynym problemem jest to, że nawet po uwierzytelnieniu w Spring Security, użytkownik / główny komponent bean nie istnieje w kontenerze, więc wstrzyknięcie zależności będzie trudne. Zanim skorzystaliśmy z Spring Security, stworzyliśmy fasolę o zasięgu sesji, która miała obecnego Zleceniodawcę, wstrzyknęliśmy ją do „AuthService”, a następnie wstrzyknęliśmy tę Usługę do większości innych usług w Aplikacji. Więc te usługi po prostu wywołałyby authService.getCurrentUser (), aby uzyskać obiekt. Jeśli masz miejsce w kodzie, w którym znajduje się odwołanie do tego samego Zleceniodawcy w sesji, możesz po prostu ustawić go jako właściwość dla komponentu bean o zasięgu sesji.


1

Spróbuj tego

Uwierzytelnianie Uwierzytelnianie = SecurityContextHolder.getContext (). GetAuthentication ();
Ciąg userName = authentication.getName ();


3
Wywołanie metody statycznej SecurityContextHolder.getContext () jest dokładnie tym, na co narzekałem w pierwotnym pytaniu. Nic nie odpowiedziałeś.
Scott Bale,

2
Jest to jednak dokładnie to, co zaleca dokumentacja: static.springsource.org/spring-security/site/docs/3.0.x/... Więc co osiągasz , unikając tego? Szukasz skomplikowanego rozwiązania prostego problemu. W najlepszym wypadku - masz takie samo zachowanie. W najgorszym przypadku masz błąd lub dziurę w zabezpieczeniach.
Bob Kerns,

2
@BobKerns Do celów testowych czystsze jest możliwość wstrzyknięcia uwierzytelnienia w przeciwieństwie do umieszczania go w lokalnym wątku.

1

Najlepszym rozwiązaniem, jeśli używasz Spring 3 i potrzebujesz uwierzytelnionego podmiotu głównego w kontrolerze, to zrobić coś takiego:

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

    }

1
Dlaczego wykonujesz tę instancję sprawdzania UsernamePasswordAuthenticationToken, gdy parametr jest już typu: UsernamePasswordAuthenticationToken?
Scott Bale,

(authToken instanceof UsernamePasswordAuthenticationToken) jest funkcjonalnym odpowiednikiem if (authToken! = null). Ten ostatni może być trochę czystszy, ale w przeciwnym razie nie ma różnicy.
Mark

1

@AuthenticationPrincipalAdnotacji używam zarówno w @Controllerklasach, jak i w @ControllerAdviceradnotacjach. Dawny.:

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


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

Gdzie UserActivejest klasa, z której korzystam dla usług zalogowanych użytkowników i od której się rozszerza org.springframework.security.core.userdetails.User. Coś jak:

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
}

Naprawdę proste.


0

Zdefiniuj Principaljako zależność w metodzie kontrolera, a sprężyna wstrzykuje bieżącego uwierzytelnionego użytkownika do metody podczas wywołania.


-2

Lubię udostępniać sposób wspierania danych użytkownika na stronie Freemarker. Wszystko jest bardzo proste i działa idealnie!

Musisz tylko złożyć żądanie uwierzytelnienia default-target-url(strona po formularzu logowania) To jest moja metoda Kontrolera dla tej strony:

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

A to jest mój kod ftl:

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

I to wszystko, nazwa użytkownika pojawi się na każdej stronie po autoryzacji.


Dziękujemy za próbę odpowiedzi, ale użycie metody statycznej SecurityContextHolder.getContext () jest dokładnie tym, czego chciałem uniknąć, i powód, dla którego zadałem to pytanie w pierwszej kolejności.
Scott Bale,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.