Spring Security - Rôles et privilèges

Spring Security - Rôles et privilèges

1. Vue d'ensemble

Cet article poursuit la série Inscription avec Spring Security en expliquant comment implémenter correctementRoles and Privileges.

Lectures complémentaires:

Introduction aux expressions de sécurité de printemps

Guide simple et pratique sur les expressions de sécurité de printemps.

Read more

Introduction à la sécurité de la méthode Spring

Guide sur la sécurité au niveau des méthodes à l'aide de la structure Spring Security.

Read more

Spring Security - Rediriger vers l'URL précédente après la connexion

Petit exemple de redirection après la connexion à Spring Security

Read more

2. UserRole etPrivilege

Commençons par nos entités. Nous avons trois entités principales:

  • lesUser

  • leRole - cela représente les rôles de haut niveau de l'utilisateur dans le système; chaque rôle aura un ensemble de privilèges de bas niveau

  • lePrivilege - représente un privilège / autorité granulaire de bas niveau dans le système

Voicithe user:

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String firstName;
    private String lastName;
    private String email;
    private String password;
    private boolean enabled;
    private boolean tokenExpired;

    @ManyToMany
    @JoinTable(
        name = "users_roles",
        joinColumns = @JoinColumn(
          name = "user_id", referencedColumnName = "id"),
        inverseJoinColumns = @JoinColumn(
          name = "role_id", referencedColumnName = "id"))
    private Collection roles;
}

Comme vous pouvez le constater, l’utilisateur contient les rôles, mais également quelques détails supplémentaires nécessaires au bon fonctionnement du mécanisme d’enregistrement.

Suivant - voicithe role:

@Entity
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;
    @ManyToMany(mappedBy = "roles")
    private Collection users;

    @ManyToMany
    @JoinTable(
        name = "roles_privileges",
        joinColumns = @JoinColumn(
          name = "role_id", referencedColumnName = "id"),
        inverseJoinColumns = @JoinColumn(
          name = "privilege_id", referencedColumnName = "id"))
    private Collection privileges;
}

Et enfinthe privilege:

@Entity
public class Privilege {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "privileges")
    private Collection roles;
}

Comme vous pouvez le voir, nous considérons à la fois les relations User <→ Role et Role <→ Privilegemany-to-many bidirectional.

3. Configurer les privilèges et les rôles

Ensuite, concentrons-nous sur la configuration précoce des privilèges et des rôles dans le système.

Nous allons lier cela au démarrage de l'application et nous utiliserons unApplicationListener surContextRefreshedEvent pour charger nos données initiales au démarrage du serveur:

@Component
public class InitialDataLoader implements
  ApplicationListener {

    boolean alreadySetup = false;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    @Autowired
    private PrivilegeRepository privilegeRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    @Transactional
    public void onApplicationEvent(ContextRefreshedEvent event) {

        if (alreadySetup)
            return;
        Privilege readPrivilege
          = createPrivilegeIfNotFound("READ_PRIVILEGE");
        Privilege writePrivilege
          = createPrivilegeIfNotFound("WRITE_PRIVILEGE");

        List adminPrivileges = Arrays.asList(
          readPrivilege, writePrivilege);
        createRoleIfNotFound("ROLE_ADMIN", adminPrivileges);
        createRoleIfNotFound("ROLE_USER", Arrays.asList(readPrivilege));

        Role adminRole = roleRepository.findByName("ROLE_ADMIN");
        User user = new User();
        user.setFirstName("Test");
        user.setLastName("Test");
        user.setPassword(passwordEncoder.encode("test"));
        user.setEmail("[email protected]");
        user.setRoles(Arrays.asList(adminRole));
        user.setEnabled(true);
        userRepository.save(user);

        alreadySetup = true;
    }

    @Transactional
    private Privilege createPrivilegeIfNotFound(String name) {

        Privilege privilege = privilegeRepository.findByName(name);
        if (privilege == null) {
            privilege = new Privilege(name);
            privilegeRepository.save(privilege);
        }
        return privilege;
    }

    @Transactional
    private Role createRoleIfNotFound(
      String name, Collection privileges) {

        Role role = roleRepository.findByName(name);
        if (role == null) {
            role = new Role(name);
            role.setPrivileges(privileges);
            roleRepository.save(role);
        }
        return role;
    }
}

Alors, que se passe-t-il pendant ce code de configuration simple? Rien de compliqué:

  • nous créons les privilèges

  • nous créons les rôles et leur attribuons les privilèges

  • nous créons un utilisateur et lui attribuons un rôle

