web-dev-qa-db-fra.com

Implémenter AuthenticationProvider personnalisé dans Spring Security 2.06

J'utilise Spring Security pour sécuriser une application Web Struts2. En raison des contraintes du projet, j'utilise Spring Security 2.06.

Mon équipe a créé une API de gestion des utilisateurs personnalisée qui authentifie un utilisateur après avoir saisi les paramètres de nom d'utilisateur et de mot de passe, et renvoie un objet utilisateur personnalisé contenant une liste de rôles et d'autres attributs comme l'e-mail, le nom, etc.

D'après ma compréhension, le cas d'utilisation typique de Spring Security utilise un UserDetailsService par défaut pour récupérer un objet UserDetails; cet objet contiendra (entre autres) un champ de mot de passe qui sera utilisé par le framework pour authentifier l'utilisateur.

Dans mon cas, je veux laisser notre API personnalisée faire l'authentification, puis renvoyer un objet UserDetails personnalisé contenant les rôles et autres attributs (e-mail, etc.).

Après quelques recherches, j'ai compris que je pouvais le faire grâce à une implémentation personnalisée d'AuthenticationProvider. J'ai également des implémentations personnalisées de UserDetailsService et UserDetails.

Mon problème est que je ne comprends pas vraiment ce que je suis censé retourner dans CustomAuthenticationProvider. Dois-je utiliser mon objet UserDetailsService personnalisé ici? Est-ce même nécessaire? Désolé, je suis vraiment confus.

CustomAuthenticationProvider:

public class CustomAuthenticationProvider implements AuthenticationProvider {

private Logger logger = Logger.getLogger(CustomAuthenticationProvider.class);

private UserDetailsService userDetailsService; //what am i supposed to do with this?

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
    String username = String.valueOf(auth.getPrincipal());
    String password = String.valueOf(auth.getCredentials());

    logger.info("username:" + username);
    logger.info("password:" + password);
    /* what should happen here? */

    return null;  //what do i return?
}

@Override
public boolean supports(Class aClass) {
    return true;  //To indicate that this authenticationprovider can handle the auth request. since there's currently only one way of logging in, always return true
}

public UserDetailsService getUserDetailsService() {
    return userDetailsService;
}

public void setUserDetailsService(UserDetailsService userDetailsService) {
    this.userDetailsService = userDetailsService;
}

}

applicationContext-security.xml:

<beans:bean id="customUserDetailsService" scope="prototype" class="com.test.testconsole.security.CustomUserDetailsService"/>

<beans:bean id="customAuthenticationProvider" class="com.test.testconsole.security.CustomAuthenticationProvider">
    <custom-authentication-provider />
    <beans:property name="userDetailsService" ref="customUserDetailsService" />
</beans:bean>

Pour résumer, voici ce dont j'ai besoin:

  1. L'utilisateur se connecte via un formulaire Web
  2. Authentifier l'utilisateur à l'aide de l'API de gestion des utilisateurs en interne
  3. Pour les utilisateurs authentifiés avec succès, remplissez GrantedAuthories, etc.
  4. Renvoyer une entité utilisateur contenant des rôles/autorités et d'autres attributs comme le courrier électronique, le nom, etc. Je devrais alors être en mesure d'accéder à cet objet comme ça ..

    //spring security get user name
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    userName = auth.getName(); //get logged in username
    logger.info("username: " + userName);
    
    //spring security get user role
    GrantedAuthority[] authorities = auth.getAuthorities();
    userRole = authorities[0].getAuthority();
    logger.info("user role: " + userRole);
    

J'espère que cela a du sens. Toute aide ou pointeur sera apprécié!

Merci!

Mise à jour:

J'ai fait des progrès, je pense.

J'ai un objet d'authentification personnalisé implémentant l'interface d'authentification:

public class CustomAuthentication implements Authentication {

    String name;
    GrantedAuthority[] authorities;
    Object credentials;
    Object details;
    Object principal;
    boolean authenticated;

    public CustomAuthentication(String name, GrantedAuthority[] authorities, Object credentials, Object details, Object principal, boolean
                                authenticated){
        this.name=name;
        this.authorities=authorities;
        this.details=details;
        this.principal=principal;
        this.authenticated=authenticated;

    }
    @Override
    public GrantedAuthority[] getAuthorities() {
        return new GrantedAuthority[0];  //To change body of implemented methods use File | Settings | File Templates.
    }

    @Override
    public Object getCredentials() {
        return null;  //To change body of implemented methods use File | Settings | File Templates.
    }

    @Override
    public Object getDetails() {
        return null;  //To change body of implemented methods use File | Settings | File Templates.
    }

    @Override
    public Object getPrincipal() {
        return null;  //To change body of implemented methods use File | Settings | File Templates.
    }

    @Override
    public boolean isAuthenticated() {
        return false;  //To change body of implemented methods use File | Settings | File Templates.
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        //To change body of implemented methods use File | Settings | File Templates.
    }

