Comment définir manuellement un utilisateur authentifié dans Spring Security / SpringMVC

107

Une fois qu'un nouvel utilisateur a soumis un formulaire «Nouveau compte», je souhaite connecter manuellement cet utilisateur afin qu'il n'ait pas à se connecter sur la page suivante.

La page de connexion sous forme normale passant par l'intercepteur de sécurité Spring fonctionne très bien.

Dans le contrôleur de formulaire de nouveau compte, je crée un UsernamePasswordAuthenticationToken et le configure manuellement dans SecurityContext:

SecurityContextHolder.getContext().setAuthentication(authentication);

Sur cette même page, je vérifie plus tard que l'utilisateur est connecté avec:

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

Cela renvoie les autorités que j'ai définies plus tôt dans l'authentification. Tout est bien.

Mais lorsque ce même code est appelé sur la page suivante que je charge, le jeton d'authentification est simplement UserAnonymous.

Je ne sais pas pourquoi il n'a pas conservé l'authentification que j'ai définie sur la demande précédente. Des pensées?

  • Cela pourrait-il être dû au fait que l'ID de session n'est pas configuré correctement?
  • Y a-t-il quelque chose qui écrase peut-être mon authentification?
  • Peut-être ai-je juste besoin d'une autre étape pour enregistrer l'authentification?
  • Ou dois-je faire quelque chose pour déclarer l'authentification sur toute la session plutôt qu'une seule demande?

Je cherche juste quelques pensées qui pourraient m'aider à voir ce qui se passe ici.

David Parks
la source
1
Vous pouvez suivre ma réponse sur stackoverflow.com/questions/4824395/…
AlexK
2
Les lecteurs, méfiez - vous des réponses à cette question si on vous dit de le faire: SecurityContextHolder.getContext().setAuthentication(authentication). Cela fonctionne et est courant, mais vous rencontrerez de graves lacunes de fonctionnalité si vous le faites simplement. Pour plus d'informations, voir ma question et la réponse: stackoverflow.com/questions/47233187/…
chèvre

Réponses:

62

J'ai eu le même problème que vous il y a quelque temps. Je ne me souviens pas des détails, mais le code suivant a fait fonctionner les choses pour moi. Ce code est utilisé dans un flux Spring Webflow, d'où les classes RequestContext et ExternalContext. Mais la partie la plus pertinente pour vous est la méthode 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
la source
2
Merci, le code est très utile pour m'aider à savoir que je dépanne dans la bonne zone. On dirait que j'ai un pistolet fumant, il crée un nouvel identifiant de session après l'authentification manuelle, mais l'ancien identifiant de session est toujours identifié à partir du cookie. Je dois comprendre pourquoi maintenant, mais au moins je suis clairement sur la bonne voie. Merci!
David Parks
4
Toute personne qui suit ces instructions devrait également voir ce problème connexe: stackoverflow.com/questions/4824395/…
David Parks
14
Pouvez-vous s'il vous plaît expliquer comment vous obtenez l'authentificationProvider
Vivex
1
@ s1moner3d vous devriez pouvoir l'injecter via IoC -> \ @Autowired
Hartmut
1
@Configuration public class WebConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationProvider() throws Exception { return super.authenticationManagerBean(); } }
slisnychyi
66

Je n'ai pas trouvé d'autres solutions complètes, alors j'ai pensé publier la mienne. Cela peut être un peu un hack, mais cela a résolu le problème au problème ci-dessus:

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
la source
7
+1 - Cela m'a aidé! Il me manquait la mise à jour SPRING_SECURITY_CONTEXT. ... Mais à quel point est-ce «sale»?
l3dx
12
d'où viens-tu authenticationManager?
Isaac
2
authenticationManager est automatiquement câblé dans votre classe comme ceci @Autowired AuthenticationServiceImpl authenticationManager. Et il doit également y avoir une injection de bean dans votre configuration xml, pour que Spring sache quoi injecter.
1
où est l'implémentation d'AuthenticationServiceImpl? Que contient cette classe?
Pra_A
3
Pourquoi est-il nécessaire de créer une nouvelle session? SecurityContext ne gère-t-il pas cela?
Vlad Manuel Mureșan
17

Finalement compris la racine du problème.

