Spring Security avec JWT pour l'API REST
Publié: 2022-03-11Spring est considéré comme un framework de confiance dans l'écosystème Java et est largement utilisé. Il n'est plus valide de faire référence à Spring en tant que cadre, car il s'agit plutôt d'un terme générique qui couvre divers cadres. L'un de ces frameworks est Spring Security, qui est un framework d'authentification et d'autorisation puissant et personnalisable. Il est considéré comme la norme de facto pour la sécurisation des applications basées sur Spring.
Malgré sa popularité, je dois admettre que lorsqu'il s'agit d'applications d'une seule page, ce n'est pas simple et direct à configurer. Je soupçonne que la raison en est qu'il a commencé davantage comme un framework orienté application MVC, où le rendu des pages Web se produit côté serveur et la communication est basée sur la session.
Si le back-end est basé sur Java et Spring, il est logique d'utiliser Spring Security pour l'authentification/autorisation et de le configurer pour une communication sans état. Bien qu'il existe de nombreux articles expliquant comment cela se fait, pour moi, c'était toujours frustrant de le configurer pour la première fois, et j'ai dû lire et résumer des informations provenant de plusieurs sources. C'est pourquoi j'ai décidé d'écrire cet article, où j'essaierai de résumer et de couvrir tous les détails subtils et les faiblesses que vous pourriez rencontrer pendant le processus de configuration.
Définition de la terminologie
Avant de plonger dans les détails techniques, je souhaite définir explicitement la terminologie utilisée dans le contexte de Spring Security juste pour être sûr que nous parlons tous le même langage.
Voici les termes que nous devons aborder :
- L' authentification fait référence au processus de vérification de l'identité d'un utilisateur, sur la base des informations d'identification fournies. Un exemple courant consiste à entrer un nom d'utilisateur et un mot de passe lorsque vous vous connectez à un site Web. Vous pouvez le considérer comme une réponse à la question Qui êtes-vous ? .
- L'autorisation fait référence au processus consistant à déterminer si un utilisateur dispose de l'autorisation appropriée pour effectuer une action particulière ou lire des données particulières, en supposant que l'utilisateur est authentifié avec succès. Vous pouvez le considérer comme une réponse à la question Un utilisateur peut-il faire/lire ceci ? .
- Le principe fait référence à l'utilisateur actuellement authentifié.
- L'autorité accordée fait référence à l'autorisation de l'utilisateur authentifié.
- Le rôle fait référence à un groupe d'autorisations de l'utilisateur authentifié.
Création d'une application Spring de base
Avant de passer à la configuration du framework Spring Security, créons une application Web Spring de base. Pour cela, nous pouvons utiliser un Spring Initializr et générer un modèle de projet. Pour une application web simple, seule une dépendance du framework web Spring suffit :
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
Une fois que nous avons créé le projet, nous pouvons y ajouter un simple contrôleur REST comme suit :
@RestController @RequestMapping("hello") public class HelloRestController { @GetMapping("user") public String helloUser() { return "Hello User"; } @GetMapping("admin") public String helloAdmin() { return "Hello Admin"; } }
Après cela, si nous construisons et exécutons le projet, nous pouvons accéder aux URL suivantes dans le navigateur Web :
-
http://localhost:8080/hello/user
renverra la chaîneHello User
. -
http://localhost:8080/hello/admin
renverra la chaîneHello Admin
.
Maintenant, nous pouvons ajouter le framework Spring Security à notre projet, et nous pouvons le faire en ajoutant la dépendance suivante à notre fichier pom.xml
:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>
L'ajout d'autres dépendances du framework Spring n'a normalement pas d'effet immédiat sur une application jusqu'à ce que nous fournissions la configuration correspondante, mais Spring Security est différent en ce sens qu'il a un effet immédiat, ce qui déroute généralement les nouveaux utilisateurs. Après l'avoir ajouté, si nous reconstruisons et exécutons le projet, puis essayons d'accéder à l'une des URL susmentionnées au lieu d'afficher le résultat, nous serons redirigés vers http://localhost:8080/login
. Il s'agit du comportement par défaut car le framework Spring Security nécessite une authentification prête à l'emploi pour toutes les URL.
Pour réussir l'authentification, nous pouvons utiliser le nom user
par défaut et trouver un mot de passe généré automatiquement dans notre console :
Using generated security password: 1fc15145-dfee-4bec-a009-e32ca21c77ce
N'oubliez pas que le mot de passe change chaque fois que nous réexécutons l'application. Si nous voulons changer ce comportement et rendre le mot de passe statique, nous pouvons ajouter la configuration suivante à notre fichier application.properties
:
spring.security.user.password=Test12345_
Maintenant, si nous entrons les informations d'identification dans le formulaire de connexion, nous serons redirigés vers notre URL et nous verrons le résultat correct. Veuillez noter que le processus d'authentification prêt à l'emploi est basé sur la session, et si nous voulons nous déconnecter, nous pouvons accéder à l'URL suivante : http://localhost:8080/logout
Ce comportement prêt à l'emploi peut être utile pour les applications Web MVC classiques où nous avons une authentification basée sur la session, mais dans le cas d'applications d'une seule page, ce n'est généralement pas utile car dans la plupart des cas d'utilisation, nous avons côté client rendu et authentification sans état basée sur JWT. Dans ce cas, nous devrons fortement personnaliser le framework Spring Security, ce que nous ferons dans la suite de l'article.
À titre d'exemple, nous allons implémenter une application Web de librairie classique et créer un back-end qui fournira des API CRUD pour créer des auteurs et des livres ainsi que des API pour la gestion et l'authentification des utilisateurs.
Présentation de l'architecture de sécurité Spring
Avant de commencer à personnaliser la configuration, discutons d'abord du fonctionnement de l'authentification Spring Security dans les coulisses.
Le diagramme suivant présente le flux et montre comment les demandes d'authentification sont traitées :
Architecture de sécurité Spring

