Spring Security mit JWT für die REST-API

Veröffentlicht: 2022-03-11

Spring gilt als vertrauenswürdiges Framework im Java-Ökosystem und ist weit verbreitet. Es ist nicht mehr gültig, Spring als Framework zu bezeichnen, da es sich eher um einen Oberbegriff handelt, der verschiedene Frameworks abdeckt. Eines dieser Frameworks ist Spring Security, ein leistungsstarkes und anpassbares Authentifizierungs- und Autorisierungsframework. Es gilt als De-facto-Standard zum Sichern von Spring-basierten Anwendungen.

Trotz seiner Popularität muss ich zugeben, dass Single-Page-Anwendungen nicht einfach und unkompliziert zu konfigurieren sind. Ich vermute, der Grund ist, dass es eher als anwendungsorientiertes MVC-Framework begann, bei dem das Rendern von Webseiten auf der Serverseite erfolgt und die Kommunikation sitzungsbasiert ist.

Wenn das Backend auf Java und Spring basiert, ist es sinnvoll, Spring Security für die Authentifizierung/Autorisierung zu verwenden und es für die zustandslose Kommunikation zu konfigurieren. Obwohl es viele Artikel gibt, die erklären, wie das gemacht wird, war es für mich immer noch frustrierend, es zum ersten Mal einzurichten, und ich musste Informationen aus mehreren Quellen lesen und zusammenfassen. Aus diesem Grund habe ich mich entschlossen, diesen Artikel zu schreiben, in dem ich versuchen werde, alle erforderlichen subtilen Details und Schwächen, denen Sie während des Konfigurationsprozesses begegnen könnten, zusammenzufassen und abzudecken.

Terminologie definieren

Bevor ich in die technischen Details eintauche, möchte ich die im Kontext von Spring Security verwendete Terminologie explizit definieren, nur um sicherzugehen, dass wir alle die gleiche Sprache sprechen.

Dies sind die Begriffe, die wir ansprechen müssen:

  • Authentifizierung bezieht sich auf den Prozess der Überprüfung der Identität eines Benutzers basierend auf bereitgestellten Anmeldeinformationen. Ein gängiges Beispiel ist die Eingabe eines Benutzernamens und eines Passworts, wenn Sie sich bei einer Website anmelden. Sie können es sich als Antwort auf die Frage „ Wer bist du?“ vorstellen. .
  • Autorisierung bezieht sich auf den Prozess der Bestimmung, ob ein Benutzer die richtige Berechtigung hat, eine bestimmte Aktion auszuführen oder bestimmte Daten zu lesen, vorausgesetzt, dass der Benutzer erfolgreich authentifiziert wurde. Sie können es sich als Antwort auf die Frage vorstellen Kann ein Benutzer dies tun/lesen? .
  • Prinzip bezieht sich auf den aktuell authentifizierten Benutzer.
  • Gewährte Berechtigung bezieht sich auf die Berechtigung des authentifizierten Benutzers.
  • Rolle bezieht sich auf eine Gruppe von Berechtigungen des authentifizierten Benutzers.

Erstellen einer grundlegenden Federanwendung

Bevor wir zur Konfiguration des Spring Security-Frameworks übergehen, erstellen wir eine grundlegende Spring-Webanwendung. Dazu können wir einen Spring Initializr verwenden und ein Vorlagenprojekt generieren. Für eine einfache Webanwendung reicht nur eine Spring-Webframework-Abhängigkeit:

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

Nachdem wir das Projekt erstellt haben, können wir ihm wie folgt einen einfachen REST-Controller hinzufügen:

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

Wenn wir danach das Projekt erstellen und ausführen, können wir im Webbrowser auf die folgenden URLs zugreifen:

  • http://localhost:8080/hello/user gibt die Zeichenfolge Hello User zurück.
  • http://localhost:8080/hello/admin gibt die Zeichenfolge Hello Admin zurück.

Jetzt können wir das Spring Security-Framework zu unserem Projekt hinzufügen, und wir können dies tun, indem wir unserer pom.xml -Datei die folgende Abhängigkeit hinzufügen:

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

