Avertissement : Cet article a été traduit de l’anglais par un LLM. La précision n’est pas garantie. Vous pouvez lire l’article original en anglais.
Spring est l’un des frameworks Java les plus (sinon le plus) utilisés. Il possède de nombreuses fonctionnalités et peut vous permettre de démarrer rapidement un projet, mais si vous construisez une API, la partie gestion de l’authentification est mal documentée. Dans cet article, nous verrons comment tirer parti de Spring Security (v5.7.3) pour sécuriser votre API Spring Boot et affiner les permissions avec des rôles. Dans ce qui suit, je supposerai que vous avez quelques connaissances de Spring et Hibernate.
Options d’authentification#
Malheureusement, Spring Security ne fournit rien pour générer des Tokens et les utiliser comme mécanisme d’authentification (ou du moins ce n’est pas visible dans la documentation), nous utiliserons donc Basic Auth.
Auparavant, l’authentification pouvait être gérée avec la classe WebSecurityConfigurerAdapter (presque tous les tutoriels sur Internet l’utilisent), mais elle a été dépréciée cette année, nous ne l’utiliserons donc pas.
Passons au code#
Par défaut, Spring Security fournit des classes d’utilisateurs et de rôles, mais elles semblent nécessiter l’application de certains patchs de base de données et ne sont pas flexibles. Au lieu de les utiliser, nous créerons nos propres classes et implémenterons les bonnes interfaces, afin que ce soit plus facile maintenant, et à l’avenir si nous voulons étendre les fonctionnalités de notre classe User. Notez que j’utiliserai Lombok et JPA dans le code suivant.
Utilisateurs#
Pour rendre notre classe User compatible avec Spring Security, nous devrons implémenter org.springframework.security.core.userdetails.UserDetails. Au minimum, vous aurez besoin d’attributs username et password, qui sont utilisés par le framework. Vous devrez également implémenter diverses méthodes telles que getAuthorities (nous y reviendrons plus en détail), isLocked, … Ces méthodes seront utilisées par Spring Security pour accorder l’accès, donc faites attention à ce qu’elles renvoient.
@Entity
@Audited
@NoArgsConstructor
@Table(name = "users")
public class User implements UserDetails {
@Id @GeneratedValue @Getter
private Long id;
@Getter @Setter
private String email;
@Getter @Setter
@Column(unique = true)
private String username;
@Getter @Setter
private String password;
@Getter @Setter
private boolean enabled;
@Getter @Setter
private boolean locked;
@Getter @Setter
@Column(name = "expiration_date")
private Date expirationDate;
@Setter @Getter
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
private Set<UserRole> roles = new HashSet<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles;
}
@Override
public boolean isAccountNonExpired() {
return expirationDate == null || expirationDate.after(new Date());
}
@Override
public boolean isAccountNonLocked() {
return !isLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
}Vous noterez l’annotation @Audited. Elle est fournie par org.hibernate:hibernate-envers. Ce qu’elle fait, c’est s’assurer de conserver les anciennes versions de l’objet à chaque fois qu’une modification est poussée vers la base de données, ce qui est pratique pour la gestion des utilisateurs.
Implémentation des rôles#
Maintenant que nous avons notre classe User de base, configurons les rôles. L’implémentation est assez triviale, car il suffit d’implémenter GrantedAuthority et de surcharger la méthode getAuthority.
Ici, j’ai défini deux rôles en utilisant un enum, mais rien ne vous empêche de faire les choses différemment et d’utiliser d’autres rôles. Assurez-vous simplement de les nommer ROLE_[...].
@Embeddable
public class UserRole implements GrantedAuthority {
@Setter
@Enumerated(EnumType.STRING)
@Column(name = "role_name")
private RoleName roleName;
@Override
public String getAuthority() {
return roleName.name();
}
public enum RoleName {
ROLE_ADMIN,
ROLE_USER
}
}Accès à la base de données#
L’étape suivante consiste à s’assurer que Spring Security peut récupérer les détails des utilisateurs depuis la base de données. Pour cela, nous devrons implémenter deux classes. Premièrement, un JpaRepository pour permettre l’interrogation de la base de données.
@Configuration
public interface UserRepository extends JpaRepository<User, Long> {
User findUserByUsername(String username);
}Puis, une classe implémentant UserDetailsService, qui est ce que Spring Security utilise pour trouver les utilisateurs.
@Component
public class SecurityUserDetailService implements UserDetailsService {
private final UserRepository _userRepository;
public SecurityUserDetailService(UserRepository userRepository) {
this._userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return _userRepository.findUserByUsername(username);
}
}Configuration de la sécurité#
Enfin, nous devons créer une classe de configuration pour définir comment Spring Security se comportera. Notez que nous désactivons la protection CSRF et configurons la gestion de session comme stateless. C’est parce que nous écrivons ceci pour être utilisé avec une API REST. Si vous construisez une interface web, vous ne devriez pas faire cela.
Notez également la méthode passwordEncoder qui est utilisée pour définir comment les mots de passe sont hachés. Ici, ils utilisent 10 tours de bcrypt.
@Configuration
@EnableMethodSecurity
public class SecurityConfiguration
{
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic();
return http.build();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
@Bean
public AuthenticationManager authManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, SecurityUserDetailService securityUserDetailService) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(securityUserDetailService)
.passwordEncoder(bCryptPasswordEncoder)
.and()
.build();
}
}Autoriser l’appel de méthodes par des rôles spécifiques#
Si vous avez appliqué la configuration jusqu’ici, votre application exige désormais que les utilisateurs soient authentifiés pour accéder à tous vos endpoints. Voyons maintenant comment affiner qui peut accéder à quoi.
Premièrement, vous pouvez gérer les choses depuis la classe de configuration. Par exemple, vous pouvez modifier la méthode filterChain comme suit pour permettre à quiconque d’interroger /api/public sans être authentifié, et exiger le rôle ADMIN pour accéder à /api/admin
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers("/api/public").permitAll()
.antMatchers("/api/admin").hasRole("ADMIN")
.anyRequest().authenticated().and().httpBasic();Une autre option est d’annoter directement vos contrôleurs. Ici, nous exigeons que les utilisateurs aient le rôle ADMIN pour accéder à /api/admin
@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')")
public class AdminController {
...Enfin, vous pouvez directement annoter les méthodes dans vos contrôleurs. Vous remarquerez que la méthode utilise @AuthenticationPrincipal. Cela permet d’obtenir l’utilisateur authentifié faisant la requête et est totalement optionnel.
@GetMapping("/user/{userId}")
@PreAuthorize("hasRole('ADMIN')")
ResponseEntity<?> getUser(@AuthenticationPrincipal User user, @PathVariable Long userId) {
...Derniers mots#
Dans cet article, nous avons appris comment implémenter des mécanismes d’authentification de base pour fournir l’authentification et l’autorisation grâce à Spring-Security lors de l’écriture d’une API REST avec Spring Boot. Cela devrait suffire pour démarrer, mais notez que les fonctionnalités de Spring Security ne se limitent pas à ce qui est listé dans cet article. Par exemple, vous pouvez autoriser plusieurs groupes, utiliser d’autres annotations telles que @PostAuthorize, …
Sources et crédits#
Sources#
Crédits#
- Photo de couverture par Clément Hélardot sur Unsplash