Lorsque je crée le contexte de sécurité manuellement, aucun objet de session n'est créé. Ce n'est qu'à la fin du traitement de la demande que le mécanisme de sécurité Spring se rend compte que l'objet de session est nul (lorsqu'il essaie de stocker le contexte de sécurité dans la session après que la demande a été traitée).

À la fin de la demande, Spring Security crée un nouvel objet de session et un nouvel ID de session. Cependant, ce nouvel ID de session ne parvient jamais au navigateur car il se produit à la fin de la demande, après que la réponse au navigateur a été faite. Cela entraîne la perte du nouvel ID de session (et donc du contexte de sécurité contenant mon utilisateur connecté manuellement) lorsque la demande suivante contient l'ID de session précédent.

David Parks
la source
4
Honnêtement, cela ressemble plus que tout à un défaut de conception dans la sécurité du ressort. Il existe de nombreux frameworks écrits dans d'autres langages qui n'auraient aucun problème avec cela, mais Spring Security casse simplement.
chubbsondubs
3
et la solution est?
s1moner3d
2
et quelle est la solution?
Thiago
6

Activez la journalisation du débogage pour obtenir une meilleure image de ce qui se passe.

Vous pouvez savoir si les cookies de session sont définis en utilisant un débogueur côté navigateur pour examiner les en-têtes renvoyés dans les réponses HTTP. (Il existe également d'autres moyens.)

Une possibilité est que SpringSecurity configure des cookies de session sécurisés, et votre prochaine page demandée a une URL "http" au lieu d'une URL "https". (Le navigateur n'enverra pas de cookie sécurisé pour une URL "http".)

Stephen C
la source
Merci, toutes ces suggestions étaient très utiles et pertinentes!
David Parks
5

La nouvelle fonctionnalité de filtrage de Servlet 2.4 allège fondamentalement la restriction selon laquelle les filtres ne peuvent fonctionner que dans le flux de requêtes avant et après le traitement réel des requêtes par le serveur d'applications. Au lieu de cela, les filtres Servlet 2.4 peuvent désormais interagir avec le répartiteur de requêtes à chaque point de répartition. Cela signifie que lorsqu'une ressource Web transmet une requête à une autre ressource (par exemple, un servlet transmettant la requête à une page JSP dans la même application), un filtre peut fonctionner avant que la requête ne soit traitée par la ressource ciblée. Cela signifie également que si une ressource Web inclut la sortie ou la fonction d'autres ressources Web (par exemple, une page JSP comprenant la sortie de plusieurs autres pages JSP), les filtres Servlet 2.4 peuvent fonctionner avant et après chacune des ressources incluses. .

Pour activer cette fonctionnalité, vous devez:

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>

EnregistrementContrôleur

return "forward:/login?j_username=" + registrationModel.getUserEmail()
        + "&j_password=" + registrationModel.getPassword();
AlexK
la source
Bonnes informations, mais mettre le nom d'utilisateur et le mot de passe dans l'URL est mauvais. 1) aucun échappement n'est fait, donc un nom d'utilisateur ou un mot de passe avec un caractère spécial est susceptible de casser, ou pire encore, d'être utilisé comme vecteur d'exploit de sécurité. 2) les mots de passe dans les URL sont mauvais parce que les URL sont souvent enregistrées sur le disque, ce qui est plutôt mauvais pour la sécurité - tous vos mots de passe en texte clair se trouvent juste là.
chèvre
1

J'essayais de tester une application extjs et après avoir défini avec succès un testingAuthenticationToken, cela a soudainement cessé de fonctionner sans cause évidente.

Je ne pouvais pas faire fonctionner les réponses ci-dessus, donc ma solution a été de sauter ce bout de printemps dans l'environnement de test. J'ai introduit une couture autour du ressort comme ceci:

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

L'utilisateur est un type personnalisé ici.

Je l'enveloppe ensuite dans une classe qui a juste une option pour que le code de test change de ressort.

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

La version de test ressemble à ceci:

public class TestUserAccessor implements UserAccessor
{
    private static User _user;

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

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

Dans le code d'appel, j'utilise toujours un utilisateur approprié chargé à partir de la base de données:

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

Évidemment, cela ne conviendra pas si vous avez réellement besoin d'utiliser la sécurité, mais que je fonctionne avec une configuration sans sécurité pour le déploiement de test. Je pensais que quelqu'un d'autre pourrait se retrouver dans une situation similaire. C'est un modèle que j'ai déjà utilisé pour se moquer des dépendances statiques. L'autre alternative est que vous pouvez maintenir la statique de la classe wrapper mais je préfère celle-ci car les dépendances du code sont plus explicites car vous devez passer CurrentUserAccessor dans les classes où cela est nécessaire.

JonnyRaa
la source