lundi 19 avril 2010

GWT et Spring Security (ex ACEGI)



Ce billet présentera comment rapidement sécuriser une application GWT (SmartGWT en fait) en utilisant un formulaire d'authentification géré par GWT et non une page web avec un formulaire HTML classique.

L'application est developpée avec les outils suivants :

Le projet GWT-ent propose une intégration avec Spring Security mais seulement avec la version 2.5 et la qualité du code laisse à désirer...
De plus, avec les dernières versions de Spring Security la configuration a été grandement simplifiée!

Configuration du projet avec Maven

-- POM.xml --

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-config</artifactId>
  <version>3.0.2.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-core</artifactId>
  <version>3.0.2.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-web</artifactId>
  <version>3.0.2.RELEASE</version>
</dependency>

Configuration de l'application web

-- web.xml --

<web-app id="gwtSecurity">

  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
      classpath:applicationContext.xml
      classpath:applicationContext-security.xml
    </param-value>
  </context-param>

  <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>/*</url-pattern>
  </filter-mapping>

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <listener>
    <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
  </listener>

  <servlet>
    <servlet-name>rpc-dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>rpc-dispatcher</servlet-name>
    <url-pattern>*.rpc</url-pattern>
  </servlet-mapping>

  [...]

</web-app>

Configuration de Spring Security

Ici, nous indiquons à Spring d'utiliser notre UserService pour accéder aux utilisateurs ainsi que le SHA pour crypter les mots de passe.
Nous ne protégeons aucune URL puisque notre application est hébergée sur une unique page.
Nous avons été obligé de redéfinir le RememberMeServices pour forcer le alwaysRemember à vrai. par défaut Spring s'attend à recevoir un paramètre dans l'URL qui lui demanderait de se souvenir de cet utilisateur lors du prochain accès. Comme nous utilisons des appels RPC, nous ne pouvons pas passer ce paramètre dans le request.

-- applicationContext-security.xml --

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:security="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
  http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd">

  <security:authentication-manager alias="authenticationManager">
    <security:authentication-provider user-service-ref="userService">
      <security:password-encoder hash="sha" />
    </security:authentication-provider>
  </security:authentication-manager>

  <security:http auto-config="true">
    <security:intercept-url pattern="/*" access="IS_AUTHENTICATED_ANONYMOUSLY" />
    <security:intercept-url pattern="/css/**" filters="none" />
    <security:intercept-url pattern="/images/**" filters="none" />
    <security:logout logout-url="/logout" logout-success-url="/gwt.html" />
    <security:remember-me key="gwtSecurity" />
  </security:http>

  <bean id="rememberMeServices" class="org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
    <property name="alwaysRemember" value="true" />
    <property name="userDetailsService" ref="userService" />
    <property name="key" value="gwtSecurity" />
  </bean>

</beans>

Configuration de la servlet pour les appels RPC

On utilise ici Gilead et GWT-SL.
-- rpc-dispatcher-servlet.xml --

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

  <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
      <map>
        <entry key="/user.rpc" value-ref="userRemoteService" />
        <entry key="/security.rpc" value-ref="securityRemoteService" />
        [...]
      </map>
    </property>
  </bean>

  <bean id="abstractGileadRPCServiceExporter" class="org.gwtwidgets.server.spring.gilead.GileadRPCServiceExporter" abstract="true">
    <property name="beanManager" ref="persistentBeanManager" />
  </bean>

  <bean id="userRemoteService" parent="abstractGileadRPCServiceExporter">
    <property name="service" ref="userService" />
    <property name="serviceInterfaces" value="service.user.UserRemoteService" />
  </bean>

  <bean id="securityRemoteService" parent="abstractGileadRPCServiceExporter">
    <property name="service" ref="securityService" />
    <property name="serviceInterfaces" value="service.security.SecurityRemoteService" />
  </bean>

  [...]

</beans>

Implementation du service d'authentification

Le Authentication Manager de Spring doit pouvoir récupérer un utilisateur par son nom d'usager. Ici c'est la class UserService qui implémentera l'interface org.springframework.security.core.userdetails.UserDetailsService pour pouvoir être utilisé par le Authentication Manager de Spring.

@Service("userService")
@Transactional(readOnly = true)
public class UserServiceImpl implements UserService, UserDetailsService {

  [...]

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
    if (StringUtils.isBlank(username)) throw new UsernameNotFoundException(username);

    User user = findByLogin(username);
    if (user == null) throw new UsernameNotFoundException(username);

    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(user.getPermissions().size());
    for (String perm : user.getPermissions()) {
      authorities.add(new GrantedAuthorityImpl(perm));
    }

    boolean enabled = user.isActive();
    boolean accountNonExpired = true;
    boolean accountNonLocked = true;
    boolean credentialsNonExpired = true;
    return new org.springframework.security.core.userdetails.User(username, user.getPassword(), enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
  }

}

Dans notre cas, on ne vas pas d'authentifier par un formulaire HTML qui posterait vers une adresse géré par Spring Security car tout est géré en Javascript et on ne veut pas quitter la page courante.
Il va donc falloir définir un service que l'on pourra appeler via RPC pour ouvrir une session. Ce service s'occupera de gérer aussi le "Remember Me".

@Service("securityService")
public class SecurityServiceImpl implements SecurityService {

  @Resource
  private AuthenticationManager authenticationManager;

  @Resource
  private RememberMeServices rememberMeServices;

  @Override
  public boolean login(String username, String password, boolean rememberMe) {
    try {
      Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
      SecurityContextHolder.getContext().setAuthentication(authentication);
      if (rememberMe) {
        rememberMeServices.loginSuccess(ServletUtils.getRequest(), ServletUtils.getResponse(), authentication);
      }      
      return authentication.isAuthenticated();
    } catch (AuthenticationException e) {
      if (rememberMe) {
        rememberMeServices.loginFail(ServletUtils.getRequest(), ServletUtils.getResponse());
      }
      return false;
    }
  }

  [...]

}

Finalement, il en reste plus qu'à notre interface (ici avec SmartGWT) à appeler notre service d'authentification :

public class LoginForm extends DynamicForm {

  private SecurityRemoteServiceAsync securityService = GWT.create(SecurityRemoteService.class);

  TextItem login;
  PasswordItem password;
  CheckboxItem rememberMe;

  [...]

  void login() {
    securityService.login(username, password, remeberMe,
        new AsyncCallback<Boolean>() {
          public void onFailure(Throwable caught) {
            loginFailed();
          }

          public void onSuccess(Boolean success) {
            if (success) {
              // user successfully logged in
            } else {
              loginFailed();
            }
          }
        });
  }

}

6 commentaires:

sylvain.saurel a dit...

Bonjour,

Je tente actuellement de mettre en place à peu près la même chose dans une application GWT 2.0. Cependant, j'ai quelques soucis pour sécuriser les URLS de mes vues GWT.

Je poste à tout hasard mon problème au cas où vous auriez le temps de me donner un conseil.

Ainsi, mon application est localisée dans le fichier HTML Application.html.

Au sein de cette page, il est possible d'accéder au formulaire de login via l'URL suivante :

http:// 127.0.0.1:8888/fr.myapp.Application/Application.html?gwt.codesvr=127.0.0.1:9997#login

Une fois le login validé, l'authentification se fait bien avec Spring Security 3.0 côté serveur. Et je redirige vers ma vue où se trouve la page sécurisée.

L'URL est la suivante :

http:// 127.0.0.1:8888/fr.myapp.Application/Application.html?gwt.codesvr=127.0.0.1:9997#pagesecured

Sur cette page, en faisan un appel au service de sécurité j'arrive à vérifier que je suis bien connecté puisque je peux récupérer l'utilisateur loggué via le SecurityContect de Spring Security.

Mon problème vient du fait que je n'arrive pas à sécuriser les vues de mon application car toutes se trouvent sur la même page (Application.html).

Ainsi, je ne vois comment configurer mes interceptions dans la balise interceptor-url de Spring Security (et également vers quelle page rediriger l'utilisateur lorsqu'il tente d'accéder directement à une page qui lui est interdite).

Auriez-vous une idée de la manière dont je peux arriver à configurer la sécurisation des mes pages (ou vues pour être plus précis) ?

Merci d'avance de votre aide.

Sylvain

Cédric Thiébault a dit...

Je n'ai pas eu ce problème car je gère moi-même l'accès aux widgets de mon application GWT. 
Il faudrait voir comment Spring Security gère les url comme "#pagesecured" :
< http auto-config='true' >
< intercept-url pattern="/Application.html?*#pagesecured" access="ROLE_USER" />
< /http >

Eric a dit...

Bonjour,

Je suis actuellement entrain d'essayer d'utilise votre méthode d'authentification. J'avoue que j'ai un peu de mal, je suis tt nouveau en Spring.

J'aurai voulu savoir s'il était possible d'avoir les sources complète de votre projet. Ce qui me permettrait de comprendre un peu mieux.

Merci par avance.

Eric

Cédric Thiébault a dit...

Désolé ce code fait partie d'un trop gros projet pour pouvoir le publier...

Eric a dit...

Ok, je comprends.
Merci d'avoir répondu, je vais essayer de mieux isoler mon pb pour pouvoir poser des questions précises.

Waldo a dit...

Salut,

Merci pour ce tuto, je pense que je vais largement m'en inspirer.

Par contre j'ai deux questions :
- Dans la configuration de spring-security tu donnes cette directive :
security:logout logout-url="/logout" logout-success-url="/gwt.html"
Mais dans le code je ne voie pas avec quoi cela est mappé/utilisé.

Ma deuxième question concerne les annotations. J'aimerais savoir si tu les utilises pour sécuriser certaines méthodes de classes Business? je pense par exemple à ce type annotation @Secured({"ROLE_UTILISATEUR"})

En tout cas merci pour ce retour d'expérience!