Das Hinzufügen anderer Spring-Framework-Abhängigkeiten hat normalerweise keine unmittelbaren Auswirkungen auf eine Anwendung, bis wir die entsprechende Konfiguration bereitstellen, aber Spring Security unterscheidet sich dadurch, dass es eine unmittelbare Wirkung hat, und dies verwirrt normalerweise neue Benutzer. Wenn wir nach dem Hinzufügen das Projekt neu erstellen und ausführen und dann versuchen, auf eine der oben genannten URLs zuzugreifen, anstatt das Ergebnis anzuzeigen, werden wir zu http://localhost:8080/login umgeleitet. Dies ist das Standardverhalten, da das Spring Security-Framework eine standardmäßige Authentifizierung für alle URLs erfordert.

Um die Authentifizierung zu bestehen, können wir den Standardbenutzernamen user verwenden und ein automatisch generiertes Passwort in unserer Konsole finden:

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

Bitte denken Sie daran, dass sich das Passwort jedes Mal ändert, wenn wir die Anwendung erneut ausführen. Wenn wir dieses Verhalten ändern und das Passwort statisch machen möchten, können wir unserer Datei „ application.properties “ die folgende Konfiguration hinzufügen:

 spring.security.user.password=Test12345_

Wenn wir nun Anmeldeinformationen in das Anmeldeformular eingeben, werden wir zurück zu unserer URL umgeleitet und sehen das richtige Ergebnis. Bitte beachten Sie, dass der standardmäßige Authentifizierungsprozess sitzungsbasiert ist, und wenn wir uns abmelden möchten, können wir auf die folgende URL zugreifen: http://localhost:8080/logout

Dieses sofort einsatzbereite Verhalten kann für klassische MVC-Webanwendungen mit sitzungsbasierter Authentifizierung nützlich sein, aber im Fall von Single-Page-Anwendungen ist es normalerweise nicht nützlich, da wir in den meisten Anwendungsfällen clientseitig arbeiten Rendering und JWT-basierte zustandslose Authentifizierung. In diesem Fall müssen wir das Spring Security-Framework stark anpassen, was wir im Rest des Artikels tun werden.

Als Beispiel werden wir eine klassische Buchhandlungs-Webanwendung implementieren und ein Backend erstellen, das CRUD-APIs zum Erstellen von Autoren und Büchern sowie APIs für die Benutzerverwaltung und -authentifizierung bereitstellt.

Überblick über die Spring Security-Architektur

Bevor wir mit dem Anpassen der Konfiguration beginnen, lassen Sie uns zunächst besprechen, wie die Spring Security-Authentifizierung hinter den Kulissen funktioniert.

Das folgende Diagramm stellt den Ablauf dar und zeigt, wie Authentifizierungsanfragen verarbeitet werden:

Spring-Sicherheitsarchitektur

Spring-Sicherheitsarchitektur

Lassen Sie uns nun dieses Diagramm in Komponenten zerlegen und jede einzeln besprechen.

Spring Security Filterkette

Wenn Sie Ihrer Anwendung das Spring Security-Framework hinzufügen, registriert es automatisch eine Filterkette, die alle eingehenden Anfragen abfängt. Diese Kette besteht aus verschiedenen Filtern, von denen jeder einen bestimmten Anwendungsfall behandelt.

Zum Beispiel:

  • Überprüfen Sie basierend auf der Konfiguration, ob die angeforderte URL öffentlich zugänglich ist.
  • Prüfen Sie bei sitzungsbasierter Authentifizierung, ob der Benutzer in der aktuellen Sitzung bereits authentifiziert ist.
  • Überprüfen Sie, ob der Benutzer berechtigt ist, die angeforderte Aktion auszuführen, und so weiter.

Ein wichtiges Detail, das ich erwähnen möchte, ist, dass Spring Security-Filter mit der niedrigsten Ordnung registriert werden und die ersten Filter sind, die aufgerufen werden. Wenn Sie Ihren benutzerdefinierten Filter in einigen Anwendungsfällen vor ihnen platzieren möchten, müssen Sie deren Reihenfolge auffüllen. Dies ist mit folgender Konfiguration möglich:

 spring.security.filter.order=10

