Jak ręcznie ustawić uwierzytelnionego użytkownika w Spring Security / SpringMVC

107

Po przesłaniu przez nowego użytkownika formularza „Nowe konto” chcę ręcznie zalogować tego użytkownika, aby nie musiał się logować na kolejnej stronie.

Normalna strona logowania do formularza przechodząca przez przechwytywacz zabezpieczenia sprężyny działa dobrze.

W kontrolerze formularza nowego konta tworzę UsernamePasswordAuthenticationToken i ustawiam go ręcznie w SecurityContext:

SecurityContextHolder.getContext().setAuthentication(authentication);

Na tej samej stronie później sprawdzam, czy użytkownik jest zalogowany:

SecurityContextHolder.getContext().getAuthentication().getAuthorities();

Powoduje to zwrócenie uprawnień, które ustawiłem wcześniej podczas uwierzytelniania. Wszystko dobrze.

Ale kiedy ten sam kod jest wywoływany na następnej ładowanej stronie, token uwierzytelniający jest po prostu UserAnonymous.

Nie jestem pewien, dlaczego nie zachował uwierzytelnienia, które ustawiłem w poprzednim żądaniu. jakieś pomysły?

  • Czy może to mieć coś wspólnego z nieprawidłowym skonfigurowaniem identyfikatora sesji?
  • Czy jest coś, co prawdopodobnie nadpisuje moje uwierzytelnienie?
  • Może potrzebuję tylko kolejnego kroku, aby zapisać uwierzytelnienie?
  • A może jest coś, co muszę zrobić, aby w jakiś sposób zadeklarować uwierzytelnianie w całej sesji zamiast pojedynczego żądania?

Szukam tylko myśli, które pomogą mi zobaczyć, co się tutaj dzieje.

David Parks
źródło
1
Możesz śledzić moją odpowiedź na stackoverflow.com/questions/4824395/ ...
AlexK
2
Czytelnicy, strzeżcie się odpowiedzi na to pytanie, jeśli trzeba by zrobić: SecurityContextHolder.getContext().setAuthentication(authentication). To działa i jest powszechne, ale istnieją poważne wady w funkcjonalności, które napotkasz, jeśli to zrobisz. Aby uzyskać więcej informacji, zobacz moje pytanie i odpowiedź: stackoverflow.com/questions/47233187/…
goat

Odpowiedzi:

62

Miałem ten sam problem co ty jakiś czas temu. Nie pamiętam szczegółów, ale poniższy kod sprawił, że wszystko działało. Ten kod jest używany w przepływie Spring Webflow, stąd klasy RequestContext i ExternalContext. Jednak najbardziej istotną częścią jest metoda doAutoLogin.

public String registerUser(UserRegistrationFormBean userRegistrationFormBean,
                           RequestContext requestContext,
                           ExternalContext externalContext) {

    try {
        Locale userLocale = requestContext.getExternalContext().getLocale();
        this.userService.createNewUser(userRegistrationFormBean, userLocale, Constants.SYSTEM_USER_ID);
        String emailAddress = userRegistrationFormBean.getChooseEmailAddressFormBean().getEmailAddress();
        String password = userRegistrationFormBean.getChoosePasswordFormBean().getPassword();
        doAutoLogin(emailAddress, password, (HttpServletRequest) externalContext.getNativeRequest());
        return "success";

    } catch (EmailAddressNotUniqueException e) {
        MessageResolver messageResolvable 
                = new MessageBuilder().error()
                                      .source(UserRegistrationFormBean.PROPERTYNAME_EMAIL_ADDRESS)
                                      .code("userRegistration.emailAddress.not.unique")
                                      .build();
        requestContext.getMessageContext().addMessage(messageResolvable);
        return "error";
    }

}


