Spring Security avec JWT pour REST API

tonux_samb

NDONGO TONUX SAMB

Posted on May 6, 2022

Spring Security avec JWT pour REST API

Spring est considéré comme un framework de confiance dans l'écosystème Java et est largement utilisé. Il n'est plus valable de parler de Spring comme d'un framework, car il s'agit plutôt d'un terme générique qui couvre divers frameworks. 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 monopages, il n'est pas simple et direct à configurer. Je soupçonne que la raison en est qu'il s'agissait à l'origine d'un cadre orienté application MVC, où le rendu des pages Web se fait 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. J'ai décidé d'écrire cet article, dans lequel j'essaierai de résumer et de couvrir tous les détails subtils et les difficultés que vous pouvez rencontrer au cours du processus de configuration.

- Définir 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, afin d'être sûr que nous parlons tous le même langage.

Voici les termes que nous devons aborder :

Authentication désigne le processus de vérification de l'identité d'un utilisateur, sur la base des informations d'identification fournies. Un exemple courant est la saisie d'un nom d'utilisateur et d'un mot de passe lorsque vous vous connectez à un site web. On peut le considérer comme une réponse à la question "Qui êtes-vous ?
Authorization désigne le processus consistant à déterminer si un utilisateur a l'autorisation d'effectuer une action particulière ou de lire des données particulières, en supposant que l'utilisateur a été authentifié avec succès. Il s'agit en fait d'une réponse à la question suivante : "Un utilisateur peut-il faire ou lire ceci ?
Principle fait référence à l'utilisateur actuellement authentifié.
Granted authority fait référence à la permission de l'utilisateur authentifié.
Role fait référence à un groupe de permissions 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 Initializr Spring et générer un projet modèle. Pour une application web simple, seule une dépendance au framework web Spring est suffisante :

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

Une fois que nous avons créé le projet, nous pouvons y ajouter un contrôleur REST simple comme suit :

@RestController @RequestMapping("hello")
public class HelloRestController {

    @GetMapping("user")
    public String helloUser() {
        return "Hello User";
    }

    @GetMapping("admin")
    public String helloAdmin() {
        return "Hello Admin";
    }

}
Enter fullscreen mode Exit fullscreen mode

Après cela, si nous construisons et exécutons le projet, nous pouvons accéder aux URLs suivants dans le navigateur web :

http://localhost:8080/hello/user => Hello User

http://localhost:8080/hello/admin => Hello 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>
Enter fullscreen mode Exit fullscreen mode

L'ajout d'autres dépendances du framework Spring n'a normalement pas d'effet immédiat sur une application tant que nous n'avons pas fourni 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 de visualiser le résultat, nous serons redirigés vers http://localhost:8080/login . Il s'agit d'un comportement par défaut, car Spring Sécurité exige d'emblée une authentification pour toutes les URL.

Pour passer l'authentification, nous pouvons utiliser le nom d'utilisateur par défaut user et trouver un mot de passe auto-généré dans notre console :

Using generated security password: 1fc10045-dfaa-4baq-a119-e32ez32c99ez
Enter fullscreen mode Exit fullscreen mode

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=Passer123@
Enter fullscreen mode Exit fullscreen mode

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 que 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 disposons d'une authentification basée sur la session, mais dans le cas d'applications à page unique, il n'est généralement pas utile car dans la plupart des cas d'utilisation, nous avons un rendu côté client et une 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.

A titre d'exemple, nous allons implémenter une application web classique de boutique et créer un back-end qui fournira des API CRUD pour créer des fournisseurs et des produits ainsi que des API pour la gestion et l'authentification des utilisateurs.

Présentation de l'architecture de Spring Security

Avant de commencer à personnaliser la configuration, voyons d'abord comment l'authentification Spring Security fonctionne en coulisses.

Le diagramme suivant présente le flux et montre comment les demandes d'authentification sont traitées :

- Architecture de Spring Security

Architecture de Spring Security

Maintenant, décomposons ce diagramme en composants et discutons de chacun d'entre eux séparément.

Chaîne de filtres de Spring Security

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 plusieurs filtres, et chacun d'entre eux gère un cas d'utilisation particulier.

Par exemple :

Vérifier si l'URL demandée est accessible publiquement, en fonction de la configuration.
En cas d'authentification basée sur la session, vérifier si l'utilisateur est déjà authentifié dans la session en cours.
Vérifier 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 de 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 voulez placer votre filtre personnalisé devant eux, vous devrez ajouter du remplissage à leur ordre. Ceci peut être fait avec la configuration suivante :

spring.security.filter.order=10
Enter fullscreen mode Exit fullscreen mode

Une fois cette configuration ajoutée à notre fichier application.properties, nous aurons de la place pour 10 filtres personnalisés devant les filtres de Spring Security.

AuthenticationManager