    @Override
    public String getName() {
        return null;  
    }
}

et mis à jour ma classe CustomerAuthenticationProvider:

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
        String username = String.valueOf(auth.getPrincipal());
        String password = String.valueOf(auth.getCredentials());

        logger.info("username:" + username);
        logger.info("password:" + password);

        //no actual validation done at this time

        GrantedAuthority[] authorities = new GrantedAuthorityImpl[1];
        authorities[0] = new GrantedAuthorityImpl("ROLE_USER");

        CustomAuthentication customAuthentication = new CustomAuthentication("TestMerchant",authorities,"details",username,password,true);

    return customAuthentication;

    //return new UsernamePasswordAuthenticationToken(username,password,authorities); 
}

Cela fonctionne si je renvoie un objet UsernamePasswordAuthenticationToken, mais si j'essaie de retourner CustomAuthentication, j'obtiens l'erreur suivante:

Java.lang.ClassCastException: com.test.testconsole.security.CustomAuthentication cannot be cast to org.springframework.security.providers.UsernamePasswordAuthenticationToken
    at com.test.testconsole.security.CustomAuthenticationProvider.authenticate(CustomAuthenticationProvider.Java:27)
    at org.springframework.security.providers.ProviderManager.doAuthentication(ProviderManager.Java:188)
    at org.springframework.security.AbstractAuthenticationManager.authenticate(AbstractAuthenticationManager.Java:46)
    at org.springframework.security.intercept.AbstractSecurityInterceptor.authenticateIfRequired(AbstractSecurityInterceptor.Java:319)
    at org.springframework.security.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.Java:258)
    at org.springframework.security.intercept.web.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.Java:106)
    at org.springframework.security.intercept.web.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.Java:83)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.Java:390)
    at org.springframework.security.ui.SessionFixationProtectionFilter.doFilterHttp(SessionFixationProtectionFilter.Java:67)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.Java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.Java:390)
    at org.springframework.security.ui.ExceptionTranslationFilter.doFilterHttp(ExceptionTranslationFilter.Java:101)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.Java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.Java:390)
    at org.springframework.security.providers.anonymous.AnonymousProcessingFilter.doFilterHttp(AnonymousProcessingFilter.Java:105)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.Java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.Java:390)
    at org.springframework.security.ui.rememberme.RememberMeProcessingFilter.doFilterHttp(RememberMeProcessingFilter.Java:116)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.Java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.Java:390)
    at org.springframework.security.wrapper.SecurityContextHolderAwareRequestFilter.doFilterHttp(SecurityContextHolderAwareRequestFilter.Java:91)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.Java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.Java:390)
    at org.springframework.security.ui.basicauth.BasicProcessingFilter.doFilterHttp(BasicProcessingFilter.Java:174)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.Java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.Java:390)
    at org.springframework.security.ui.AbstractProcessingFilter.doFilterHttp(AbstractProcessingFilter.Java:278)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.Java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.Java:390)
    at org.springframework.security.ui.logout.LogoutFilter.doFilterHttp(LogoutFilter.Java:89)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.Java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.Java:390)
    at org.springframework.security.context.HttpSessionContextIntegrationFilter.doFilterHttp(HttpSessionContextIntegrationFilter.Java:235)
    at org.springframework.security.ui.SpringSecurityFilter.doFilter(SpringSecurityFilter.Java:53)
    at org.springframework.security.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.Java:390)
    at org.springframework.security.util.FilterChainProxy.doFilter(FilterChainProxy.Java:175)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.Java:236)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.Java:167)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.Java:1157)
    at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.Java:388)
    at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.Java:216)
    at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.Java:182)
    at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.Java:765)
    at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.Java:418)
    at org.mortbay.jetty.handler.ContextHandlerCollection.handle(ContextHandlerCollection.Java:230)
    at org.mortbay.jetty.handler.HandlerCollection.handle(HandlerCollection.Java:114)
    at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.Java:152)
    at org.mortbay.jetty.Server.handle(Server.Java:326)
    at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.Java:536)
    at org.mortbay.jetty.HttpConnection$RequestHandler.headerComplete(HttpConnection.Java:915)
    at org.mortbay.jetty.HttpParser.parseNext(HttpParser.Java:539)
    at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.Java:212)
    at org.mortbay.jetty.HttpConnection.handle(HttpConnection.Java:405)
    at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.Java:409)
    at org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.Java:582)

C'est comme si quelque chose attend non seulement n'importe quel objet d'authentification, mais une implémentation spécifique de celui-ci - UsernamePasswordAuthenticationToken. Cela me fait penser que je manque peut-être un autre composant personnalisé .. peut-être un filtre?

36
shaunlim

