Spring Security con JWT per API REST

Pubblicato: 2022-03-11

La primavera è considerata una struttura affidabile nell'ecosistema Java ed è ampiamente utilizzata. Non è più valido fare riferimento a Spring come a un framework, poiché è più un termine generico che copre vari framework. Uno di questi framework è Spring Security, un framework di autenticazione e autorizzazione potente e personalizzabile. È considerato lo standard de facto per la protezione delle applicazioni basate su Spring.

Nonostante la sua popolarità, devo ammettere che quando si tratta di applicazioni a pagina singola, non è semplice e lineare da configurare. Sospetto che il motivo sia che è iniziato più come un framework MVC orientato alle applicazioni, in cui il rendering delle pagine Web avviene sul lato server e la comunicazione è basata sulla sessione.

Se il back-end è basato su Java e Spring, ha senso utilizzare Spring Security per l'autenticazione/autorizzazione e configurarlo per la comunicazione stateless. Sebbene ci siano molti articoli che spiegano come farlo, per me è stato comunque frustrante configurarlo per la prima volta e ho dovuto leggere e riassumere informazioni da più fonti. Ecco perché ho deciso di scrivere questo articolo, in cui cercherò di riassumere e coprire tutti i dettagli sottili e le debolezze richieste che potresti incontrare durante il processo di configurazione.

Definizione della terminologia

Prima di addentrarmi nei dettagli tecnici, voglio definire esplicitamente la terminologia utilizzata nel contesto della Spring Security proprio per essere sicuro che parliamo tutti la stessa lingua.

Questi sono i termini che dobbiamo affrontare:

  • L' autenticazione si riferisce al processo di verifica dell'identità di un utente, in base alle credenziali fornite. Un esempio comune è l'inserimento di un nome utente e di una password quando si accede a un sito Web. Puoi pensarla come una risposta alla domanda Chi sei? .
  • L'autorizzazione si riferisce al processo per determinare se un utente dispone dell'autorizzazione adeguata per eseguire una determinata azione o leggere dati particolari, presupponendo che l'utente sia autenticato correttamente. Puoi pensarla come una risposta alla domanda Può un utente fare/leggere questo? .
  • Il principio si riferisce all'utente attualmente autenticato.
  • L' autorizzazione concessa si riferisce all'autorizzazione dell'utente autenticato.
  • Il ruolo si riferisce a un gruppo di autorizzazioni dell'utente autenticato.

Creazione di un'applicazione primaverile di base

Prima di passare alla configurazione del framework Spring Security, creiamo un'applicazione web Spring di base. Per questo, possiamo utilizzare Spring Initializr e generare un progetto modello. Per una semplice applicazione web è sufficiente solo una dipendenza dal framework web Spring:

 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>

Una volta creato il progetto, possiamo aggiungere un semplice controller REST come segue:

 @RestController @RequestMapping("hello") public class HelloRestController { @GetMapping("user") public String helloUser() { return "Hello User"; } @GetMapping("admin") public String helloAdmin() { return "Hello Admin"; } }

Dopodiché, se costruiamo ed eseguiamo il progetto, possiamo accedere ai seguenti URL nel browser web:

  • http://localhost:8080/hello/user restituirà la stringa Hello User .
  • http://localhost:8080/hello/admin restituirà la stringa Hello Admin .

Ora possiamo aggiungere il framework Spring Security al nostro progetto e possiamo farlo aggiungendo la seguente dipendenza al nostro file pom.xml :

 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>

L'aggiunta di altre dipendenze del framework Spring normalmente non ha un effetto immediato su un'applicazione finché non forniamo la configurazione corrispondente, ma Spring Security è diverso in quanto ha un effetto immediato e questo di solito confonde i nuovi utenti. Dopo averlo aggiunto, se ricostruiamo ed eseguiamo il progetto e poi proviamo ad accedere a uno dei suddetti URL invece di visualizzare il risultato, verremo reindirizzati a http://localhost:8080/login . Questo è il comportamento predefinito perché il framework Spring Security richiede l'autenticazione predefinita per tutti gli URL.

Per superare l'autenticazione, possiamo utilizzare il nome user utente predefinito e trovare una password generata automaticamente nella nostra console:

 Using generated security password: 1fc15145-dfee-4bec-a009-e32ca21c77ce