Vous pouvez considérer l'AuthenticationManager comme un coordinateur où vous pouvez enregistrer plusieurs fournisseurs et, en fonction du type de demande, il transmettra une demande d'authentification au bon fournisseur.

AuthenticationProvider

AuthenticationProvider traite des types d'authentification spécifiques. Son interface n'expose que deux fonctions :

  • authenticate effectue l'authentification avec la demande.

  • 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 projet d'exemple est DaoAuthenticationProvider, qui récupère les détails de l'utilisateur à partir d'un UserDetailsService .

UserDetailsService

UserDetailsService est décrit comme une interface de base qui charge les données spécifiques à l'utilisateur dans la documentation de Spring.

Dans la plupart des cas d'utilisation, les fournisseurs d'authentification extraient les informations d'identité de l'utilisateur à partir des informations d'identification d'une base de données, puis effectuent la validation. Ce cas d'utilisation étant très courant, les développeurs de Spring ont décidé de l'extraire sous la forme d'une interface distincte, qui expose la seule fonction :

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 aspects 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 classpath. En outre, pour simplifier le processus de personnalisation, le framework expose une classe WebSecurityConfigurerAdapter . Nous allons étendre cet adaptateur et surcharger ses deux fonctions afin de :

1 - Configurer le gestionnaire d'authentification avec le bon fournisseur.
2 - Configurer la sécurité web (URLs publics, URLs privés, 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
    }

}
Enter fullscreen mode Exit fullscreen mode

Dans notre application d'exemple, nous stockons les identités des utilisateurs dans une base de données H2, dans la collection users. Ces identités sont mappées par l'entité User, et leurs opérations CRUD sont définies par le référentiel 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 en utilisant les 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;

}
Enter fullscreen mode Exit fullscreen mode

Ici, nous pouvons voir qu'il est nécessaire de retourner l'objet qui implémente l'interface UserDetails, et notre entité User l'implémente (pour les détails de l'implémentation, veuillez consulter le référentiel du projet d'exemple). Étant donné qu'elle n'expose que le prototype à fonction unique, nous pouvons la traiter comme une interface fonctionnelle et fournir l'implémentation sous forme d'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

}
Enter fullscreen mode Exit fullscreen mode

Ici, l'appel de fonction auth.userDetailsService va initier l'instance de DaoAuthenticationProvider en utilisant notre implémentation de l'interface UserDetailsService et l'enregistrer dans le gestionnaire d'authentification.

Avec le fournisseur d'authentification, nous devons configurer un gestionnaire d'authentification avec le schéma correct de codage de mot de passe 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 comme un bean.

Dans notre projet d'exemple, 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

}
Enter fullscreen mode Exit fullscreen mode

Après avoir configuré le gestionnaire d'authentification, nous devons maintenant configurer la sécurité web. Nous implémentons 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 :

  • Activer CORS et désactiver CSRF.
  • Définir la gestion de session comme étant sans état.
  • Définir le gestionnaire d'exception des demandes non autorisées.
  • Définir les permissions sur les points de terminaison.
  • Ajouter un filtre pour les jetons 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);
    }

}
Enter fullscreen mode Exit fullscreen mode

Veuillez noter que nous avons ajouté le filtre JwtTokenFilter avant le filtre interne UsernamePasswordAuthenticationFilter 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 fait dans le filtre JWT token basé sur le jeton JWT fourni. Ceci est implémenté 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);
    }

}
Enter fullscreen mode Exit fullscreen mode

Avant d'implémenter notre fonction API de connexion, nous devons nous occuper d'une étape supplémentaire : nous devons avoir accès au gestionnaire d'authentification. Par défaut, il n'est pas accessible au public, et nous devons l'exposer explicitement comme un 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();
    }

}
Enter fullscreen mode Exit fullscreen mode

Et maintenant, nous sommes prêts à mettre en œuvre notre fonction 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();
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

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 comme 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 URLs publiques/privées. Cela peut être suffisant pour les 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 allons aborder cette question et mettre en place un schéma d'autorisation basé sur les rôles en utilisant le framework Spring Security.

Dans notre application d'exemple, nous avons défini les trois rôles suivants :

USER_ADMIN nous permet de gérer les utilisateurs de l'application.
PARTNER_ADMIN nous permet de gérer les fournisseurs.
PRODUCT_ADMIN nous permet de gérer les produits.
Maintenant, nous devons les appliquer aux URLs correspondants :

