So legen Sie einen authentifizierten Benutzer in Spring Security / SpringMVC manuell fest

107

Nachdem ein neuer Benutzer ein Formular "Neues Konto" eingereicht hat, möchte ich diesen Benutzer manuell anmelden, damit er sich nicht auf der folgenden Seite anmelden muss.

Die normale Formular-Anmeldeseite, die den Spring Security Interceptor durchläuft, funktioniert einwandfrei.

Im New-Account-Form-Controller erstelle ich ein UsernamePasswordAuthenticationToken und setze es manuell im SecurityContext:

SecurityContextHolder.getContext().setAuthentication(authentication);

Auf derselben Seite überprüfe ich später, ob der Benutzer angemeldet ist mit:

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

Dies gibt die Berechtigungen zurück, die ich zuvor in der Authentifizierung festgelegt habe. Alles ist gut.

Wenn jedoch derselbe Code auf der nächsten Seite aufgerufen wird, die ich lade, lautet das Authentifizierungstoken nur UserAnonymous.

Mir ist nicht klar, warum die bei der vorherigen Anforderung festgelegte Authentifizierung nicht beibehalten wurde. Irgendwelche Gedanken?

  • Könnte es daran liegen, dass die Sitzungs-IDs nicht richtig eingerichtet sind?
  • Gibt es etwas, das meine Authentifizierung möglicherweise irgendwie überschreibt?
  • Vielleicht brauche ich nur einen weiteren Schritt, um die Authentifizierung zu speichern?
  • Oder muss ich etwas tun, um die Authentifizierung während der gesamten Sitzung zu deklarieren, anstatt irgendwie eine einzelne Anfrage?

Ich suche nur nach Gedanken, die mir helfen könnten zu sehen, was hier passiert.

David Parks
quelle
1
Sie können meiner Antwort auf stackoverflow.com/questions/4824395/…
AlexK
2
Leser, achten Sie auf die Antworten auf diese Frage, wenn Sie dazu aufgefordert werden : SecurityContextHolder.getContext().setAuthentication(authentication). Es funktioniert und ist üblich, aber es gibt schwerwiegende Funktionsmängel, auf die Sie stoßen werden, wenn Sie dies einfach tun. Weitere Informationen finden Sie in meiner Frage und in der Antwort: stackoverflow.com/questions/47233187/…
Ziege

Antworten:

62

Ich hatte vor einiger Zeit das gleiche Problem wie Sie. Ich kann mich nicht an die Details erinnern, aber der folgende Code hat die Dinge für mich zum Laufen gebracht. Dieser Code wird in einem Spring Webflow-Flow verwendet, daher die Klassen RequestContext und ExternalContext. Der für Sie relevanteste Teil ist jedoch die doAutoLogin-Methode.

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
quelle
2
Vielen Dank, der Code ist sehr hilfreich, damit ich weiß, dass ich im richtigen Bereich Fehler behebe. Sieht so aus, als hätte ich eine rauchende Waffe. Nach der manuellen Authentifizierung wird eine neue Sitzungs-ID erstellt, aber die alte Sitzungs-ID wird immer noch anhand des Cookies identifiziert. Ich muss jetzt herausfinden, warum, aber zumindest bin ich klar auf dem richtigen Weg. Vielen Dank!
David Parks
4
Jeder, der diese Anleitung befolgt,
David Parks
14
Können Sie bitte erklären, wie Sie Authentifizierung erhaltenProvider
Vivex
1
@ s1moner3d Sie sollten in der Lage sein, es über IoC injizieren zu lassen -> \ @Autowired
Hartmut
1
@Configuration public class WebConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationProvider() throws Exception { return super.authenticationManagerBean(); } }
Slisnychyi
66

Ich konnte keine anderen vollständigen Lösungen finden und dachte, ich würde meine veröffentlichen. Dies mag ein bisschen hacken, aber es hat das Problem mit dem oben genannten Problem behoben:

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
quelle
7
+1 - Das hat mir geholfen! Mir fehlte das SPRING_SECURITY_CONTEXT-Update. ... aber wie "dreckig" ist das?
13.
12
Woher kommst du authenticationManager?
Isaac
2
authenticationManager wird in Ihrer Klasse wie folgt automatisch verdrahtet: @Autowired AuthenticationServiceImpl authenticationManager. Außerdem muss Ihre XML-Konfiguration eine Bean-Injektion enthalten, damit Spring weiß, was zu injizieren ist.
1
Wo ist die Implementierung von AuthenticationServiceImpl? Was hält diese Klasse?
Pra_A
3
Warum muss eine neue Sitzung erstellt werden? Behandelt SecurityContext das nicht?
Vlad Manuel Mureșan
17

Letztendlich die Wurzel des Problems herausgefunden.

Wenn ich den Sicherheitskontext manuell erstelle, wird kein Sitzungsobjekt erstellt. Erst wenn die Anforderung die Verarbeitung abgeschlossen hat, erkennt der Spring Security-Mechanismus, dass das Sitzungsobjekt null ist (wenn er versucht, den Sicherheitskontext in der Sitzung zu speichern, nachdem die Anforderung verarbeitet wurde).

