tete du loic

 Loïc YON [KIUX]

  • Enseignant-chercheur
  • Référent Formation Continue
  • Responsable des contrats pros ingénieur
  • Référent entrepreneuriat
  • Responsable de la filière F2 ingénieur
  • Secouriste Sauveteur du Travail
mail
loic.yon@isima.fr
phone
(+33 / 0) 4 73 40 50 42
location_on
Institut d'informatique ISIMA
  • twitter
  • linkedin
  • viadeo

eco Securité eco

Date de première publication : 2019/11/07

L'objectif de ce TP est de tester la sécurité basique sur une application SpringBoot :

Spring Security implémente la sécurité avec des filtres qui s'intercalent entre le client et la servlets. Cela va bien avec le concept d'AOP, ou programmation orientée aspects où la sécurité est justement un aspect (transverse) de l'application

On a les concepts d'authentication (qui es-tu ?), d'authorization (que peux-tu faire ?) et de Principal (l'utilisateur connecté)

Mise en place de l'application

On va continuer sur l'application "TOUDOU". On suppose que vous avez une page qui liste toutes les tâches en les affichant dans des posts-its et une page qui permet d'ajouter une nouvelle page.

Pour mettre en place la sécurité, il suffit d'ajouter les deux dépendances suivantes dans le fichier build.gradle


dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    testImplementation 'org.springframework.security:spring-security-test'
}

/* 
tasks.withType(JavaCompile) {
//    options.compilerArgs << '-Xlint:unchecked'
//    options.deprecation = true
} 
*/

Si vous relancez l'application, elle est maintenant "sécurisée". La console affichera quelque chose comme :


Using generated security password: 6a474bb1-9324-43c0-9e1c-3dd50daa0db8

Mais surtout, il apparait maintenant une page de demande d'authentication. Pour voir ce que cela donne, il suffit de saisir comme identifiant "user" et le mot de passe affiché dans la console ou bien de donner un nom/mot de passe dans le fichier application.properties


spring.security.user.name = loic
spring.security.user.password = loic

Pour se déconnecter, l'URL en /logout est configurée pour que cela marche.

Mais on va aussi pouvoir faire un truc comme cela :


package app;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.beans.factory.annotation.Autowired;


@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

 // code a decommenter pour lire les users dans la base
 // @Autowired
 // MyUserDetailsService userDetailsService;

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
        .antMatchers("/", "/tasks").permitAll()
        .anyRequest().authenticated()
        .and()
      .formLogin()
        //.loginPage("/login")
        //.usernameParameter("email")
        //.defaultSuccessUrl("/tasks")
        .permitAll()
        .and() 
      .logout().logoutSuccessUrl("/")
        .permitAll();
  }

  // Code a commenter pour lire les users dans la base
  @Bean
  @Override
  public UserDetailsService userDetailsService() {
    UserDetails user =
       User.withDefaultPasswordEncoder()
        .username("user")
        .password("password")
        .roles("USER")
        .build();

    return new InMemoryUserDetailsManager(user);
  } 
}

Vous pouvez maintenant vérifier que les URL / et /tasks/ sont toujours en accès libre mais que toutes les autres URLs et en particulier /tasks/new ne sont accessibles qu'après authentification.

Quelle est la classe qui provoque un warning deprecated ?

L'usage de cette classe est toléré pour les démonstrations.

Si vous voulez votre propre formulaire de connexion : décommentez .loginPage("/login") dans la méthode configure, ajoutez ce qu'il faut dans le contrôleur pour le routage et surtout implémentez un formulaire, par exemple :


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" >
    <head>
        <title>connexion </title>
    </head>
    <body>
        <div th:if="${param.error}">
            Mot de passe ou identifiant invalide
        </div>
        <div th:if="${param.logout}">
            Vous avez été déconnecté.
        </div>
        <form th:action="@{/login}" method="post">
            <div><label> Identifiant : <input type="text" name="username"/> </label></div>
            <div><label> Mot de passe : <input type="password" name="password"/> </label></div>
            <div><input type="submit" value="Connexion"/></div>
        </form>
    </body>
</html>

Base d'utilisateurs

Pour disposer d'une base d'utilisateurs, il faut ... une base que l'on peut créer, manipuler et lire comme le TP précédent.