Notez comment nous utilisons un indicateuralreadySetup pourdetermine if the setup needs to run or not. C'est simplement parce que, selon le nombre de contextes que vous avez configurés dans votre application - lesContextRefreshedEvent peuvent être déclenchés plusieurs fois. Et nous voulons seulement que la configuration soit exécutée une fois.

Deux notes rapides ici -first, about terminology. Nous utilisons ici les termesPrivilege – Role, mais au printemps, ils sont légèrement différents. Au printemps, notre privilège est désigné sous le nom de rôle, mais également en tant qu'autorité (accordée) - ce qui est légèrement déroutant. Pas de problème pour la mise en œuvre bien sûr, mais à noter.

Second – these Spring Roles (our Privileges) need a prefix; par défaut, ce préfixe est «ROLE», mais il peut être modifié. Nous n'utilisons pas ce préfixe ici, juste pour garder les choses simples, mais gardez à l'esprit que si vous ne le modifiez pas explicitement, il sera nécessaire.

4. PersonnaliséUserDetailsService

Voyons maintenant le processus d'authentification.

Nous allons voir comment récupérer l'utilisateur dans nosUserDetailsService personnalisés et comment mapper le bon ensemble d'autorités à partir des rôles et privilèges attribués par l'utilisateur:

@Service("userDetailsService")
@Transactional
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private IUserService service;

    @Autowired
    private MessageSource messages;

    @Autowired
    private RoleRepository roleRepository;

    @Override
    public UserDetails loadUserByUsername(String email)
      throws UsernameNotFoundException {

        User user = userRepository.findByEmail(email);
        if (user == null) {
            return new org.springframework.security.core.userdetails.User(
              " ", " ", true, true, true, true,
              getAuthorities(Arrays.asList(
                roleRepository.findByName("ROLE_USER"))));
        }

        return new org.springframework.security.core.userdetails.User(
          user.getEmail(), user.getPassword(), user.isEnabled(), true, true,
          true, getAuthorities(user.getRoles()));
    }

    private Collection getAuthorities(
      Collection roles) {

        return getGrantedAuthorities(getPrivileges(roles));
    }

    private List getPrivileges(Collection roles) {

        List privileges = new ArrayList<>();
        List collection = new ArrayList<>();
        for (Role role : roles) {
            collection.addAll(role.getPrivileges());
        }
        for (Privilege item : collection) {
            privileges.add(item.getName());
        }
        return privileges;
    }

    private List getGrantedAuthorities(List privileges) {
        List authorities = new ArrayList<>();
        for (String privilege : privileges) {
            authorities.add(new SimpleGrantedAuthority(privilege));
        }
        return authorities;
    }
}

La chose intéressante à suivre ici est la manière dont les privilèges (et les rôles) sont mappés aux entités GrantedAuthority.

Ce mappage rend toute la configuration de sécuritéhighly flexible and powerful - vous pouvez mélanger et associer des rôles et des privilèges aussi précis que nécessaire, et à la fin, ils seront correctement mappés aux autorités et renvoyés au framework.

5. EnregistrementUser

Enfin, examinons l'enregistrement d'un nouvel utilisateur.

Nous avons vu comment la configuration consiste à créer l'utilisateur et à lui attribuer des rôles (et des privilèges). Voyons maintenant comment cela doit être fait lors de l'enregistrement d'un nouvel utilisateur:

@Override
public User registerNewUserAccount(UserDto accountDto) throws EmailExistsException {

    if (emailExist(accountDto.getEmail())) {
        throw new EmailExistsException
          ("There is an account with that email adress: " + accountDto.getEmail());
    }
    User user = new User();

    user.setFirstName(accountDto.getFirstName());
    user.setLastName(accountDto.getLastName());
    user.setPassword(passwordEncoder.encode(accountDto.getPassword()));
    user.setEmail(accountDto.getEmail());

    user.setRoles(Arrays.asList(roleRepository.findByName("ROLE_USER")));
    return repository.save(user);
}

Dans cette implémentation simple, nous partons du principe qu’un utilisateur standard est en cours d’enregistrement, le rôleROLE_USER lui est donc attribué.

Bien sûr, une logique plus complexe peut facilement être mise en œuvre de la même manière - soit en utilisant plusieurs méthodes d'enregistrement codées en dur, soit en permettant au client d'envoyer le type d'utilisateur qui est enregistré.

6. Conclusion

Dans ce didacticiel, nous avons montré comment implémenter des rôles et des privilèges avec JPA pour un système prenant en charge Spring Security.

Lesfull implementation de ce didacticiel d'enregistrement avec Spring Security peuvent être trouvés dansthe GitHub project - il s'agit d'un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.