Am Ende der Anforderung erstellt Spring Security ein neues Sitzungsobjekt und eine neue Sitzungs-ID. Diese neue Sitzungs-ID gelangt jedoch nie zum Browser, da sie am Ende der Anforderung auftritt, nachdem die Antwort an den Browser erfolgt ist. Dies führt dazu, dass die neue Sitzungs-ID (und damit der Sicherheitskontext mit meinem manuell angemeldeten Benutzer) verloren geht, wenn die nächste Anforderung die vorherige Sitzungs-ID enthält.

David Parks
quelle
4
Ehrlich gesagt fühlt sich dies mehr als alles andere wie ein Designfehler in der Frühlingssicherheit an. Es gibt viele Frameworks in anderen Sprachen, die damit kein Problem haben würden, aber Spring Security bricht einfach zusammen.
Chubbsondubs
3
und die Lösung ist?
s1moner3d
2
und was ist die Lösung?
Thiago
6

Aktivieren Sie die Debug-Protokollierung, um ein besseres Bild von den Vorgängen zu erhalten.

Mithilfe eines browserbasierten Debuggers können Sie feststellen, ob die Sitzungscookies gesetzt werden, um die in HTTP-Antworten zurückgegebenen Header anzuzeigen. (Es gibt auch andere Möglichkeiten.)

Eine Möglichkeit besteht darin, dass SpringSecurity sichere Sitzungscookies setzt und Ihre nächste angeforderte Seite eine "http" -URL anstelle einer "https" -URL enthält. (Der Browser sendet kein sicheres Cookie für eine "http" -URL.)

Stephen C.
quelle
Vielen Dank, das waren alles sehr hilfreiche und relevante Vorschläge!
David Parks
5

Die neue Filterfunktion in Servlet 2.4 verringert im Wesentlichen die Einschränkung, dass Filter nur im Anforderungsfluss vor und nach der eigentlichen Anforderungsverarbeitung durch den Anwendungsserver ausgeführt werden können. Stattdessen können Servlet 2.4-Filter jetzt an jedem Versandpunkt mit dem Anforderungs-Dispatcher interagieren. Dies bedeutet, dass, wenn eine Webressource eine Anforderung an eine andere Ressource weiterleitet (z. B. ein Servlet, das die Anforderung an eine JSP-Seite in derselben Anwendung weiterleitet), ein Filter ausgeführt werden kann, bevor die Anforderung von der Zielressource verarbeitet wird. Dies bedeutet auch, dass Servlet 2.4-Filter vor und nach jeder der enthaltenen Ressourcen arbeiten können, wenn eine Webressource die Ausgabe oder Funktion anderer Webressourcen enthält (z. B. eine JSP-Seite, die die Ausgabe mehrerer anderer JSP-Seiten enthält). .

Um diese Funktion zu aktivieren, benötigen Sie:

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
quelle
Gute Infos, aber das Einfügen des Benutzernamens und des Passworts in die URL ist schlecht. 1) Es wird kein Escape-Vorgang ausgeführt, sodass ein Benutzername oder ein Kennwort mit Sonderzeichen wahrscheinlich beschädigt oder, noch schlimmer, als Sicherheits-Exploit-Vektor verwendet wird. 2) Passwörter in URLs sind schlecht, da URLs häufig auf der Festplatte protokolliert werden, was für die Sicherheit ziemlich schlecht ist - alle Ihre Passwörter im Klartext sitzen nur dort.
Ziege
1

Ich habe versucht, eine extjs-Anwendung zu testen, und nachdem ich erfolgreich ein testingAuthenticationToken festgelegt hatte, funktionierte dies plötzlich ohne offensichtlichen Grund nicht mehr.

Ich konnte die oben genannten Antworten nicht zum Laufen bringen, daher bestand meine Lösung darin, diesen Frühling in der Testumgebung auszulassen. Ich habe eine Naht um den Frühling herum eingeführt:

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

Benutzer ist hier ein benutzerdefinierter Typ.

Ich wickle es dann in eine Klasse ein, die nur eine Option für den Testcode hat, um die Feder herauszuschalten.

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

Die Testversion sieht einfach so aus:

public class TestUserAccessor implements UserAccessor
{
    private static User _user;

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

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

Im aufrufenden Code verwende ich immer noch einen richtigen Benutzer, der aus der Datenbank geladen wurde:

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

Dies ist natürlich nicht geeignet, wenn Sie die Sicherheit tatsächlich verwenden müssen, aber ich verwende ein Setup ohne Sicherheit für die Testbereitstellung. Ich dachte, jemand anderes könnte in eine ähnliche Situation geraten. Dies ist ein Muster, mit dem ich zuvor statische Abhängigkeiten verspottet habe. Die andere Alternative ist, dass Sie die Statik der Wrapper-Klasse beibehalten können, aber ich bevorzuge diese, da die Abhängigkeiten des Codes expliziter sind, da Sie CurrentUserAccessor an Klassen übergeben müssen, in denen dies erforderlich ist.

JonnyRaa
quelle