Spring Security cu JWT pentru API-ul REST
Publicat: 2022-03-11Spring este considerat un cadru de încredere în ecosistemul Java și este utilizat pe scară largă. Nu mai este valabil să ne referim la Spring ca un cadru, deoarece este mai mult un termen umbrelă care acoperă diverse cadre. Unul dintre aceste cadre este Spring Security, care este un cadru de autentificare și autorizare puternic și personalizabil. Este considerat standardul de facto pentru securizarea aplicațiilor bazate pe Spring.
În ciuda popularității sale, trebuie să recunosc că atunci când vine vorba de aplicații cu o singură pagină, nu este simplu și simplu de configurat. Bănuiesc că motivul este că a început mai mult ca un cadru MVC orientat spre aplicații, în care redarea paginilor web are loc pe partea de server și comunicarea se bazează pe sesiune.
Dacă back-end-ul se bazează pe Java și Spring, este logic să folosiți Spring Security pentru autentificare/autorizare și să îl configurați pentru comunicarea fără stat. Deși există o mulțime de articole care explică cum se face acest lucru, pentru mine, a fost încă frustrant să îl configurez pentru prima dată și a trebuit să citesc și să rezumam informații din mai multe surse. De aceea m-am hotărât să scriu acest articol, în care voi încerca să rezum și să acopăr toate detaliile subtile și labilele necesare pe care le puteți întâlni în timpul procesului de configurare.
Definirea terminologiei
Înainte de a mă scufunda în detaliile tehnice, vreau să definesc în mod explicit terminologia folosită în contextul Spring Security doar pentru a fi sigur că vorbim cu toții aceeași limbă.
Aceștia sunt termenii pe care trebuie să-i abordăm:
- Autentificarea se referă la procesul de verificare a identității unui utilizator, pe baza acreditărilor furnizate. Un exemplu comun este introducerea unui nume de utilizator și a unei parole atunci când vă conectați la un site web. Poți să te gândești la asta ca un răspuns la întrebarea Cine ești? .
- Autorizarea se referă la procesul de a determina dacă un utilizator are permisiunea adecvată pentru a efectua o anumită acțiune sau pentru a citi anumite date, presupunând că utilizatorul este autentificat cu succes. Puteți să vă gândiți la el ca un răspuns la întrebarea Poate un utilizator să facă/să citească asta? .
- Principiul se referă la utilizatorul autentificat în prezent.
- Autoritatea acordată se referă la permisiunea utilizatorului autentificat.
- Rolul se referă la un grup de permisiuni ale utilizatorului autentificat.
Crearea unei aplicații de primăvară de bază
Înainte de a trece la configurația cadrului Spring Security, să creăm o aplicație web Spring de bază. Pentru aceasta, putem folosi un Spring Initializr și putem genera un proiect șablon. Pentru o aplicație web simplă, este suficientă doar o dependență de cadru web Spring:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
Odată ce am creat proiectul, putem adăuga un controler REST simplu, după cum urmează:
@RestController @RequestMapping("hello") public class HelloRestController { @GetMapping("user") public String helloUser() { return "Hello User"; } @GetMapping("admin") public String helloAdmin() { return "Hello Admin"; } }
După aceasta, dacă construim și rulăm proiectul, putem accesa următoarele adrese URL în browser web:
-
http://localhost:8080/hello/user
va returna șirulHello User
. -
http://localhost:8080/hello/admin
va returna șirulHello Admin
.
Acum, putem adăuga cadrul Spring Security la proiectul nostru și putem face acest lucru adăugând următoarea dependență la fișierul nostru pom.xml
:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>
Adăugarea altor dependențe ale cadrului Spring nu are, în mod normal, un efect imediat asupra unei aplicații până când nu furnizăm configurația corespunzătoare, dar Spring Security este diferită prin faptul că are un efect imediat și, de obicei, acest lucru confundă utilizatorii noi. După adăugarea acestuia, dacă reconstruim și rulăm proiectul și apoi încercăm să accesăm una dintre adresele URL menționate mai sus în loc să vedem rezultatul, vom fi redirecționați către http://localhost:8080/login
. Acesta este un comportament implicit, deoarece cadrul Spring Security necesită autentificare imediată pentru toate adresele URL.
Pentru a trece autentificarea, putem folosi numele user
implicit și găsim o parolă generată automat în consola noastră:
Using generated security password: 1fc15145-dfee-4bec-a009-e32ca21c77ce
Vă rugăm să rețineți că parola se schimbă de fiecare dată când reluăm aplicația. Dacă dorim să schimbăm acest comportament și să facem parola statică, putem adăuga următoarea configurație la fișierul nostru application.properties
:
spring.security.user.password=Test12345_
Acum, dacă introducem acreditările în formularul de conectare, vom fi redirecționați înapoi la adresa URL și vom vedea rezultatul corect. Vă rugăm să rețineți că procesul de autentificare out-of-the-box este bazat pe sesiune și, dacă dorim să ne deconectam, putem accesa următoarea adresă URL: http://localhost:8080/logout
Acest comportament ieșit din cutie poate fi util pentru aplicațiile web MVC clasice în care avem autentificare bazată pe sesiune, dar în cazul aplicațiilor cu o singură pagină, de obicei nu este util deoarece, în majoritatea cazurilor de utilizare, avem partea clientului randare și autentificare fără stat bazată pe JWT. În acest caz, va trebui să personalizăm puternic cadrul Spring Security, ceea ce vom face în restul articolului.
De exemplu, vom implementa o aplicație web clasică de librărie și vom crea un back-end care va oferi API-uri CRUD pentru a crea autori și cărți plus API-uri pentru gestionarea și autentificarea utilizatorilor.
Prezentare generală a arhitecturii de securitate de primăvară
Înainte de a începe personalizarea configurației, să discutăm mai întâi cum funcționează autentificarea Spring Security în culise.
Următoarea diagramă prezintă fluxul și arată cum sunt procesate cererile de autentificare:
Arhitectura de securitate de primăvară