private void doAutoLogin(String username, String password, HttpServletRequest request) {

    try {
        // Must be called from request filtered by Spring Security, otherwise SecurityContextHolder is not updated
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
        token.setDetails(new WebAuthenticationDetails(request));
        Authentication authentication = this.authenticationProvider.authenticate(token);
        logger.debug("Logging in with [{}]", authentication.getPrincipal());
        SecurityContextHolder.getContext().setAuthentication(authentication);
    } catch (Exception e) {
        SecurityContextHolder.getContext().setAuthentication(null);
        logger.error("Failure in autoLogin", e);
    }

}
Kevin Stembridge
źródło
2
Dziękuję, kod jest bardzo pomocny, pomagając mi wiedzieć, że rozwiązuję problemy we właściwym obszarze. Wygląda na to, że mam dymiącą broń, tworzy ona nowy identyfikator sesji po ręcznym uwierzytelnieniu, ale stary identyfikator sesji jest nadal identyfikowany na podstawie pliku cookie. Muszę teraz dowiedzieć się, dlaczego, ale przynajmniej jestem na dobrej drodze. Dzięki!
David Parks
4
Każdy, kto postępuje zgodnie z tymi wskazówkami, powinien również zapoznać się z tym powiązanym problemem: stackoverflow.com/questions/4824395/…
David Parks
14
Czy możesz wyjaśnić, w jaki sposób uzyskujesz
dostawcę
1
@ s1moner3d powinieneś być w stanie wstrzyknąć go przez IoC -> \ @Autowired
Hartmut
1
@Configuration public class WebConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationProvider() throws Exception { return super.authenticationManagerBean(); } }
slisnychyi
66

Nie mogłem znaleźć innych pełnych rozwiązań, więc pomyślałem, że opublikuję swoje. To może być trochę hackowanie, ale rozwiązało problem powyższego problemu:

public void login(HttpServletRequest request, String userName, String password)
{

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userName, password);

    // Authenticate the user
    Authentication authentication = authenticationManager.authenticate(authRequest);
    SecurityContext securityContext = SecurityContextHolder.getContext();
    securityContext.setAuthentication(authentication);

    // Create a new session and add the security context.
    HttpSession session = request.getSession(true);
    session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
}
Stuart McIntyre
źródło
7
+1 - To mi pomogło! Brakowało mi aktualizacji SPRING_SECURITY_CONTEXT. ... Ale czy to "brudne"?
l3dx
12
gdzie można dostać authenticationManagerod?
Izaak
2
authenticationManager jest automatycznie przypisywana w twojej klasie, tak jak ta @Autowired AuthenticationServiceImpl authenticationManager. W konfiguracji xml musi być również wstrzyknięcie fasoli, aby Spring wiedział, co wstrzyknąć.
1
gdzie jest implementacja AuthenticationServiceImpl? Co zawiera ta klasa?
Pra_A
3
Dlaczego konieczne jest utworzenie nowej sesji? Czy SecurityContext tego nie obsługuje?
Vlad Manuel Mureșan
17

Ostatecznie odkryłem źródło problemu.

Gdy ręcznie tworzę kontekst zabezpieczeń, nie jest tworzony żaden obiekt sesji. Dopiero po zakończeniu przetwarzania żądania mechanizm Spring Security zdaje sobie sprawę, że obiekt sesji jest pusty (gdy próbuje zapisać kontekst bezpieczeństwa w sesji po przetworzeniu żądania).

Pod koniec żądania Spring Security tworzy nowy obiekt sesji i identyfikator sesji. Jednak ten nowy identyfikator sesji nigdy nie trafia do przeglądarki, ponieważ pojawia się na końcu żądania, po udzieleniu odpowiedzi przeglądarce. Powoduje to utratę nowego identyfikatora sesji (a tym samym kontekstu zabezpieczeń zawierającego mojego ręcznie zalogowanego użytkownika), gdy następne żądanie zawiera identyfikator poprzedniej sesji.

David Parks
źródło
4
Szczerze mówiąc, bardziej niż cokolwiek innego wydaje się to jak wada konstrukcyjna w zabezpieczeniach wiosennych. Istnieje wiele frameworków napisanych w innych językach, które nie miałyby z tym problemu, ale Spring Security po prostu się psuje.
chubbsondubs
3
a rozwiązaniem jest?
s1moner3d
2
i jakie jest rozwiązanie?
Thiago
6

Włącz rejestrowanie debugowania, aby uzyskać lepszy obraz tego, co się dzieje.

Możesz sprawdzić, czy pliki cookie sesji są ustawiane, używając debugera po stronie przeglądarki do przeglądania nagłówków zwracanych w odpowiedziach HTTP. (Są też inne sposoby.)

Jedną z możliwości jest to, że SpringSecurity ustawia pliki cookie sesji bezpiecznej, a następna żądana strona ma adres URL „http” zamiast adresu URL „https”. (Przeglądarka nie wyśle ​​bezpiecznego pliku cookie dla adresu URL „http”).