Ricorda che la password cambia ogni volta che eseguiamo nuovamente l'applicazione. Se vogliamo modificare questo comportamento e rendere statica la password, possiamo aggiungere la seguente configurazione al nostro file application.properties :

 spring.security.user.password=Test12345_

Ora, se inseriamo le credenziali nel modulo di accesso, verremo reindirizzati al nostro URL e vedremo il risultato corretto. Tieni presente che il processo di autenticazione pronto all'uso è basato sulla sessione e, se vogliamo disconnetterci, possiamo accedere al seguente URL: http://localhost:8080/logout

Questo comportamento pronto all'uso può essere utile per le classiche applicazioni Web MVC in cui abbiamo l'autenticazione basata sulla sessione, ma nel caso di applicazioni a pagina singola, di solito non è utile perché nella maggior parte dei casi d'uso abbiamo lato client rendering e autenticazione stateless basata su JWT. In questo caso, dovremo personalizzare pesantemente il framework Spring Security, cosa che faremo nel resto dell'articolo.

Ad esempio, implementeremo un'applicazione Web classica da libreria e creeremo un back-end che fornirà API CRUD per creare autori e libri, oltre ad API per la gestione e l'autenticazione degli utenti.

Panoramica dell'architettura di sicurezza di primavera

Prima di iniziare a personalizzare la configurazione, discutiamo prima di come funziona l'autenticazione di Spring Security dietro le quinte.

Il diagramma seguente presenta il flusso e mostra come vengono elaborate le richieste di autenticazione:

Architettura di sicurezza primaverile

Architettura di sicurezza primaverile

Ora, suddividiamo questo diagramma in componenti e discutiamo ciascuno di essi separatamente.

Catena di filtri di sicurezza a molla

Quando aggiungi il framework Spring Security alla tua applicazione, registra automaticamente una catena di filtri che intercetta tutte le richieste in arrivo. Questa catena è composta da vari filtri e ognuno di essi gestisce un caso d'uso particolare.

Per esempio:

  • Verifica se l'URL richiesto è accessibile pubblicamente, in base alla configurazione.
  • In caso di autenticazione basata sulla sessione, verificare se l'utente è già autenticato nella sessione corrente.
  • Verificare se l'utente è autorizzato a eseguire l'azione richiesta e così via.

Un dettaglio importante che voglio menzionare è che i filtri Spring Security sono registrati con l'ordine più basso e sono i primi filtri invocati. Per alcuni casi d'uso, se vuoi mettere il tuo filtro personalizzato davanti a loro, dovrai aggiungere il riempimento al loro ordine. Questo può essere fatto con la seguente configurazione:

 spring.security.filter.order=10

Dopo aver aggiunto questa configurazione al nostro file application.properties , avremo spazio per 10 filtri personalizzati davanti ai filtri Spring Security.

Autenticazione Manager

Puoi pensare ad AuthenticationManager come a un coordinatore in cui puoi registrare più provider e, in base al tipo di richiesta, invierà una richiesta di autenticazione al provider corretto.

Provider di autenticazione

AuthenticationProvider elabora tipi specifici di autenticazione. La sua interfaccia espone solo due funzioni:

  • authenticate esegue l'autenticazione con la richiesta.
  • supports controlla se questo provider supporta il tipo di autenticazione indicato.

Un'importante implementazione dell'interfaccia che stiamo usando nel nostro progetto di esempio è DaoAuthenticationProvider , che recupera i dettagli dell'utente da un UserDetailsService .

UserDetailsService

UserDetailsService è descritto come un'interfaccia principale che carica i dati specifici dell'utente nella documentazione di Spring.

Nella maggior parte dei casi d'uso, i provider di autenticazione estraggono le informazioni sull'identità dell'utente in base alle credenziali da un database e quindi eseguono la convalida. Poiché questo caso d'uso è così comune, gli sviluppatori di Spring hanno deciso di estrarlo come un'interfaccia separata, che espone la singola funzione:

  • loadUserByUsername accetta nome utente come parametro e restituisce l'oggetto identità utente.

Autenticazione tramite JWT con Spring Security

Dopo aver discusso gli interni del framework Spring Security, configuriamolo per l'autenticazione stateless con un token JWT.