Acum, să împărțim această diagramă în componente și să discutăm fiecare dintre ele separat.
Spring Security Filter Chain
Când adăugați cadrul Spring Security la aplicația dvs., acesta înregistrează automat un lanț de filtre care interceptează toate solicitările primite. Acest lanț este format din diferite filtre și fiecare dintre ele se ocupă de un anumit caz de utilizare.
De exemplu:
- Verificați dacă adresa URL solicitată este accesibilă public, pe baza configurației.
- În cazul autentificării bazate pe sesiune, verificați dacă utilizatorul este deja autentificat în sesiunea curentă.
- Verificați dacă utilizatorul este autorizat să efectueze acțiunea solicitată și așa mai departe.
Un detaliu important pe care vreau să-l menționez este că filtrele Spring Security sunt înregistrate cu cea mai mică ordine și sunt primele filtre invocate. Pentru unele cazuri de utilizare, dacă doriți să puneți filtrul personalizat în fața lor, va trebui să adăugați umplutură la comanda lor. Acest lucru se poate face cu următoarea configurație:
spring.security.filter.order=10
Odată ce adăugăm această configurație în fișierul nostru application.properties
, vom avea spațiu pentru 10 filtre personalizate în fața filtrelor Spring Security.
AuthenticationManager
Vă puteți gândi la AuthenticationManager
ca un coordonator în care puteți înregistra mai mulți furnizori și, pe baza tipului de solicitare, va livra o cerere de autentificare furnizorului corect.
AuthenticationProvider
AuthenticationProvider
procesează anumite tipuri de autentificare. Interfața sa expune doar două funcții:
-
authenticate
realizează autentificarea cu cererea. -
supports
verificări dacă acest furnizor acceptă tipul de autentificare indicat.
O implementare importantă a interfeței pe care o folosim în proiectul nostru exemplu este DaoAuthenticationProvider
, care preia detaliile utilizatorului dintr-un UserDetailsService
.
UserDetailsService
UserDetailsService
este descris ca o interfață de bază care încarcă date specifice utilizatorului în documentația Spring.
În majoritatea cazurilor de utilizare, furnizorii de autentificare extrag informații despre identitatea utilizatorului pe baza acreditărilor dintr-o bază de date și apoi efectuează validarea. Deoarece acest caz de utilizare este atât de comun, dezvoltatorii Spring au decis să îl extragă ca o interfață separată, care expune funcția unică:
-
loadUserByUsername
acceptă numele de utilizator ca parametru și returnează obiectul de identitate al utilizatorului.
Autentificare folosind JWT cu Spring Security
După ce am discutat despre elementele interne ale cadrului Spring Security, să-l configuram pentru autentificare fără stat cu un token JWT.
Pentru a personaliza Spring Security, avem nevoie de o clasă de configurare adnotată cu adnotarea @EnableWebSecurity
în calea noastră de clasă. De asemenea, pentru a simplifica procesul de personalizare, cadrul expune o clasă WebSecurityConfigurerAdapter
. Vom extinde acest adaptor și vom suprascrie ambele funcții ale acestuia astfel încât:
- Configurați managerul de autentificare cu furnizorul corect
- Configurați securitatea web (URL-uri publice, URL-uri private, autorizare 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 } }
În aplicația noastră exemplu, stocăm identitățile utilizatorilor într-o bază de date MongoDB, în colecția users
. Aceste identități sunt mapate de entitatea User
, iar operațiunile lor CRUD sunt definite de depozitul UserRepo
Spring Data.
Acum, când acceptăm cererea de autentificare, trebuie să recuperăm identitatea corectă din baza de date folosind acreditările furnizate și apoi să o verificăm. Pentru aceasta, avem nevoie de implementarea interfeței UserDetailsService
, care este definită după cum urmează:
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
Aici, putem vedea că este necesar să returnăm obiectul care implementează interfața UserDetails
, iar entitatea noastră User
o implementează (pentru detalii de implementare, consultați depozitul exemplului de proiect). Având în vedere faptul că expune doar prototipul cu o singură funcție, îl putem trata ca pe o interfață funcțională și putem oferi implementare ca o expresie 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 }
Aici, apelul funcției auth.userDetailsService
va iniția instanța DaoAuthenticationProvider
folosind implementarea noastră a interfeței UserDetailsService
și o va înregistra în managerul de autentificare.
Împreună cu furnizorul de autentificare, trebuie să configuram un manager de autentificare cu schema corectă de codificare a parolei care va fi utilizată pentru verificarea acreditărilor. Pentru aceasta, trebuie să expunem implementarea preferată a interfeței PasswordEncoder
ca un bean.
În proiectul nostru exemplu, vom folosi algoritmul de hashing a parolei 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 }
După ce am configurat managerul de autentificare, acum trebuie să configuram securitatea web. Implementăm un API REST și avem nevoie de autentificare fără stat cu un token JWT; prin urmare, trebuie să setăm următoarele opțiuni:
- Activați CORS și dezactivați CSRF.
- Setați managementul sesiunii la apatrid.
- Setați un handler de excepții pentru cereri neautorizate.
- Setați permisiunile pentru punctele finale.
- Adăugați filtru de simbol JWT.
Această configurație este implementată după cum urmează:
@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); } }
Vă rugăm să rețineți că am adăugat JwtTokenFilter
înainte de Spring Security intern UsernamePasswordAuthenticationFilter
. Facem acest lucru deoarece avem nevoie de acces la identitatea utilizatorului în acest moment pentru a efectua autentificare/autorizare, iar extragerea acesteia are loc în interiorul filtrului de simbol JWT pe baza jetonului JWT furnizat. Aceasta este implementată după cum urmează:
@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); } }
Înainte de a implementa funcția noastră API de conectare, trebuie să avem grijă de încă un pas - avem nevoie de acces la managerul de autentificare. În mod implicit, nu este accesibil public și trebuie să-l expunem în mod explicit ca bean în clasa noastră de configurare.