Maintenant, décomposons ce diagramme en composants et discutons de chacun d'eux séparément.
Chaîne de filtres de sécurité à ressort
Lorsque vous ajoutez le framework Spring Security à votre application, il enregistre automatiquement une chaîne de filtres qui intercepte toutes les requêtes entrantes. Cette chaîne se compose de différents filtres, et chacun d'eux gère un cas d'utilisation particulier.
Par exemple:
- Vérifiez si l'URL demandée est accessible au public, en fonction de la configuration.
- En cas d'authentification basée sur la session, vérifiez si l'utilisateur est déjà authentifié dans la session en cours.
- Vérifiez si l'utilisateur est autorisé à effectuer l'action demandée, et ainsi de suite.
Un détail important que je veux mentionner est que les filtres Spring Security sont enregistrés avec l'ordre le plus bas et sont les premiers filtres invoqués. Pour certains cas d'utilisation, si vous souhaitez mettre votre filtre personnalisé devant eux, vous devrez ajouter un rembourrage à leur commande. Cela peut être fait avec la configuration suivante :
spring.security.filter.order=10
Une fois que nous aurons ajouté cette configuration à notre fichier application.properties
, nous aurons de la place pour 10 filtres personnalisés devant les filtres Spring Security.
Gestionnaire d'authentification
Vous pouvez considérer AuthenticationManager
comme un coordinateur où vous pouvez enregistrer plusieurs fournisseurs et, en fonction du type de demande, il enverra une demande d'authentification au bon fournisseur.
Fournisseur d'authentification
AuthenticationProvider
traite des types d'authentification spécifiques. Son interface n'expose que deux fonctions :
-
authenticate
effectue l'authentification avec la requête. -
supports
vérifie si ce fournisseur prend en charge le type d'authentification indiqué.
Une implémentation importante de l'interface que nous utilisons dans notre exemple de projet est DaoAuthenticationProvider
, qui récupère les détails de l'utilisateur à partir d'un UserDetailsService
.
UserDetailsService
UserDetailsService
est décrit comme une interface principale qui charge des données spécifiques à l'utilisateur dans la documentation Spring.
Dans la plupart des cas d'utilisation, les fournisseurs d'authentification extraient les informations d'identité de l'utilisateur en fonction des informations d'identification d'une base de données, puis effectuent la validation. Étant donné que ce cas d'utilisation est si courant, les développeurs de Spring ont décidé de l'extraire en tant qu'interface distincte, qui expose la fonction unique :
-
loadUserByUsername
accepte le nom d'utilisateur comme paramètre et renvoie l'objet d'identité de l'utilisateur.
Authentification à l'aide de JWT avec Spring Security
Après avoir discuté des éléments internes du framework Spring Security, configurons-le pour une authentification sans état avec un jeton JWT.
Pour personnaliser Spring Security, nous avons besoin d'une classe de configuration annotée avec l'annotation @EnableWebSecurity
dans notre chemin de classe. De plus, pour simplifier le processus de personnalisation, le framework expose une classe WebSecurityConfigurerAdapter
. Nous allons étendre cet adaptateur et remplacer ses deux fonctions afin de :
- Configurer le gestionnaire d'authentification avec le bon fournisseur
- Configurer la sécurité Web (URL publiques, URL privées, autorisation, etc.)
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO configure authentication manager } @Override protected void configure(HttpSecurity http) throws Exception { // TODO configure web security } }
Dans notre exemple d'application, nous stockons les identités des utilisateurs dans une base de données MongoDB, dans la collection users
. Ces identités sont mappées par l'entité User
et leurs opérations CRUD sont définies par le UserRepo
Spring Data.
Maintenant, lorsque nous acceptons la demande d'authentification, nous devons récupérer l'identité correcte de la base de données à l'aide des informations d'identification fournies, puis la vérifier. Pour cela, nous avons besoin de l'implémentation de l'interface UserDetailsService
, qui est définie comme suit :
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
Ici, nous pouvons voir qu'il est nécessaire de retourner l'objet qui implémente l'interface UserDetails
, et notre entité User
implémente (pour les détails d'implémentation, veuillez consulter le référentiel du projet exemple). Compte tenu du fait qu'il n'expose que le prototype à fonction unique, nous pouvons le traiter comme une interface fonctionnelle et fournir une implémentation en tant qu'expression lambda.
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format("User: %s, not found", username) ) )); } // Details omitted for brevity }
Ici, l'appel de fonction auth.userDetailsService
lancera l'instance DaoAuthenticationProvider
à l'aide de notre implémentation de l'interface UserDetailsService
et l'enregistrera dans le gestionnaire d'authentification.
En plus du fournisseur d'authentification, nous devons configurer un gestionnaire d'authentification avec le schéma de codage de mot de passe correct qui sera utilisé pour la vérification des informations d'identification. Pour cela, nous devons exposer l'implémentation préférée de l'interface PasswordEncoder
en tant que bean.
Dans notre exemple de projet, nous utiliserons l'algorithme de hachage de mot de passe bcrypt.
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format("User: %s, not found", username) ) )); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // Details omitted for brevity }
Après avoir configuré le gestionnaire d'authentification, nous devons maintenant configurer la sécurité Web. Nous mettons en œuvre une API REST et avons besoin d'une authentification sans état avec un jeton JWT ; par conséquent, nous devons définir les options suivantes :
- Activez CORS et désactivez CSRF.
- Définissez la gestion de session sur sans état.
- Définissez le gestionnaire d'exceptions des demandes non autorisées.
- Définissez les autorisations sur les points de terminaison.
- Ajoutez un filtre de jeton JWT.
Cette configuration est implémentée comme suit :
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; private final JwtTokenFilter jwtTokenFilter; public SecurityConfig(UserRepo userRepo, JwtTokenFilter jwtTokenFilter) { this.userRepo = userRepo; this.jwtTokenFilter = jwtTokenFilter; } // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers("/api/public/**").permitAll() .antMatchers(HttpMethod.GET, "/api/author/**").permitAll() .antMatchers(HttpMethod.POST, "/api/author/search").permitAll() .antMatchers(HttpMethod.GET, "/api/book/**").permitAll() .antMatchers(HttpMethod.POST, "/api/book/search").permitAll() // Our private endpoints .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Used by spring security if CORS is enabled. @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }
Veuillez noter que nous avons ajouté le JwtTokenFilter
avant le UsernamePasswordAuthenticationFilter
interne de Spring Security. Nous faisons cela parce que nous avons besoin d'accéder à l'identité de l'utilisateur à ce stade pour effectuer l'authentification/autorisation, et son extraction se produit à l'intérieur du filtre de jeton JWT en fonction du jeton JWT fourni. Ceci est mis en œuvre comme suit :
@Component public class JwtTokenFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; private final UserRepo userRepo; public JwtTokenFilter(JwtTokenUtil jwtTokenUtil, UserRepo userRepo) { this.jwtTokenUtil = jwtTokenUtil; this.userRepo = userRepo; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // Get authorization header and validate final String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (isEmpty(header) || !header.startsWith("Bearer ")) { chain.doFilter(request, response); return; } // Get jwt token and validate final String token = header.split(" ")[1].trim(); if (!jwtTokenUtil.validate(token)) { chain.doFilter(request, response); return; } // Get user identity and set it on the spring security context UserDetails userDetails = userRepo .findByUsername(jwtTokenUtil.getUsername(token)) .orElse(null); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails == null ? List.of() : userDetails.getAuthorities() ); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } }
Avant d'implémenter notre fonction d'API de connexion, nous devons nous occuper d'une étape supplémentaire - nous avons besoin d'accéder au gestionnaire d'authentification. Par défaut, il n'est pas accessible au public et nous devons l'exposer explicitement en tant que bean dans notre classe de configuration.

Cela peut être fait comme suit:
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
Et maintenant, nous sommes prêts à implémenter notre fonction d'API de connexion :
@Api(tags = "Authentication") @RestController @RequestMapping(path = "api/public") public class AuthApi { private final AuthenticationManager authenticationManager; private final JwtTokenUtil jwtTokenUtil; private final UserViewMapper userViewMapper; public AuthApi(AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil, UserViewMapper userViewMapper) { this.authenticationManager = authenticationManager; this.jwtTokenUtil = jwtTokenUtil; this.userViewMapper = userViewMapper; } @PostMapping("login") public ResponseEntity<UserView> login(@RequestBody @Valid AuthRequest request) { try { Authentication authenticate = authenticationManager .authenticate( new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword() ) ); User user = (User) authenticate.getPrincipal(); return ResponseEntity.ok() .header( HttpHeaders.AUTHORIZATION, jwtTokenUtil.generateAccessToken(user) ) .body(userViewMapper.toUserView(user)); } catch (BadCredentialsException ex) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } } }
Ici, nous vérifions les informations d'identification fournies à l'aide du gestionnaire d'authentification et, en cas de succès, nous générons le jeton JWT et le renvoyons en tant qu'en-tête de réponse avec les informations d'identité de l'utilisateur dans le corps de la réponse.
Autorisation avec Spring Security
Dans la section précédente, nous avons mis en place un processus d'authentification et configuré des URL publiques/privées. Cela peut suffire pour des applications simples, mais pour la plupart des cas d'utilisation réels, nous avons toujours besoin de politiques d'accès basées sur les rôles pour nos utilisateurs. Dans ce chapitre, nous aborderons ce problème et mettrons en place un schéma d'autorisation basé sur les rôles à l'aide du framework Spring Security.
Dans notre exemple d'application, nous avons défini les trois rôles suivants :
-
USER_ADMIN
nous permet de gérer les utilisateurs de l'application. -
AUTHOR_ADMIN
nous permet de gérer les auteurs. -
BOOK_ADMIN
nous permet de gérer les livres.
Maintenant, nous devons les appliquer aux URL correspondantes :
-
api/public
est accessible au public. -
api/admin/user
peut accéder aux utilisateurs avec le rôleUSER_ADMIN
. -
api/author
peut accéder aux utilisateurs avec le rôleAUTHOR_ADMIN
. -
api/book
peut accéder aux utilisateurs avec le rôleBOOK_ADMIN
.
Le framework Spring Security nous offre deux options pour configurer le schéma d'autorisation :
- Configuration basée sur l'URL
- Configuration basée sur les annotations
Voyons d'abord comment fonctionne la configuration basée sur l'URL. Il peut être appliqué à la configuration de la sécurité Web comme suit :
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers("/api/public/**").permitAll() .antMatchers(HttpMethod.GET, "/api/author/**").permitAll() .antMatchers(HttpMethod.POST, "/api/author/search").permitAll() .antMatchers(HttpMethod.GET, "/api/book/**").permitAll() .antMatchers(HttpMethod.POST, "/api/book/search").permitAll() // Our private endpoints .antMatchers("/api/admin/user/**").hasRole(Role.USER_ADMIN) .antMatchers("/api/author/**").hasRole(Role.AUTHOR_ADMIN) .antMatchers("/api/book/**").hasRole(Role.BOOK_ADMIN) .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Details omitted for brevity }
Comme vous pouvez le voir, cette approche est simple et directe, mais elle a un inconvénient. Le schéma d'autorisation de notre application peut être complexe, et si nous définissons toutes les règles en un seul endroit, il deviendra très volumineux, complexe et difficile à lire. Pour cette raison, je préfère généralement utiliser une configuration basée sur des annotations.
Le framework Spring Security définit les annotations suivantes pour la sécurité Web :
-
@PreAuthorize
prend en charge Spring Expression Language et est utilisé pour fournir un contrôle d'accès basé sur l'expression avant d'exécuter la méthode. -
@PostAuthorize
prend en charge Spring Expression Language et est utilisé pour fournir un contrôle d'accès basé sur l'expression après l'exécution de la méthode (offre la possibilité d'accéder au résultat de la méthode). -
@PreFilter
prend en charge Spring Expression Language et est utilisé pour filtrer la collection ou les tableaux avant d'exécuter la méthode en fonction des règles de sécurité personnalisées que nous définissons. -
@PostFilter
prend en charge Spring Expression Language et est utilisé pour filtrer la collection ou les tableaux renvoyés après l'exécution de la méthode en fonction des règles de sécurité personnalisées que nous définissons (offre la possibilité d'accéder au résultat de la méthode). -
@Secured
ne prend pas en charge Spring Expression Language et est utilisé pour spécifier une liste de rôles sur une méthode. -
@RolesAllowed
ne prend pas en charge Spring Expression Language et est l'annotation équivalente de la JSR 250 de l'annotation@Secured
.
Ces annotations sont désactivées par défaut et peuvent être activées dans notre application comme suit :
@EnableWebSecurity @EnableGlobalMethodSecurity( securedEnabled = true, jsr250Enabled = true, prePostEnabled = true ) public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity }
securedEnabled = true
active l'annotation @Secured
.
jsr250Enabled = true
active l'annotation @RolesAllowed
.
prePostEnabled = true
active @PreAuthorize
, @PostAuthorize
, @PreFilter
, @PostFilter
.
Après les avoir activés, nous pouvons appliquer des politiques d'accès basées sur les rôles sur nos points de terminaison d'API comme ceci :
@Api(tags = "UserAdmin") @RestController @RequestMapping(path = "api/admin/user") @RolesAllowed(Role.USER_ADMIN) public class UserAdminApi { // Details omitted for brevity } @Api(tags = "Author") @RestController @RequestMapping(path = "api/author") public class AuthorApi { // Details omitted for brevity @RolesAllowed(Role.AUTHOR_ADMIN) @PostMapping public void create() { } @RolesAllowed(Role.AUTHOR_ADMIN) @PutMapping("{id}") public void edit() { } @RolesAllowed(Role.AUTHOR_ADMIN) @DeleteMapping("{id}") public void delete() { } @GetMapping("{id}") public void get() { } @GetMapping("{id}/book") public void getBooks() { } @PostMapping("search") public void search() { } } @Api(tags = "Book") @RestController @RequestMapping(path = "api/book") public class BookApi { // Details omitted for brevity @RolesAllowed(Role.BOOK_ADMIN) @PostMapping public BookView create() { } @RolesAllowed(Role.BOOK_ADMIN) @PutMapping("{id}") public void edit() { } @RolesAllowed(Role.BOOK_ADMIN) @DeleteMapping("{id}") public void delete() { } @GetMapping("{id}") public void get() { } @GetMapping("{id}/author") public void getAuthors() { } @PostMapping("search") public void search() { } }
Veuillez noter que les annotations de sécurité peuvent être fournies à la fois au niveau de la classe et au niveau de la méthode.
Les exemples démontrés sont simples et ne représentent pas des scénarios réels, mais Spring Security fournit un riche ensemble d'annotations et vous pouvez gérer un schéma d'autorisation complexe si vous choisissez de les utiliser.
Préfixe par défaut du nom de rôle
Dans cette sous-section séparée, je tiens à souligner un détail plus subtil qui déroute beaucoup de nouveaux utilisateurs.
Le framework Spring Security différencie deux termes :
-
Authority
représente une autorisation individuelle. -
Role
représente un groupe d'autorisations.
Les deux peuvent être représentés avec une seule interface appelée GrantedAuthority
et vérifiés ultérieurement avec Spring Expression Language dans les annotations Spring Security comme suit :
-
Authority
: @PreAuthorize("hasAuthority('EDIT_BOOK')") -
Role
: @PreAuthorize("hasRole('BOOK_ADMIN')")
Pour rendre la différence entre ces deux termes plus explicite, le framework Spring Security ajoute un préfixe ROLE_
au nom du rôle par défaut. Ainsi, au lieu de vérifier un rôle nommé BOOK_ADMIN
, il vérifiera ROLE_BOOK_ADMIN
.
Personnellement, je trouve ce comportement déroutant et préfère le désactiver dans mes applications. Il peut être désactivé dans la configuration de Spring Security comme suit :
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Bean GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix } }
Tester avec Spring Security
Pour tester nos points de terminaison avec des tests unitaires ou d'intégration lors de l'utilisation du framework Spring Security, nous devons ajouter la dépendance spring-security-test
avec le spring-boot-starter-test
. Notre fichier de construction pom.xml
ressemblera à ceci :
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>
Cette dépendance nous donne accès à certaines annotations qui peuvent être utilisées pour ajouter un contexte de sécurité à nos fonctions de test.
Ces annotations sont :
-
@WithMockUser
peut être ajouté à une méthode de test pour émuler l'exécution avec un utilisateur simulé. -
@WithUserDetails
peut être ajouté à une méthode de test pour émuler l'exécution avecUserDetails
renvoyé parUserDetailsService
. -
@WithAnonymousUser
peut être ajouté à une méthode de test pour émuler l'exécution avec un utilisateur anonyme. Ceci est utile lorsqu'un utilisateur souhaite exécuter une majorité de tests en tant qu'utilisateur spécifique et remplacer quelques méthodes pour rester anonyme. -
@WithSecurityContext
détermine leSecurityContext
à utiliser, et les trois annotations décrites ci-dessus sont basées sur celui-ci. Si nous avons un cas d'utilisation spécifique, nous pouvons créer notre propre annotation qui utilise@WithSecurityContext
pour créer n'importe quelSecurityContext
que nous voulons. Sa discussion sort du cadre de notre article et veuillez vous reporter à la documentation de Spring Security pour plus de détails.
Le moyen le plus simple d'exécuter les tests avec un utilisateur spécifique consiste à utiliser l'annotation @WithMockUser
. Nous pouvons créer un utilisateur fictif avec et exécuter le test comme suit :
@Test @WithMockUser(username="[email protected]", roles={"USER_ADMIN"}) public void test() { // Details omitted for brevity }
Cette approche présente cependant quelques inconvénients. Tout d'abord, l'utilisateur fictif n'existe pas, et si vous exécutez le test d'intégration, qui interroge ensuite les informations de l'utilisateur à partir de la base de données, le test échouera. Deuxièmement, l'utilisateur fictif est l'instance de la classe org.springframework.security.core.userdetails.User
, qui est l'implémentation interne du framework Spring de l'interface UserDetails
, et si nous avons notre propre implémentation, cela peut provoquer des conflits plus tard, pendant exécution des tests.
Si les inconvénients précédents bloquent notre application, l'annotation @WithUserDetails
est la solution. Il est utilisé lorsque nous avons des implémentations personnalisées UserDetails
et UserDetailsService
. Il suppose que l'utilisateur existe, nous devons donc soit créer la ligne réelle dans la base de données, soit fournir l'instance UserDetailsService
avant d'exécuter les tests.
Voici comment nous pouvons utiliser cette annotation :
@Test @WithUserDetails("[email protected]") public void test() { // Details omitted for brevity }
Il s'agit d'une annotation préférée dans les tests d'intégration de notre exemple de projet, car nous avons des implémentations personnalisées des interfaces susmentionnées.
L'utilisation @WithAnonymousUser
permet de s'exécuter en tant qu'utilisateur anonyme. Ceci est particulièrement pratique lorsque vous souhaitez exécuter la plupart des tests avec un utilisateur spécifique, mais quelques tests en tant qu'utilisateur anonyme. Par exemple, ce qui suit exécutera les scénarios de test test1 et test2 avec un utilisateur fictif et test3 avec un utilisateur anonyme :
@SpringBootTest @AutoConfigureMockMvc @WithMockUser public class WithUserClassLevelAuthenticationTests { @Test public void test1() { // Details omitted for brevity } @Test public void test2() { // Details omitted for brevity } @Test @WithAnonymousUser public void test3() throws Exception { // Details omitted for brevity } }
Emballer
En fin de compte, je voudrais mentionner que le framework Spring Security ne gagnera probablement aucun concours de beauté et qu'il a certainement une courbe d'apprentissage abrupte. J'ai rencontré de nombreuses situations où il a été remplacé par une solution maison en raison de sa complexité de configuration initiale. Mais une fois que les développeurs ont compris son fonctionnement interne et réussi à mettre en place la configuration initiale, il devient relativement simple à utiliser.
Dans cet article, j'ai essayé de démontrer tous les détails subtils de la configuration, et j'espère que vous trouverez les exemples utiles. Pour des exemples de code complets, veuillez vous référer au référentiel Git de mon exemple de projet Spring Security.