Per personalizzare Spring Security, abbiamo bisogno di una classe di configurazione annotata con l'annotazione @EnableWebSecurity nel nostro percorso di classe. Inoltre, per semplificare il processo di personalizzazione, il framework espone una classe WebSecurityConfigurerAdapter . Estenderemo questo adattatore e sovrascriveremo entrambe le sue funzioni in modo da:

  1. Configurare il gestore dell'autenticazione con il provider corretto
  2. Configura la sicurezza web (URL pubblici, URL privati, autorizzazione, ecc.)
 @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 } }

Nella nostra applicazione di esempio, memorizziamo le identità degli utenti in un database MongoDB, nella raccolta degli users . Queste identità sono mappate dall'entità User e le loro operazioni CRUD sono definite dal repository UserRepo Spring Data.

Ora, quando accettiamo la richiesta di autenticazione, dobbiamo recuperare l'identità corretta dal database utilizzando le credenziali fornite e quindi verificarla. Per questo, abbiamo bisogno dell'implementazione dell'interfaccia UserDetailsService , che è definita come segue:

 public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }

Qui possiamo vedere che è necessario restituire l'oggetto che implementa l'interfaccia UserDetails e la nostra entità User lo implementa (per i dettagli sull'implementazione, vedere il repository del progetto di esempio). Considerando il fatto che espone solo il prototipo a funzione singola, possiamo trattarlo come un'interfaccia funzionale e fornire l'implementazione come un'espressione 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 }

Qui, la chiamata alla funzione auth.userDetailsService avvierà l'istanza DaoAuthenticationProvider utilizzando la nostra implementazione dell'interfaccia UserDetailsService e la registrerà nel gestore dell'autenticazione.

Insieme al provider di autenticazione, è necessario configurare un gestore di autenticazione con lo schema di codifica della password corretto che verrà utilizzato per la verifica delle credenziali. Per questo, dobbiamo esporre l'implementazione preferita dell'interfaccia PasswordEncoder come bean.

Nel nostro progetto di esempio, utilizzeremo l'algoritmo di hashing della password 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 }

Dopo aver configurato il gestore dell'autenticazione, ora è necessario configurare la sicurezza web. Stiamo implementando un'API REST e abbiamo bisogno dell'autenticazione stateless con un token JWT; pertanto, dobbiamo impostare le seguenti opzioni:

  • Abilita CORS e disabilita CSRF.
  • Imposta la gestione della sessione su stateless.
  • Imposta il gestore delle eccezioni delle richieste non autorizzate.
  • Imposta le autorizzazioni sugli endpoint.
  • Aggiungi filtro token JWT.

Questa configurazione è implementata come segue:

 @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); } }

Si noti che è stato aggiunto JwtTokenFilter prima dello UsernamePasswordAuthenticationFilter interno di Spring Security. Lo stiamo facendo perché a questo punto è necessario accedere all'identità dell'utente per eseguire l'autenticazione/autorizzazione e la sua estrazione avviene all'interno del filtro token JWT in base al token JWT fornito. Questo è implementato come segue:

 @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); } }

Prima di implementare la nostra funzione API di accesso, dobbiamo occuparci di un altro passaggio: abbiamo bisogno dell'accesso al gestore dell'autenticazione. Per impostazione predefinita, non è accessibile pubblicamente e dobbiamo esporlo esplicitamente come bean nella nostra classe di configurazione.

Questo può essere fatto come segue:

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }

E ora siamo pronti per implementare la nostra funzione API di accesso:

 @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(); } } }

Qui, verifichiamo le credenziali fornite utilizzando il gestore dell'autenticazione e, in caso di esito positivo, generiamo il token JWT e lo restituiamo come intestazione di risposta insieme alle informazioni sull'identità dell'utente nel corpo della risposta.

Autorizzazione con Spring Security

Nella sezione precedente, abbiamo impostato un processo di autenticazione e configurato URL pubblici/privati. Questo può essere sufficiente per applicazioni semplici, ma per la maggior parte dei casi d'uso del mondo reale, abbiamo sempre bisogno di criteri di accesso basati sui ruoli per i nostri utenti. In questo capitolo affronteremo questo problema e imposteremo uno schema di autorizzazione basato sui ruoli utilizzando il framework Spring Security.