Stephen C.
źródło
Dzięki temu wszystkie były bardzo pomocne i trafne sugestie!
David Parks,
5

Nowa funkcja filtrowania w Servlecie 2.4 zasadniczo łagodzi ograniczenie, że filtry mogą działać tylko w przepływie żądań przed i po rzeczywistym przetworzeniu żądania przez serwer aplikacji. Zamiast tego, filtry Servlet 2.4 mogą teraz współdziałać z dyspozytorem żądań w każdym punkcie wysyłania. Oznacza to, że gdy zasób WWW przekazuje żądanie do innego zasobu (na przykład serwlet przekazujący żądanie do strony JSP w tej samej aplikacji), filtr może działać, zanim żądanie zostanie obsłużone przez docelowy zasób. Oznacza to również, że jeśli zasób sieciowy zawiera dane wyjściowe lub funkcję z innych zasobów sieci Web (na przykład strona JSP zawierająca dane wyjściowe z wielu innych stron JSP), filtry Servlet 2.4 mogą działać przed i po każdym z uwzględnionych zasobów. .

Aby włączyć tę funkcję, potrzebujesz:

web.xml

<filter>   
    <filter-name>springSecurityFilterChain</filter-name>   
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 
</filter>  
<filter-mapping>   
    <filter-name>springSecurityFilterChain</filter-name>   
    <url-pattern>/<strike>*</strike></url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>

RegistrationController

return "forward:/login?j_username=" + registrationModel.getUserEmail()
        + "&j_password=" + registrationModel.getPassword();
AlexK
źródło
Dobra informacja, ale umieszczanie nazwy użytkownika i hasła w adresie URL jest złe. 1) żadna ucieczka nie jest wykonywana, więc nazwa użytkownika lub hasło ze specjalnym znakiem może się zepsuć lub, co gorsza, zostać użyte jako wektor exploita bezpieczeństwa. 2) hasła w adresach URL są złe, ponieważ adresy URL często są rejestrowane na dysku, co jest bardzo niekorzystne dla bezpieczeństwa - wszystkie hasła w postaci zwykłego tekstu po prostu tam znajdują się.
koza
1

Próbowałem przetestować aplikację extjs i po pomyślnym ustawieniu testingAuthenticationToken nagle przestało działać bez wyraźnej przyczyny.

Nie udało mi się uzyskać powyższych odpowiedzi, więc moim rozwiązaniem było pominięcie tej wiosny w środowisku testowym. W okolicach wiosny wprowadziłem szew w ten sposób:

public class SpringUserAccessor implements UserAccessor
{
    @Override
    public User getUser()
    {
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();
        return (User) authentication.getPrincipal();
    }
}

Użytkownik jest tutaj typem niestandardowym.

Następnie opakowuję go w klasę, która ma tylko opcję przełączania kodu testowego.

public class CurrentUserAccessor
{
    private static UserAccessor _accessor;

    public CurrentUserAccessor()
    {
        _accessor = new SpringUserAccessor();
    }

    public User getUser()
    {
        return _accessor.getUser();
    }

    public static void UseTestingAccessor(User user)
    {
        _accessor = new TestUserAccessor(user);
    }
}

Wersja testowa wygląda tak:

public class TestUserAccessor implements UserAccessor
{
    private static User _user;

    public TestUserAccessor(User user)
    {
        _user = user;
    }

    @Override
    public User getUser()
    {
        return _user;
    }
}

W kodzie wywołującym nadal używam właściwego użytkownika załadowanego z bazy danych:

    User user = (User) _userService.loadUserByUsername(username);
    CurrentUserAccessor.UseTestingAccessor(user);

Oczywiście nie będzie to odpowiednie, jeśli faktycznie potrzebujesz użyć zabezpieczeń, ale używam konfiguracji bez zabezpieczeń do wdrożenia testowego. Myślałem, że ktoś inny może znaleźć się w podobnej sytuacji. To jest wzorzec, którego użyłem wcześniej do wyśmiewania statycznych zależności. Inną alternatywą jest zachowanie statyczności klasy opakowania, ale ja wolę tę, ponieważ zależności kodu są bardziej wyraźne, ponieważ musisz przekazać CurrentUserAccessor do klas, w których jest to wymagane.

JonnyRaa
źródło