Si vous implémentez votre propre AuthenticationProvider, vous n'avez pas besoin d'implémenter un UserDetailsService si vous ne le souhaitez pas. UserDetailsService fournit juste un DAO standard pour charger les informations utilisateur et quelques autres classes dans le framework sont implémentées pour l'utiliser.

Normalement, pour vous authentifier à l'aide d'un nom d'utilisateur et d'un mot de passe, vous instanciez un DaoAuthenticationProvider et l'injectez avec un UserDetailsService. C'est peut-être toujours votre meilleure approche. Si vous implémentez votre propre fournisseur, vous assumez la responsabilité de vous assurer que l'utilisateur a fourni le mot de passe correct, etc. Cependant, dans certains cas, il s'agit d'une approche plus simple.

Pour répondre à "ce qui devrait arriver ici?" commenter dans votre code, ce serait quelque chose comme

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
  String username = String.valueOf(auth.getPrincipal());
  String password = String.valueOf(auth.getCredentials());

  logger.info("username:" + username);
  logger.info("password:" + password); // Don't log passwords in real app

  // 1. Use the username to load the data for the user, including authorities and password.
  YourUser user = ....

  // 2. Check the passwords match (should use a hashed password here).
  if (!user.getPassword().equals(password)) {
    throw new BadCredentialsException("Bad Credentials");
  }

  // 3. Preferably clear the password in the user object before storing in authentication object
  user.clearPassword();

  // 4. Return an authenticated token, containing user data and authorities  

  return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()) ;
}

L'objet utilisateur sera alors accessible à l'aide du

Authentication.getPrincipal()

et vous pouvez accéder aux propriétés supplémentaires (e-mail, etc.) en les castant dans votre implémentation utilisateur personnalisée.

La façon dont vous chargez les données utilisateur dépend de vous. Tout ce dont Spring Security se soucie ici est l'interface AuthenticationProvider.

Vous devez également stocker les mots de passe hachés et valider le mot de passe fourni en utilisant le même algorithme, plutôt qu'une simple vérification d'égalité.

50
Shaun the Sheep

merci d'avoir posté ce Luke!

M'a sauvé de plus de lésions cérébrales.

La seule chose à noter que j'ai rencontrée, pour tous ceux qui s'en soucient:

Ma configuration:

  • Grails 2.0.4
  • Groovy 1.8
  • spring-security-core 1.2.7.3
  • spring-security-ui 0.2
  • hibernate 2.0.4

Lors de l'utilisation de l'approche grandement appréciée simplifiée/élégante suggérée par Luke, NE PAS implémenter un objet UserDetails (ou UserDetailsService) personnalisé -et- en utilisant votre propre utilisateur domaine objet qui ne prolonge rien de spécial, vous devez prendre une étape supplémentaire si vous utilisez les balises personnalisées "sec" de Spring Security (dans vos pages bien sûr):

Lorsque vous instanciez un UsernamePasswordAuthenticationToken de base non personnalisé, vous DEVEZ lui transmettre une instanciation de quelque chose qui étend Principal, encore une fois, si vous voulez que vos balises d'espacement personnalisées de sécurité de printemps fonctionnent. J'ai fait quelque chose comme ça, pour que ce soit aussi simple que possible (en référençant les valeurs des objets de mon domaine utilisateur lorsque cela est utile/approprié):

def principalUser = new org.springframework.security.core.userdetails.User(user.username, user.password, user.enabled, !user.accountExpired, !user.passwordExpired,!user.accountLocked, authorities)
def token = new UsernamePasswordAuthenticationToken(principalUser, presentedPassword, authorities)

Cela devrait satisfaire aux conditions testées dans grails.plugins.springsecurity.SecurityTagLib.determineSource () donc, vous savez, vos pages qui utilisent <sec:loggedInUserInfo> rendra en fait:

if (principal.metaClass.respondsTo(principal, 'getDomainClass')) {
            return principal.domainClass
}

Sinon, si vous instanciez le UsernamePasswordAuthenticationToken avec votre objet de domaine utilisateur (comme Luke le montre dans son exemple), cette méthode lib de balise de sécurité (determineSource ()) fera simplement son meilleur niveau et renverra la valeur (méta) de org.codehaus.groovy.grails.commons.DefaultGrailsDomainClass et vous obtiendrez une erreur lorsque la balise cherchera la variable membre du nom d'utilisateur indiquant:

 Error executing tag <sec:ifLoggedIn>: Error executing tag <sec:loggedInUserInfo>: No such property: username for class: org.codehaus.groovy.grails.commons.DefaultGrailsDomainClass

À moins de réimplémenter/sous-classer les balises de plug-in Spring-security-core dans mon projet Grails, il n'y a tout simplement aucun moyen d'utiliser les balises ET d'utiliser votre classe d'utilisateur de domaine personnalisé pour instancier le jeton transmis de votre filtre à votre fournisseur.

Là encore, une ligne de code supplémentaire est un très petit prix à payer :)

3
jasonwhite01