Nella nostra applicazione di esempio, abbiamo definito i tre ruoli seguenti:

  • USER_ADMIN ci consente di gestire gli utenti dell'applicazione.
  • AUTHOR_ADMIN ci permette di gestire gli autori.
  • BOOK_ADMIN ci permette di gestire i libri.

Ora, dobbiamo applicarli agli URL corrispondenti:

  • api/public è accessibile pubblicamente.
  • api/admin/user può accedere agli utenti con il ruolo USER_ADMIN .
  • api/author può accedere agli utenti con il ruolo AUTHOR_ADMIN .
  • api/book può accedere agli utenti con il ruolo BOOK_ADMIN .

Il framework Spring Security offre due opzioni per impostare lo schema di autorizzazione:

  • Configurazione basata su URL
  • Configurazione basata su annotazioni

Per prima cosa, vediamo come funziona la configurazione basata su URL. Può essere applicato alla configurazione della sicurezza web come segue:

 @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 }

Come puoi vedere, questo approccio è semplice e diretto, ma ha uno svantaggio. Lo schema di autorizzazione nella nostra applicazione può essere complesso e, se definiamo tutte le regole in un unico posto, diventerà molto grande, complesso e difficile da leggere. Per questo motivo, di solito preferisco utilizzare la configurazione basata su annotazioni.

Il framework Spring Security definisce le seguenti annotazioni per la sicurezza web:

  • @PreAuthorize supporta Spring Expression Language e viene utilizzato per fornire il controllo dell'accesso basato sull'espressione prima di eseguire il metodo.
  • @PostAuthorize supporta Spring Expression Language e viene utilizzato per fornire il controllo dell'accesso basato sull'espressione dopo l'esecuzione del metodo (fornisce la possibilità di accedere al risultato del metodo).
  • @PreFilter supporta Spring Expression Language e viene utilizzato per filtrare la raccolta o gli array prima di eseguire il metodo in base alle regole di sicurezza personalizzate che definiamo.
  • @PostFilter supporta Spring Expression Language e viene utilizzato per filtrare la raccolta o gli array restituiti dopo aver eseguito il metodo in base alle regole di sicurezza personalizzate che definiamo (fornisce la possibilità di accedere al risultato del metodo).
  • @Secured non supporta Spring Expression Language e viene utilizzato per specificare un elenco di ruoli su un metodo.
  • @RolesAllowed non supporta Spring Expression Language ed è l'annotazione equivalente di JSR 250 dell'annotazione @Secured .

Queste annotazioni sono disabilitate per impostazione predefinita e possono essere abilitate nella nostra applicazione come segue:

 @EnableWebSecurity @EnableGlobalMethodSecurity( securedEnabled = true, jsr250Enabled = true, prePostEnabled = true ) public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity }


securedEnabled = true abilita l'annotazione @Secured .
jsr250Enabled = true abilita l'annotazione @RolesAllowed .
prePostEnabled = true abilita @PreAuthorize , @PostAuthorize , @PreFilter , @PostFilter .

Dopo averli abilitati, possiamo applicare policy di accesso basate sui ruoli sui nostri endpoint API in questo modo:

 @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() { } }

Tieni presente che le annotazioni di sicurezza possono essere fornite sia a livello di classe che a livello di metodo.

Gli esempi dimostrati sono semplici e non rappresentano scenari del mondo reale, ma Spring Security fornisce un ricco set di annotazioni ed è possibile gestire uno schema di autorizzazione complesso se si sceglie di utilizzarli.

Nome ruolo Prefisso predefinito

In questa sottosezione separata, voglio sottolineare un dettaglio più sottile che confonde molti nuovi utenti.

Il quadro di Spring Security differenzia due termini:

  • Authority rappresenta un'autorizzazione individuale.
  • Role rappresenta un gruppo di autorizzazioni.

Entrambi possono essere rappresentati con un'unica interfaccia chiamata GrantedAuthority e successivamente verificati con Spring Expression Language all'interno delle annotazioni di Spring Security come segue:

  • Authority : @PreAuthorize(“hasAuthority('EDIT_BOOK')”)
  • Role : @PreAuthorize(“hasRole('BOOK_ADMIN')”)

Per rendere più esplicita la differenza tra questi due termini, il framework Spring Security aggiunge un prefisso ROLE_ al nome del ruolo per impostazione predefinita. Quindi, invece di cercare un ruolo chiamato BOOK_ADMIN , verificherà ROLE_BOOK_ADMIN .