Sobald wir diese Konfiguration zu unserer Datei „ application.properties “ hinzugefügt haben, haben wir Platz für 10 benutzerdefinierte Filter vor den Spring Security-Filtern.

AuthenticationManager

Sie können sich AuthenticationManager als einen Koordinator vorstellen, bei dem Sie mehrere Anbieter registrieren können und der je nach Anforderungstyp eine Authentifizierungsanforderung an den richtigen Anbieter übermittelt.

Authentifizierungsanbieter

AuthenticationProvider verarbeitet bestimmte Arten der Authentifizierung. Seine Schnittstelle stellt nur zwei Funktionen zur Verfügung:

  • authenticate führt die Authentifizierung mit der Anfrage durch.
  • supports überprüft, ob dieser Anbieter den angegebenen Authentifizierungstyp unterstützt.

Eine wichtige Implementierung der Schnittstelle, die wir in unserem Beispielprojekt verwenden, ist DaoAuthenticationProvider , das Benutzerdetails von einem UserDetailsService .

UserDetailsService

UserDetailsService wird in der Spring-Dokumentation als Kernschnittstelle beschrieben, die benutzerspezifische Daten lädt.

In den meisten Anwendungsfällen extrahieren Authentifizierungsanbieter Benutzeridentitätsinformationen basierend auf Anmeldeinformationen aus einer Datenbank und führen dann eine Validierung durch. Da dieser Anwendungsfall so häufig vorkommt, haben sich die Spring-Entwickler entschieden, ihn als separate Schnittstelle zu extrahieren, die die einzelne Funktion verfügbar macht:

  • loadUserByUsername akzeptiert den Benutzernamen als Parameter und gibt das Benutzeridentitätsobjekt zurück.

Authentifizierung mit JWT mit Spring Security

Nachdem wir die Interna des Spring Security-Frameworks besprochen haben, konfigurieren wir es für die zustandslose Authentifizierung mit einem JWT-Token.

Um Spring Security anzupassen, benötigen wir eine Konfigurationsklasse, die mit der Annotation @EnableWebSecurity in unserem Klassenpfad versehen ist. Um den Anpassungsprozess zu vereinfachen, macht das Framework außerdem eine WebSecurityConfigurerAdapter -Klasse verfügbar. Wir werden diesen Adapter erweitern und seine beiden Funktionen überschreiben, um:

  1. Konfigurieren Sie den Authentifizierungsmanager mit dem richtigen Anbieter
  2. Konfigurieren Sie die Websicherheit (öffentliche URLs, private URLs, Autorisierung usw.)
 @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 } }

In unserer Beispielanwendung speichern wir Benutzeridentitäten in einer MongoDB-Datenbank in der users . Diese Identitäten werden von der User -Entität zugeordnet, und ihre CRUD-Vorgänge werden vom UserRepo Spring Data-Repository definiert.

Wenn wir nun die Authentifizierungsanforderung akzeptieren, müssen wir die korrekte Identität mithilfe der bereitgestellten Anmeldeinformationen aus der Datenbank abrufen und dann verifizieren. Dazu benötigen wir die Implementierung der UserDetailsService Schnittstelle, die wie folgt definiert ist:

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

Hier können wir sehen, dass es erforderlich ist, das Objekt zurückzugeben, das die UserDetails -Schnittstelle implementiert, und unsere User -Entität implementiert es (Einzelheiten zur Implementierung finden Sie im Repository des Beispielprojekts). In Anbetracht der Tatsache, dass es nur den Einzelfunktionsprototyp verfügbar macht, können wir es als funktionale Schnittstelle behandeln und die Implementierung als Lambda-Ausdruck bereitstellen.

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

Hier initiiert der Funktionsaufruf auth.userDetailsService die DaoAuthenticationProvider Instanz unter Verwendung unserer Implementierung der UserDetailsService Schnittstelle und registriert sie im Authentifizierungsmanager.