Acest lucru se poate face după cum urmează:
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
Și acum, suntem gata să implementăm funcția noastră API de conectare:
@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(); } } }
Aici, verificăm acreditările furnizate folosind managerul de autentificare și, în caz de succes, generăm jetonul JWT și îl returnăm ca antet de răspuns împreună cu informațiile de identitate a utilizatorului din corpul răspunsului.
Autorizare cu Spring Security
În secțiunea anterioară, am configurat un proces de autentificare și am configurat adrese URL publice/private. Acest lucru poate fi suficient pentru aplicații simple, dar pentru majoritatea cazurilor de utilizare din lumea reală, avem întotdeauna nevoie de politici de acces bazate pe roluri pentru utilizatorii noștri. În acest capitol, vom aborda această problemă și vom configura o schemă de autorizare bazată pe roluri folosind cadrul Spring Security.
În aplicația noastră exemplu, am definit următoarele trei roluri:
-
USER_ADMIN
ne permite să gestionăm utilizatorii aplicației. -
AUTHOR_ADMIN
ne permite să gestionăm autorii. -
BOOK_ADMIN
ne permite să gestionăm cărți.
Acum, trebuie să le aplicăm adreselor URL corespunzătoare:
-
api/public
este accesibil public. -
api/admin/user
poate accesa utilizatorii cu rolulUSER_ADMIN
. -
api/author
poate accesa utilizatorii cu rolulAUTHOR_ADMIN
. -
api/book
poate accesa utilizatorii cu rolulBOOK_ADMIN
.
Cadrul Spring Security ne oferă două opțiuni pentru a configura schema de autorizare:
- Configurație bazată pe URL
- Configurație bazată pe adnotări
Mai întâi, să vedem cum funcționează configurația bazată pe URL. Poate fi aplicat configurației de securitate web după cum urmează:
@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 }
După cum puteți vedea, această abordare este simplă și directă, dar are un dezavantaj. Schema de autorizare din aplicația noastră poate fi complexă și, dacă definim toate regulile într-un singur loc, va deveni foarte mare, complexă și greu de citit. Din acest motiv, de obicei prefer să folosesc configurația bazată pe adnotări.
Cadrul Spring Security definește următoarele adnotări pentru securitatea web:
-
@PreAuthorize
acceptă Spring Expression Language și este folosit pentru a oferi control de acces bazat pe expresii înainte de a executa metoda. -
@PostAuthorize
acceptă Spring Expression Language și este folosit pentru a oferi control de acces bazat pe expresii după executarea metodei (oferă posibilitatea de a accesa rezultatul metodei). -
@PreFilter
acceptă Spring Expression Language și este folosit pentru a filtra colecția sau matricele înainte de a executa metoda bazată pe regulile de securitate personalizate pe care le definim. -
@PostFilter
acceptă Spring Expression Language și este folosit pentru a filtra colecția sau matricele returnate după executarea metodei pe baza regulilor de securitate personalizate pe care le definim (oferă posibilitatea de a accesa rezultatul metodei). -
@Secured
nu acceptă Spring Expression Language și este folosit pentru a specifica o listă de roluri pe o metodă. -
@RolesAllowed
nu acceptă Spring Expression Language și este adnotarea echivalentă a JSR 250 cu adnotarea@Secured
.
Aceste adnotări sunt dezactivate implicit și pot fi activate în aplicația noastră după cum urmează:
@EnableWebSecurity @EnableGlobalMethodSecurity( securedEnabled = true, jsr250Enabled = true, prePostEnabled = true ) public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity }
securedEnabled = true
activează adnotarea @Secured
.
jsr250Enabled = true
activează adnotarea @RolesAllowed
.
prePostEnabled = true
activează @PreAuthorize
, @PostAuthorize
, @PreFilter
, @PostFilter
.
După ce le-am activat, putem impune politici de acces bazate pe roluri pe punctele noastre finale API astfel:
@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() { } }
Vă rugăm să rețineți că adnotările de securitate pot fi furnizate atât la nivel de clasă, cât și la nivel de metodă.
Exemplele demonstrate sunt simple și nu reprezintă scenarii din lumea reală, dar Spring Security oferă un set bogat de adnotări și puteți gestiona o schemă de autorizare complexă dacă alegeți să le utilizați.
Nume rol Prefix implicit
În această subsecțiune separată, vreau să subliniez încă un detaliu subtil care derutează o mulțime de utilizatori noi.
Cadrul Spring Security diferențiază doi termeni:
-
Authority
reprezintă o permisiune individuală. -
Role
reprezintă un grup de permisiuni.
Ambele pot fi reprezentate cu o singură interfață numită GrantedAuthority
și verificate ulterior cu Spring Expression Language în adnotările Spring Security, după cum urmează:
-
Authority
: @PreAuthorize(„hasAuthority('EDIT_BOOK')”) -
Role
: @PreAuthorize(„hasRole('BOOK_ADMIN')”)
Pentru a face diferența dintre acești doi termeni mai explicită, cadrul Spring Security adaugă un prefix ROLE_
la numele rolului în mod implicit. Deci, în loc să verifice pentru un rol numit BOOK_ADMIN
, va verifica pentru ROLE_BOOK_ADMIN
.
Personal, consider acest comportament confuz și prefer să-l dezactivez în aplicațiile mele. Poate fi dezactivat în configurația Spring Security, după cum urmează:
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Bean GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix } }
Testare cu Spring Security
Pentru a ne testa punctele finale cu teste unitare sau de integrare atunci când folosim cadrul Spring Security, trebuie să adăugăm dependența spring-security-test
împreună cu spring-boot-starter-test
. Fișierul nostru de pom.xml
va arăta astfel:
<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>
Această dependență ne oferă acces la unele adnotări care pot fi folosite pentru a adăuga context de securitate la funcțiile noastre de testare.
Aceste adnotări sunt:
-
@WithMockUser
poate fi adăugat la o metodă de testare pentru a emula rularea cu un utilizator batjocorit. -
@WithUserDetails
poate fi adăugat la o metodă de testare pentru a emula rularea cuUserDetails
returnat deUserDetailsService
. -
@WithAnonymousUser
poate fi adăugat la o metodă de testare pentru a emula rularea cu un utilizator anonim. Acest lucru este util atunci când un utilizator dorește să ruleze majoritatea testelor ca un anumit utilizator și să anuleze câteva metode pentru a fi anonim. -
@WithSecurityContext
determină ceSecurityContext
să folosească și toate cele trei adnotări descrise mai sus se bazează pe acesta. Dacă avem un anumit caz de utilizare, putem crea propria noastră adnotare care folosește@WithSecurityContext
pentru a crea oriceSecurityContext
dorim. Discuția sa nu intră în domeniul de aplicare al articolului nostru și vă rugăm să consultați documentația Spring Security pentru mai multe detalii.
Cel mai simplu mod de a rula testele cu un anumit utilizator este să folosești adnotarea @WithMockUser
. Putem crea un utilizator simulat cu acesta și să rulăm testul după cum urmează:
@Test @WithMockUser(username="[email protected]", roles={"USER_ADMIN"}) public void test() { // Details omitted for brevity }
Această abordare are însă câteva dezavantaje. În primul rând, utilizatorul simulat nu există, iar dacă rulați testul de integrare, care interogă ulterior informațiile despre utilizator din baza de date, testul va eșua. În al doilea rând, utilizatorul simulat este instanța clasei org.springframework.security.core.userdetails.User
, care este implementarea internă a cadrului Spring a interfeței UserDetails
, iar dacă avem propria noastră implementare, aceasta poate provoca conflicte mai târziu, în timpul executarea testului.
Dacă dezavantajele anterioare sunt blocante pentru aplicația noastră, atunci adnotarea @WithUserDetails
este calea de urmat. Este folosit atunci când avem implementări personalizate UserDetails
și UserDetailsService
. Se presupune că utilizatorul există, așa că trebuie fie să creăm rândul real în baza de date, fie să furnizăm instanța simulată UserDetailsService
înainte de a rula teste.
Iată cum putem folosi această adnotare:
@Test @WithUserDetails("[email protected]") public void test() { // Details omitted for brevity }
Aceasta este o adnotare preferată în testele de integrare ale proiectului nostru, deoarece avem implementări personalizate ale interfețelor menționate mai sus.
Utilizarea @WithAnonymousUser
permite rularea ca utilizator anonim. Acest lucru este mai ales convenabil atunci când doriți să rulați majoritatea testelor cu un anumit utilizator, dar câteva teste ca utilizator anonim. De exemplu, următoarele vor rula cazurile de testare test1 și test2 cu un utilizator simulat și test3 cu un utilizator anonim:
@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 } }
Încheierea
În final, aș dori să menționez că framework-ul Spring Security probabil nu va câștiga niciun concurs de frumusețe și cu siguranță are o curbă de învățare abruptă. Am întâlnit multe situații în care a fost înlocuit cu o soluție proprie din cauza complexității configurației sale inițiale. Dar odată ce dezvoltatorii îi înțeleg elementele interne și reușesc să configureze configurația inițială, aceasta devine relativ simplu de utilizat.
În acest articol, am încercat să demonstrez toate detaliile subtile ale configurației și sper că veți găsi exemplele utile. Pentru exemple complete de cod, vă rugăm să consultați depozitul Git al exemplului meu de proiect Spring Security.