api/public est accessible au public.
api/admin/user peut accéder aux utilisateurs avec le rôle USER_ADMIN.
api/partner peut accéder aux utilisateurs avec le rôle PARTNER_ADMIN.
api/product peut accéder aux utilisateurs avec le rôle PRODUCT_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
Tout d'abord, voyons comment fonctionne la configuration basée sur les URL. Elle peut être appliquée à 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/partner/**").permitAll()
            .antMatchers(HttpMethod.POST, "/api/partner/search").permitAll()
            .antMatchers(HttpMethod.GET, "/api/product/**").permitAll()
            .antMatchers(HttpMethod.POST, "/api/product/search").permitAll()
            // Our private endpoints
            .antMatchers("/api/admin/user/**").hasRole(Role.USER_ADMIN)
            .antMatchers("/api/partner/**").hasRole(Role.PARTNER_ADMIN)
            .antMatchers("/api/product/**").hasRole(Role.PRODUCT_ADMIN)
            .anyRequest().authenticated();

        // Add JWT token filter
        http.addFilterBefore(
            jwtTokenFilter,
            UsernamePasswordAuthenticationFilter.class
        );
    }

    // Details omitted for brevity

}
Enter fullscreen mode Exit fullscreen mode

Comme vous pouvez le constater, cette approche est simple et directe, mais elle présente 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 gros, 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 supporte le langage d'expression Spring et est utilisé pour fournir un contrôle d'accès basé sur l'expression avant l'exécution de la méthode.
@PostAuthorize supporte le langage d'expression Spring et est utilisé pour fournir un contrôle d'accès basé sur l'expression après l'exécution de la méthode (fournit la possibilité d'accéder au résultat de la méthode).
@PreFilter supporte le langage d'expression Spring et est utilisé pour filtrer la collection ou les tableaux avant l'exécution de la méthode en fonction des règles de sécurité personnalisées que nous définissons.
@PostFilter supporte le langage d'expression Spring et est utilisé pour filtrer la collection ou les tableaux retournés après l'exécution de la méthode en fonction des règles de sécurité personnalisées que nous définissons (fournit la possibilité d'accéder au résultat de la méthode).
@Secured ne supporte pas le Spring Expression Language et est utilisé pour spécifier une liste de rôles sur une méthode.
@RolesAllowed ne supporte pas le langage d'expression de Spring et est l'annotation JSR 250 équivalente à 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

}
Enter fullscreen mode Exit fullscreen mode

securedEnabled = true active l'annotation @Secured.
jsr250Enabled = true active l'annotation @RolesAllowed.
prePostEnabled = true active les annotations @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter.

Après les avoir activées, 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 = "Partner")
@RestController @RequestMapping(path = "api/partner")
public class AuthorApi {

    // Details omitted for brevity

    @RolesAllowed(Role.PARTNER_ADMIN)
    @PostMapping
    public void create() { }

    @RolesAllowed(Role.PARTNER_ADMIN)
    @PutMapping("{id}")
    public void edit() { }

    @RolesAllowed(Role.PARTNER_ADMIN)
    @DeleteMapping("{id}")
    public void delete() { }

    @GetMapping("{id}")
    public void get() { }

    @GetMapping("{id}/product")
    public void getProducts() { }

    @PostMapping("search")
    public void search() { }

}

@Api(tags = "Product")
@RestController @RequestMapping(path = "api/product")
public class BookApi {

    // Details omitted for brevity

    @RolesAllowed(Role.PRODUCT_ADMIN)
    @PostMapping
    public BookView create() { }

    @RolesAllowed(Role.PRODUCT_ADMIN)
    @PutMapping("{id}")
    public void edit() { }

    @RolesAllowed(Role.PRODUCT_ADMIN)
    @DeleteMapping("{id}")
    public void delete() { }

    @GetMapping("{id}")
    public void get() { }

    @GetMapping("{id}/partner")
    public void getPartners() { }

    @PostMapping("search")
    public void search() { }

}
Enter fullscreen mode Exit fullscreen mode

Veuillez noter que les annotations de sécurité peuvent être fournies tant au niveau de la classe qu'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 ensemble riche d'annotations, et vous pouvez gérer un schéma d'autorisation complexe si vous choisissez de les utiliser.

Nom du rôle Préfixe par défaut

Dans cette sous-section distincte, je veux souligner un autre détail subtil qui déroute beaucoup de nouveaux utilisateurs.

Le framework Spring Security différencie deux termes :

Autorité représente une permission individuelle.
Le rôle représente un groupe de permissions.
Les deux peuvent être représentés par une interface unique appelée GrantedAuthority et vérifiés ultérieurement avec le langage d'expression de Spring dans les annotations de Spring Security comme suit :

Authority : @PreAuthorize("hasAuthority('EDIT_PRODUCT')")
Role : @PreAuthorize("hasRole('PRODUCT_ADMIN')")
Pour rendre plus explicite la différence entre ces deux termes, le framework Spring Security ajoute par défaut un préfixe ROLE_ au nom du rôle. Ainsi, au lieu de vérifier la présence d'un rôle nommé PRODUCT_ADMIN, il vérifiera la présence de ROLE_PRODUCT_ADMIN.

Personnellement, je trouve ce comportement déroutant et je 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
    }

}
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
tonux_samb
NDONGO TONUX SAMB

Posted on May 6, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related