Zusammen mit dem Authentifizierungsanbieter müssen wir einen Authentifizierungsmanager mit dem richtigen Kennwortcodierungsschema konfigurieren, das für die Überprüfung der Anmeldeinformationen verwendet wird. Dazu müssen wir die bevorzugte Implementierung der PasswordEncoder -Schnittstelle als Bean verfügbar machen.

In unserem Beispielprojekt verwenden wir den Passwort-Hashing-Algorithmus 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 }

Nachdem wir den Authentifizierungsmanager konfiguriert haben, müssen wir nun die Websicherheit konfigurieren. Wir implementieren eine REST-API und benötigen eine zustandslose Authentifizierung mit einem JWT-Token. Daher müssen wir die folgenden Optionen festlegen:

  • Aktivieren Sie CORS und deaktivieren Sie CSRF.
  • Setzen Sie die Sitzungsverwaltung auf zustandslos.
  • Legen Sie den Ausnahmehandler für nicht autorisierte Anforderungen fest.
  • Legen Sie Berechtigungen für Endpunkte fest.
  • JWT-Tokenfilter hinzufügen.

Diese Konfiguration wird wie folgt implementiert:

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

Bitte beachten Sie, dass wir den JwtTokenFilter vor dem Spring Security-internen UsernamePasswordAuthenticationFilter hinzugefügt haben. Wir tun dies, weil wir an dieser Stelle Zugriff auf die Benutzeridentität benötigen, um die Authentifizierung/Autorisierung durchzuführen, und ihre Extraktion erfolgt innerhalb des JWT-Tokenfilters basierend auf dem bereitgestellten JWT-Token. Dies wird wie folgt implementiert:

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

Bevor wir unsere Login-API-Funktion implementieren, müssen wir uns um einen weiteren Schritt kümmern – wir benötigen Zugriff auf den Authentifizierungsmanager. Standardmäßig ist es nicht öffentlich zugänglich, und wir müssen es explizit als Bean in unserer Konfigurationsklasse verfügbar machen.

Dies kann wie folgt erfolgen:

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

Und jetzt sind wir bereit, unsere Login-API-Funktion zu implementieren:

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

Hier überprüfen wir die bereitgestellten Anmeldeinformationen mithilfe des Authentifizierungsmanagers, und im Erfolgsfall generieren wir das JWT-Token und geben es als Antwortheader zusammen mit den Benutzeridentitätsinformationen im Antworttext zurück.

Autorisierung mit Spring Security

Im vorherigen Abschnitt haben wir einen Authentifizierungsprozess eingerichtet und öffentliche/private URLs konfiguriert. Dies mag für einfache Anwendungen ausreichen, aber für die meisten Anwendungsfälle in der Praxis benötigen wir immer rollenbasierte Zugriffsrichtlinien für unsere Benutzer. In diesem Kapitel werden wir uns mit diesem Problem befassen und ein rollenbasiertes Autorisierungsschema mit dem Spring Security-Framework einrichten.

In unserer Beispielanwendung haben wir die folgenden drei Rollen definiert:

  • USER_ADMIN ermöglicht es uns, Anwendungsbenutzer zu verwalten.
  • AUTHOR_ADMIN erlaubt uns, Autoren zu verwalten.
  • BOOK_ADMIN erlaubt uns, Bücher zu verwalten.

Jetzt müssen wir sie auf die entsprechenden URLs anwenden:

  • api/public ist öffentlich zugänglich.
  • api/admin/user kann auf Benutzer mit der Rolle USER_ADMIN .
  • api/author kann auf Benutzer mit der Rolle AUTHOR_ADMIN .
  • api/book kann auf Benutzer mit der Rolle BOOK_ADMIN .

Das Spring Security-Framework bietet uns zwei Möglichkeiten, das Autorisierungsschema einzurichten:

  • URL-basierte Konfiguration
  • Anmerkungsbasierte Konfiguration

Sehen wir uns zunächst an, wie die URL-basierte Konfiguration funktioniert. Es kann wie folgt auf die Websicherheitskonfiguration angewendet werden:

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

Wie Sie sehen können, ist dieser Ansatz einfach und unkompliziert, hat aber einen Nachteil. Das Autorisierungsschema in unserer Anwendung kann komplex sein, und wenn wir alle Regeln an einem einzigen Ort definieren, wird es sehr umfangreich, komplex und schwer lesbar. Aus diesem Grund bevorzuge ich normalerweise die annotationsbasierte Konfiguration.