Tout d'abord, l'entité User :


package app.entities;

// il manque des imports

import java.io.Serializable;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;

@Entity
public class User implements Serializable, UserDetails {
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @Column(nullable = false, unique = true)
    private String username;
 
    private String password;

    public User() {

    }

    public User(String username, String password) {
      this.username = username;
      this.password = password;
    }

    // Il manque les getter et les setter, projet lombok ?
      
    public boolean isEnabled() { return true; }

    public boolean isCredentialsNonExpired() { return true; }

    public boolean isAccountNonLocked() { return true; }

    public boolean isAccountNonExpired() { return true; }

    public Collection< ? extends GrantedAuthority> getAuthorities() { return null; }

}

Il faut ensuite définir le repository :


package app.repositories;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
 
    User findByUsername(String username);
}

On termine par la classe :


package hello;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

@Service
public class MyUserDetailsService implements UserDetailsService {
 
    @Autowired
    private UserRepository userRepository;
 
    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        }
        return user;
    }
}

Il ne faut pas oublier de commenter et décommenter les lignes signalées dans la classe WebSecurityConfig

Si vous avez l'erreur suivante :


java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

C'est que vous utilisez une base qui ne spécifie pas les mots de passe correctement, ce que n'accepte plus la version de Spring Security 5. Il faut alors encrypter le mot de passe et spécifier l'encryptage utilisé, comme par exemple :


String mdpe = new BCryptPasswordEncoder().encode("master");
User u = new User("master", "{bcrypt}"+mdpe);

Si vous insérez le code suivant dans hello, vous pouvez avoir le nom de la personne connectée :


<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

Pour la dernière phase, il faut découvrir le champ "message" de User (une chaine de caractères) et l'afficher dans hello.html

Ajouter un tel champ à la classe User. "message" est le nom de la colonne associée au champ.

Pour afficher ce message, le mieux est de créer une classe spécifique qui lira le champ avec le bout de code suivant et le passera en paramètre à la page html.


  User user = (User)
  org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication().getPrincipal();

Gestion plus fine : les rôles

Il est possible d'utiliser des rôles pour accéder à certaines fonctionnalités, cela peut se faire par annotation au niveau des méthodes (vues incluses) et au niveau des classes : @Secured, @PreAuthorize, @PostAuthorize, @PreFilter, @PostFiler. Cette gestion intervient APRÈS les opérations effectuées par l'objet de type HttpSecurity

Il est même possible de créer ses propres annotations pour la protection d'éléments qui reviennent souvent :


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('USER')")
public @interface IsAccepted {
}

@IsAccepted
public String methodeAProteger() {
    //...
}

En passant, vous pouvez voir comment fonctionne l'annotation @PreAuthorize

Pour pouvoir utiliser ces annotations, il va falloir faire un peu de configuration, par exemple :


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
}

Il faut maintenant associer les rôles avec les utilisateurs.

On peut utiliser JPA pour ce faire. Pour la liste des rôles, c'est facile :


@Entity
@Table(name = "roles")
public class Role {
    @Id
    @Column(name = "role_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Getter @Setter
    private Integer id;
    
    @Getter @Setter 
    private String name;

    public Role() {

    }

}

Je vous laisse écrire le RoleRepository...

Pour la table qui lie les rôles et les utilisateurs - une jointure - cela va se traduire par un attribut dans la classe User


@ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.EAGER)
@JoinTable(
    name = "users_roles",
    joinColumns = @JoinColumn(name = "user_id"),
    inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();

Il faut également redéfinir la méthode getAuthorities() de l'interface UserDetails qui se trouve - pour cet exercice - dans la classe User :


@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    Set<Role> roles = getRoles();
    List<SimpleGrantedAuthority> authorities = new ArrayList<>();
     
    for (Role role : roles) {
        authorities.add(new SimpleGrantedAuthority(role.getName()));
    }
     
    return authorities;
}


Il ne reste plus qu'à créer, par JPA, des rôles et des utilisateurs avec des rôles pour tester par exemple un utilisateur avec le rôle ADMIN ou USER

Il existe également les concepts d'Authority (grain plus fin que le rôle) et de groupe d'utilisateurs.

HTTP .... S