Printemps est considéré comme un framework de confiance dans l'écosystème Java et est largement utilisé. Il n’est plus valable de faire référence à Spring en tant que cadre, car il s’agit davantage d’un terme générique qui couvre divers cadres. L'un de ces cadres est Sécurité du printemps , qui est un cadre d'authentification et d'autorisation puissant et personnalisable. Il est considéré comme la norme de facto pour la sécurisation des applications Spring.
Malgré sa popularité, je dois admettre qu'en ce qui concerne applications d'une seule page , la configuration n’est ni simple ni directe. Je soupçonne que la raison est que cela a commencé plus comme un Application MVC -infrastructure orientée, où le rendu de page 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 / l'autorisation et de le configurer pour une communication sans état. Bien qu'il y ait beaucoup d'articles expliquant comment cela est 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, dans lequel 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.
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 la même langue.
Voici les conditions auxquelles nous devons répondre:
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 projet modèle. Pour une application Web simple, seule une dépendance de framework Web Spring suffit:
org.springframework.boot spring-boot-starter-web
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îne Hello User
.http://localhost:8080/hello/admin
renverra la chaîne Hello Admin
.Maintenant, nous pouvons ajouter le framework Spring Security à notre projet, et nous pouvons le faire en ajoutant la dépendance suivante à notre pom.xml
fichier:
org.springframework.boot spring-boot-starter-security
L'ajout d'autres dépendances du framework Spring n'a normalement pas d'effet immédiat sur une application tant que nous ne fournissons pas la configuration correspondante, mais Spring Security est différent en ce 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 requiert une authentification immédiate pour toutes les URL.
Pour réussir l'authentification, nous pouvons utiliser le nom d'utilisateur par défaut user
et trouvez 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 application.properties
fichier:
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
traitement (langage de programmation)
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 des applications d'une seule page, il n'est généralement pas utile car dans la plupart des cas d'utilisation, nous avons 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.
A titre d'exemple, nous allons implémenter un classique application web librairie 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 des utilisateurs et l'authentification.
Avant de commencer à personnaliser la configuration, voyons d'abord comment l'authentification Spring Security fonctionne en arrière-plan.
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 chacun d'eux séparément.
Lorsque vous ajoutez le framework Spring Security à votre application, il enregistre automatiquement une chaîne de filtres qui intercepte toutes les demandes entrantes. Cette chaîne se compose de différents filtres, et chacun d'eux gère un cas d'utilisation particulier.
Par exemple:
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 placer votre filtre personnalisé devant eux, vous devrez ajouter un remplissage à leur commande. Cela peut être fait avec la configuration suivante:
spring.security.filter.order=10
Une fois que nous ajoutons cette configuration à notre application.properties
fichier, nous aurons de l'espace pour 10 filtres personnalisés devant les filtres Spring Security.
Vous pouvez penser à AuthenticationManager
en tant que coordinateur où vous pouvez enregistrer plusieurs fournisseurs, et en fonction du type de demande, il fournira une demande d'authentification au fournisseur approprié.
AuthenticationProvider
traite des types spécifiques d'authentification. 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 exemple de projet est DaoAuthenticationProvider
, qui récupère les détails de l'utilisateur à partir d'un UserDetailsService
.
UserDetailsService
est décrite 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é des utilisateurs en fonction des informations d'identification d'une base de données, puis effectuent la validation. Parce que ce cas d'utilisation est si courant, les développeurs Spring ont décidé de l'extraire en tant qu'interface distincte, ce qui expose la fonction unique:
loadUserByUsername
accepte le nom d'utilisateur comme paramètre et renvoie l'objet d'identité de l'utilisateur.Après avoir discuté des éléments internes du framework Spring Security, configurons-le pour l'authentification sans état avec un Jeton JWT .
Pour personnaliser Spring Security, nous avons besoin d'une classe de configuration annotée avec @EnableWebSecurity
annotation dans notre classpath. De plus, pour simplifier le processus de personnalisation, le framework expose un WebSecurityConfigurerAdapter
classe. Nous allons étendre cet adaptateur et remplacer ses deux fonctions afin de:
@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 le répertoire users
collection. Ces identités sont mappées par le User
entité, et leurs opérations CRUD sont définies par UserRepo
Référentiel Spring Data.
Désormais, 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 UserDetailsService
interface, 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 le UserDetails
interface, et notre User
l'entité l'implémente (pour plus de détails sur la mise en œuvre, veuillez consulter le référentiel de l'exemple de projet). Compte tenu du fait qu'il expose uniquement le prototype à fonction unique, nous pouvons le traiter comme une interface fonctionnelle et fournir une implémentation comme une 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, le auth.userDetailsService
l'appel de fonction lancera DaoAuthenticationProvider
en utilisant notre implémentation de UserDetailsService
interface et enregistrez-le dans le gestionnaire d'authentification.
Avec le 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 PasswordEncoder
interface comme un bean.
Dans notre exemple de projet, nous utiliserons le bcrypt algorithme de hachage de mot de passe.
@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 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:
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 Spring Security interne UsernamePasswordAuthenticationFilter
. Nous faisons cela parce que nous avons besoin d'accéder à l'identité de l'utilisateur à ce stade pour effectuer l'authentification / l'autorisation, et son extraction se produit à l'intérieur du filtre de jeton JWT basé sur le 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 API de connexion, nous devons nous occuper d'une étape supplémentaire: nous devons accéder au gestionnaire d'authentification. Par défaut, il n'est pas accessible publiquement 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 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 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.
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 configurerons 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 des 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 USER_ADMIN
rôle.api/author
peut accéder aux utilisateurs avec le AUTHOR_ADMIN
rôle.api/book
peut accéder aux utilisateurs avec le BOOK_ADMIN
rôle.Le framework Spring Security nous offre deux options pour configurer le schéma d'autorisation:
Voyons d'abord comment fonctionne la configuration basée sur les URL. Il peut être appliqué à la configuration de 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 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 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:
à quoi sert le langage c
@PreAuthorize
les soutiens Langage d'expression Spring et est utilisé pour fournir un contrôle d'accès basé sur des expressions avant exécuter la méthode.@PostAuthorize
les soutiens Langage d'expression Spring et est utilisé pour fournir un contrôle d'accès basé sur des expressions après exécuter la méthode (offre la possibilité d'accéder au résultat de la méthode).@PreFilter
les soutiens Langage d'expression Spring et est utilisé pour filtrer la collection ou les tableaux avant exécuter la méthode en fonction des règles de sécurité personnalisées que nous définissons.@PostFilter
les soutiens 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 (offre la possibilité d'accéder au résultat de la méthode).@Secured
ne prend pas en charge Langage d'expression Spring et est utilisé pour spécifier une liste de rôles sur une méthode.@RolesAllowed
ne prend pas en charge Langage d'expression Spring et est le JSR 250 Annotation équivalente de @Secured
annotation.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 @Secured
annotation. jsr250Enabled = true
active @RolesAllowed
annotation. prePostEnabled = true
active @PreAuthorize
, @PostAuthorize
, @PreFilter
, @PostFilter
annotations.
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 des annotations de sécurité peuvent être fournies à la fois au niveau de la classe et au niveau de la méthode.
Les exemples illustrés sont simples et ne représentent pas des scénarios du monde réel, 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.
fichier principal c++
Dans cette sous-section distincte, je tiens à souligner un autre détail 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 plus tard vérifié avec Spring Expression Language dans les annotations de 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 ROLE_
préfixe au nom du rôle par défaut. Ainsi, au lieu de rechercher un rôle nommé BOOK_ADMIN
, il recherchera 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 } }
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 spring-security-test
dépendance avec le spring-boot-starter-test
. Notre pom.xml
Le fichier de construction ressemblera à ceci:
org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine org.springframework.security spring-security-test test
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 fictif.@WithUserDetails
peut être ajouté à une méthode de test pour émuler en cours d'exécution avec UserDetails
renvoyé par le UserDetailsService
.@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 ce que SecurityContext
à utiliser, et les trois annotations décrites ci-dessus sont basées dessus. Si nous avons un cas d'utilisation spécifique, nous pouvons créer notre propre annotation qui utilise @WithSecurityContext
pour créer n'importe quel SecurityContext
nous voulons. Sa discussion sort du cadre de notre article, et veuillez vous référer à la documentation de Spring Security pour plus de détails.La manière la plus simple d'exécuter les tests avec un utilisateur spécifique est d'utiliser le @WithMockUser
annotation. Nous pouvons créer un utilisateur fictif avec lui 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 ultérieurement les informations utilisateur dans la base de données, le test échouera. Deuxièmement, l'utilisateur fictif est l'instance de org.springframework.security.core.userdetails.User
, qui est l'implémentation interne du framework Spring de UserDetails
interface, et si nous avons notre propre implémentation, cela peut provoquer des conflits plus tard, lors de l'exécution du test.
Si les inconvénients précédents sont des bloqueurs pour notre application, alors le @WithUserDetails
l'annotation est la voie à suivre. Il est utilisé lorsque nous avons personnalisé UserDetails
et UserDetailsService
implémentations. Il suppose que l'utilisateur existe, nous devons donc soit créer la ligne réelle dans la base de données, soit fournir le UserDetailsService
mock instance 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.
Utilisation de @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 s'exécutera test1 et test2 cas de test 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 } }
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 comprennent ses composants internes et parviennent à mettre en place la configuration initiale, il devient relativement simple à utiliser.
Dans cet article, j'ai essayé de 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 .
Spring Security est un cadre d'authentification et d'autorisation puissant et hautement personnalisable. C'est la norme de facto pour la sécurisation des applications Spring.
Prêt à l'emploi, Spring Security est livré avec une authentification basée sur la session, ce qui est utile pour les applications Web MVC classiques, mais nous pouvons le configurer pour prendre en charge l'authentification sans état basée sur JWT pour les API REST.
Spring Security est assez sécurisé. Il s'intègre facilement aux applications basées sur Spring, prend en charge de nombreux types d'authentification prêts à l'emploi et est capable de programmer la sécurité déclarative.
Parce qu'il s'intègre parfaitement avec d'autres écosystèmes Spring, et de nombreux développeurs préfèrent réutiliser les solutions existantes plutôt que de réinventer la roue.
JSON Web Token (JWT) est une norme pour le codage des informations qui peuvent être transmises en toute sécurité en tant qu'objet JSON.
Nous exposons une API POST publique pour l'authentification, et après avoir transmis les informations d'identification correctes, elle générera un JWT. Si un utilisateur tente d'accéder à l'API protégée, il autorisera l'accès uniquement si une requête a un JWT valide. La validation se produira dans le filtre enregistré dans la chaîne de filtres Spring Security.