Das Spring Security-Framework definiert die folgenden Anmerkungen für die Websicherheit:

  • @PreAuthorize unterstützt Spring Expression Language und wird verwendet, um eine ausdrucksbasierte Zugriffssteuerung bereitzustellen, bevor die Methode ausgeführt wird.
  • @PostAuthorize unterstützt die Spring Expression Language und wird verwendet, um nach dem Ausführen der Methode eine ausdrucksbasierte Zugriffssteuerung bereitzustellen (bietet die Möglichkeit, auf das Methodenergebnis zuzugreifen).
  • @PreFilter unterstützt Spring Expression Language und wird verwendet, um die Sammlung oder Arrays zu filtern, bevor die Methode basierend auf von uns definierten benutzerdefinierten Sicherheitsregeln ausgeführt wird.
  • @PostFilter unterstützt Spring Expression Language und wird verwendet, um die zurückgegebene Sammlung oder Arrays zu filtern, nachdem die Methode basierend auf von uns definierten benutzerdefinierten Sicherheitsregeln ausgeführt wurde (bietet die Möglichkeit, auf das Methodenergebnis zuzugreifen).
  • @Secured unterstützt keine Spring Expression Language und wird verwendet, um eine Liste von Rollen für eine Methode anzugeben.
  • @RolesAllowed unterstützt Spring Expression Language nicht und ist die äquivalente Annotation von JSR 250 zur Annotation @Secured .

Diese Anmerkungen sind standardmäßig deaktiviert und können in unserer Anwendung wie folgt aktiviert werden:

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


securedEnabled = true aktiviert die @Secured Anmerkung.
jsr250Enabled = true aktiviert die Annotation @RolesAllowed .
prePostEnabled = true aktiviert die Annotationen @PreAuthorize , @PostAuthorize , @PreFilter , @PostFilter .

Nachdem wir sie aktiviert haben, können wir rollenbasierte Zugriffsrichtlinien auf unseren API-Endpunkten wie folgt durchsetzen:

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

Bitte beachten Sie, dass Sicherheitsanmerkungen sowohl auf Klassenebene als auch auf Methodenebene bereitgestellt werden können.

Die gezeigten Beispiele sind einfach und stellen keine Szenarien aus der realen Welt dar, aber Spring Security bietet eine Vielzahl von Anmerkungen, und Sie können ein komplexes Autorisierungsschema handhaben, wenn Sie sich dafür entscheiden, sie zu verwenden.

Rollenname Standardpräfix

In diesem separaten Unterabschnitt möchte ich ein weiteres subtiles Detail hervorheben, das viele neue Benutzer verwirrt.

Das Spring Security Framework unterscheidet zwei Begriffe:

  • Authority stellt eine individuelle Erlaubnis dar.
  • Role repräsentiert eine Gruppe von Berechtigungen.

Beide können mit einer einzigen Schnittstelle namens GrantedAuthority und später mit Spring Expression Language in den Spring Security-Anmerkungen wie folgt überprüft werden:

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

Um den Unterschied zwischen diesen beiden Begriffen deutlicher zu machen, fügt das Spring Security-Framework dem Rollennamen standardmäßig ein ROLE_ Präfix hinzu. Anstatt also nach einer Rolle mit dem Namen BOOK_ADMIN zu suchen, wird nach ROLE_BOOK_ADMIN .

Ich persönlich finde dieses Verhalten verwirrend und deaktiviere es lieber in meinen Anwendungen. Es kann in der Spring Security-Konfiguration wie folgt deaktiviert werden:

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

Testen mit Spring Security

Um unsere Endpunkte mit Einheiten- oder Integrationstests zu testen, wenn Sie das Spring Security-Framework verwenden, müssen wir zusammen mit spring-boot-starter-test test die Abhängigkeit spring-security-test hinzufügen. Unsere pom.xml -Build-Datei sieht folgendermaßen aus:

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