Personalmente, trovo questo comportamento confuso e preferisco disabilitarlo nelle mie applicazioni. Può essere disabilitato all'interno della configurazione Spring Security come segue:

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Bean GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix } }

Test con Spring Security

Per testare i nostri endpoint con test di unità o di integrazione quando si utilizza il framework Spring Security, è necessario aggiungere la dipendenza spring-security-test insieme a spring-boot-starter-test . Il nostro file di build pom.xml sarà simile a questo:

 <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>

Questa dipendenza ci dà accesso ad alcune annotazioni che possono essere utilizzate per aggiungere un contesto di sicurezza alle nostre funzioni di test.

Queste annotazioni sono:

  • @WithMockUser può essere aggiunto a un metodo di test per emulare l'esecuzione con un utente deriso.
  • @WithUserDetails può essere aggiunto a un metodo di test per emulare l'esecuzione con UserDetails restituito da UserDetailsService .
  • @WithAnonymousUser può essere aggiunto a un metodo di test per emulare l'esecuzione con un utente anonimo. Ciò è utile quando un utente desidera eseguire la maggior parte dei test come utente specifico e ignorare alcuni metodi per essere anonimo.
  • @WithSecurityContext determina quale SecurityContext utilizzare e tutte e tre le annotazioni sopra descritte si basano su di esso. Se abbiamo un caso d'uso specifico, possiamo creare la nostra annotazione che utilizza @WithSecurityContext per creare qualsiasi SecurityContext che desideriamo. La sua discussione esula dall'ambito del nostro articolo e fare riferimento alla documentazione di Spring Security per ulteriori dettagli.

Il modo più semplice per eseguire i test con un utente specifico è utilizzare l'annotazione @WithMockUser . Possiamo creare un utente fittizio con esso ed eseguire il test come segue:

 @Test @WithMockUser(username="[email protected]", roles={"USER_ADMIN"}) public void test() { // Details omitted for brevity }

Questo approccio ha un paio di inconvenienti, però. Innanzitutto, l'utente fittizio non esiste e se si esegue il test di integrazione, che in seguito richiede le informazioni sull'utente dal database, il test avrà esito negativo. In secondo luogo, l'utente fittizio è l'istanza della classe org.springframework.security.core.userdetails.User , che è l'implementazione interna del framework Spring dell'interfaccia UserDetails , e se abbiamo la nostra implementazione, ciò può causare conflitti in seguito, durante esecuzione della prova.

Se gli svantaggi precedenti sono bloccanti per la nostra applicazione, l'annotazione @WithUserDetails è la strada da percorrere. Viene utilizzato quando abbiamo implementazioni UserDetails e UserDetailsService personalizzate. Presuppone che l'utente esista, quindi dobbiamo creare la riga effettiva nel database o fornire l'istanza UserDetailsService prima di eseguire i test.

Ecco come possiamo usare questa annotazione:

 @Test @WithUserDetails("[email protected]") public void test() { // Details omitted for brevity }

Questa è un'annotazione preferita nei test di integrazione del nostro progetto di esempio perché abbiamo implementazioni personalizzate delle interfacce sopra menzionate.

L'utilizzo @WithAnonymousUser consente l'esecuzione come utente anonimo. Ciò è particolarmente comodo quando desideri eseguire la maggior parte dei test con un utente specifico, ma alcuni test come utente anonimo. Ad esempio, quanto segue eseguirà test case test1 e test2 con un utente fittizio e test3 con un utente anonimo:

 @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 } }

Avvolgendo

Alla fine, vorrei ricordare che il framework Spring Security probabilmente non vincerà nessun concorso di bellezza e ha sicuramente una curva di apprendimento ripida. Ho riscontrato molte situazioni in cui è stato sostituito con una soluzione nostrana a causa della sua complessità di configurazione iniziale. Ma una volta che gli sviluppatori ne comprendono gli interni e riescono a impostare la configurazione iniziale, diventa relativamente semplice da usare.

In questo articolo, ho cercato di dimostrare tutti i dettagli sottili della configurazione e spero che troverai utili gli esempi. Per esempi di codice completi, fare riferimento al repository Git del mio progetto Spring Security di esempio.