Diese Abhängigkeit gibt uns Zugriff auf einige Anmerkungen, die verwendet werden können, um Sicherheitskontext zu unseren Testfunktionen hinzuzufügen.

Diese Anmerkungen sind:

  • @WithMockUser kann zu einer Testmethode hinzugefügt werden, um die Ausführung mit einem verspotteten Benutzer zu emulieren.
  • @WithUserDetails kann einer Testmethode hinzugefügt werden, um die Ausführung mit UserDetails zu emulieren, die von UserDetailsService zurückgegeben werden.
  • @WithAnonymousUser kann einer Testmethode hinzugefügt werden, um die Ausführung mit einem anonymen Benutzer zu emulieren. Dies ist nützlich, wenn ein Benutzer die meisten Tests als ein bestimmter Benutzer ausführen und einige Methoden außer Kraft setzen möchte, um anonym zu bleiben.
  • @WithSecurityContext bestimmt, welcher SecurityContext verwendet werden soll, und alle drei oben beschriebenen Anmerkungen basieren darauf. Wenn wir einen bestimmten Anwendungsfall haben, können wir unsere eigene Anmerkung erstellen, die @WithSecurityContext verwendet, um jeden gewünschten SecurityContext zu erstellen. Seine Diskussion würde den Rahmen unseres Artikels sprengen, und bitte lesen Sie die Spring Security-Dokumentation für weitere Details.

Die einfachste Möglichkeit, die Tests mit einem bestimmten Benutzer auszuführen, ist die Verwendung der Annotation @WithMockUser . Wir können damit einen Scheinbenutzer erstellen und den Test wie folgt ausführen:

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

Dieser Ansatz hat jedoch einige Nachteile. Erstens existiert der Scheinbenutzer nicht, und wenn Sie den Integrationstest ausführen, der später die Benutzerinformationen aus der Datenbank abfragt, schlägt der Test fehl. Zweitens ist der Scheinbenutzer die Instanz der Klasse org.springframework.security.core.userdetails.User , bei der es sich um die interne Implementierung der UserDetails Schnittstelle des Spring-Frameworks handelt, und wenn wir unsere eigene Implementierung haben, kann dies später zu Konflikten führen Test Ausführung.

Wenn frühere Nachteile unsere Anwendung blockieren, ist die Annotation @WithUserDetails der richtige Weg. Es wird verwendet, wenn wir benutzerdefinierte UserDetails und UserDetailsService Implementierungen haben. Es wird davon ausgegangen, dass der Benutzer existiert, also müssen wir entweder die eigentliche Zeile in der Datenbank erstellen oder die UserDetailsService Mock-Instanz bereitstellen, bevor wir Tests ausführen.

So können wir diese Anmerkung verwenden:

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

Dies ist eine bevorzugte Anmerkung in den Integrationstests unseres Beispielprojekts, da wir benutzerdefinierte Implementierungen der oben genannten Schnittstellen haben.

Die Verwendung @WithAnonymousUser ermöglicht die Ausführung als anonymer Benutzer. Dies ist besonders praktisch, wenn Sie die meisten Tests mit einem bestimmten Benutzer, aber einige Tests als anonymen Benutzer ausführen möchten. Im Folgenden werden beispielsweise die Testfälle test1 und test2 mit einem Scheinbenutzer und test3 mit einem anonymen Benutzer ausgeführt:

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

Einpacken

Abschließend möchte ich erwähnen, dass das Spring Security Framework wahrscheinlich keinen Schönheitswettbewerb gewinnen wird und definitiv eine steile Lernkurve hat. Ich bin auf viele Situationen gestoßen, in denen es aufgrund seiner anfänglichen Konfigurationskomplexität durch eine selbst entwickelte Lösung ersetzt wurde. Aber sobald Entwickler die Interna verstanden haben und es geschafft haben, die anfängliche Konfiguration einzurichten, wird es relativ einfach zu bedienen.

In diesem Artikel habe ich versucht, alle subtilen Details der Konfiguration zu demonstrieren, und ich hoffe, Sie finden die Beispiele hilfreich. Vollständige Codebeispiele finden Sie im Git-Repository meines Spring Security-Beispielprojekts.