Heim >Java >javaLernprogramm >Spring Security und OAuth verstehen
In diesem Artikel befassen wir uns mit der Sicherheit von Spring und erstellen ein Authentifizierungssystem mit OAuth 2.0.
Bevor wir uns mit der Funktionsweise von Spring Security befassen, ist es wichtig, den Lebenszyklus der Anforderungsverarbeitung in einem Java-basierten Webserver zu verstehen. Spring Security lässt sich nahtlos in diesen Lebenszyklus integrieren, um eingehende Anfragen zu schützen.
Der Lebenszyklus der Bearbeitung einer HTTP-Anfrage in einer Spring-basierten Anwendung mit Spring Security umfasst mehrere Phasen, von denen jede eine entscheidende Rolle bei der Verarbeitung, Validierung und Sicherung der Anfrage spielt.
Der Lebenszyklus beginnt, wenn ein Client (z. B. Browser, mobile App oder API-Tool wie Postman) eine HTTP-Anfrage an den Server sendet.
Beispiel:
GET /api/admin/dashboard HTTP/1.1
Der Servlet-Container (z. B. Tomcat) empfängt die Anfrage und delegiert sie an das DispatcherServlet, den Front-Controller in einer Spring-Anwendung. Hier beginnt die Verarbeitungspipeline der Anwendung.
Bevor das DispatcherServlet die Anfrage verarbeitet, wird sie von der Filterkette von Spring Security abgefangen. Die Filterkette ist eine Folge von Filtern, von denen jeder für die Bearbeitung bestimmter Sicherheitsaufgaben verantwortlich ist. Diese Filter stellen sicher, dass die Anfrage die Authentifizierungs- und Autorisierungsanforderungen erfüllt, bevor sie die Anwendungslogik erreicht.
Authentifizierungsfilter:
Diese Filter überprüfen, ob die Anfrage gültige Anmeldeinformationen enthält, z. B. einen Benutzernamen/ein Passwort, ein JWT oder Sitzungscookies.
Autorisierungsfilter:
Nach der Authentifizierung stellen diese Filter sicher, dass der authentifizierte Benutzer über die erforderlichen Rollen oder Berechtigungen verfügt, um auf die angeforderte Ressource zuzugreifen.
Andere Filter:
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
Wenn die Authentifizierung erfolgreich ist, erstellt Spring Security ein Authentifizierungsobjekt und speichert es im SecurityContext. Auf dieses Objekt, das häufig in einem Thread-lokalen Speicher gespeichert ist, kann während des gesamten Anforderungslebenszyklus zugegriffen werden.
Principal: Stellt den authentifizierten Benutzer dar (z. B. Benutzername).
Anmeldeinformationen: Enthält Authentifizierungsdetails wie JWT-Tokens oder Passwörter.
Autoritäten: Enthält dem Benutzer zugewiesene Rollen und Berechtigungen.
Eine Anfrage durchläuft die Authentifizierungsfilter.
Wenn die Anmeldeinformationen gültig sind, wird das Authentifizierungsobjekt erstellt und dem SecurityContext hinzugefügt.
Wenn die Anmeldeinformationen ungültig sind, sendet der ExceptionTranslationFilter eine 401 Unauthorized-Antwort an den Client.
Sobald die Anfrage die Spring Security Filter Chain erfolgreich durchlaufen hat, übernimmt das DispatcherServlet:
Handler-Zuordnung:
Es ordnet die eingehende Anfrage basierend auf der URL und der HTTP-Methode der entsprechenden Controller-Methode zu.
Controller-Aufruf:
Der zugeordnete Controller verarbeitet die Anfrage und gibt die entsprechende Antwort zurück, häufig mit Hilfe anderer Spring-Komponenten wie Dienste und Repositorys.
Spring Security integriert sich über seine Filter in diesen Lebenszyklus und fängt Anfragen im frühesten Stadium ab. Wenn eine Anfrage die Anwendungslogik erreicht, ist sie bereits authentifiziert und autorisiert, wodurch sichergestellt wird, dass nur legitimer Datenverkehr von der Kernanwendung verarbeitet wird.
Das Design von Spring Security stellt sicher, dass Authentifizierung, Autorisierung und andere Sicherheitsmaßnahmen deklarativ gehandhabt werden, was Entwicklern die Flexibilität gibt, das Verhalten nach Bedarf anzupassen oder zu erweitern. Es setzt nicht nur Best Practices durch, sondern vereinfacht auch die Umsetzung komplexer Sicherheitsanforderungen in modernen Anwendungen.
Nachdem wir die Filterkette in Spring Security untersucht haben, wollen wir uns mit einigen anderen Schlüsselkomponenten befassen, die eine entscheidende Rolle im Authentifizierungs- und Autorisierungsprozess spielen.
AuthenticationManager ist eine Schnittstelle, die eine einzelne Methode definiert, Authenticate(Authentication Authentication) , die verwendet wird, um die Anmeldeinformationen eines Benutzers zu überprüfen und festzustellen, ob sie gültig sind. Sie können sich AuthenticationManager als einen Koordinator vorstellen, bei dem Sie mehrere Anbieter registrieren können und der je nach Anforderungstyp eine Authentifizierungsanfrage an den richtigen Anbieter übermittelt.
Ein AuthenticationProvider ist eine Schnittstelle, die einen Vertrag zur Authentifizierung von Benutzern basierend auf ihren Anmeldeinformationen definiert. Es stellt einen bestimmten Authentifizierungsmechanismus dar, z. B. Benutzername/Passwort, OAuth oder LDAP. Es können mehrere AuthenticationProvider-Implementierungen nebeneinander existieren, sodass die Anwendung verschiedene Authentifizierungsstrategien unterstützen kann.
Authentifizierungsobjekt:
Der AuthenticationProvider verarbeitet ein Authentication-Objekt, das die Anmeldeinformationen des Benutzers (z. B. Benutzername und Passwort) kapselt.
Authentifizierungsmethode:
Jeder AuthenticationProvider implementiert die Methode „authenticate“ (Authentifizierungsauthentifizierung), in der sich die eigentliche Authentifizierungslogik befindet. Diese Methode:
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
Ein datenbankgestützter AuthenticationProvider validiert Benutzernamen und Passwörter.
Ein OAuth-basierter AuthenticationProvider validiert von einem externen Identitätsanbieter ausgegebene Token.
UserDetailsService wird in der Spring-Dokumentation als Kernschnittstelle beschrieben, die benutzerspezifische Daten lädt. Es enthält eine einzelne Methode „loadUserByUsername“, die den Benutzernamen als Parameter akzeptiert und das Identitätsobjekt ==User== zurückgibt. Grundsätzlich erstellen wir eine Implementierungsklasse von UserDetailsService, in der wir die Methode „loadUserByUsername“ überschreiben.
* Validates the user’s credentials. * Returns an authenticated `Authentication` object upon success. * Throws an `AuthenticationException` if authentication fails.
Wie all diese drei nun zusammenwirken, ist, dass AuthenticationManager AuthenticationProvider auffordert, die Authentifizierung entsprechend dem angegebenen Anbietertyp fortzusetzen, und die UserDetailsService-Implementierung hilft dem AuthenticationProvider beim Nachweis der Benutzerdetails.
Der Benutzer sendet eine Anfrage mit seinen Anmeldeinformationen (Benutzername und Passwort) oder einem JWT-Token (im Header) an den authentifizierten Endpunkt und die Anfrage wird an den Authentifizierungsfilter weitergeleitet
AuthenticationFilter (z. B. UsernamePasswordAuthenticationFilter):
Dieser benutzerdefinierte Filter erweitert OncePerRequestFilter und wird vor UsernamePasswordAuthenticationFilter platziert. Er extrahiert das Token aus der Anfrage und validiert es.
Wenn das Token gültig ist, erstellt es ein UsernamePasswordAuthenticationToken und setzt dieses Token in den Sicherheitskontext, der der Spring-Sicherheit mitteilt, dass die Anfrage authentifiziert ist, und wenn diese Anfrage an den UsernamePasswordAuthenticationFilter weitergeleitet wird, wird sie einfach weitergeleitet, da sie das UsernamePasswordAuthenticationToken hat
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
Dieses UsernamePasswordAuthenticationToken wird mit Hilfe von AuthenticationManager und AuthenticationProvider generiert, wenn wir Benutzernamen und Passwort anstelle des Tokens übergeben haben, nachdem wir den Benutzernamen und das Passwort mit Hilfe unserer UserDetails-Klasse authentifiziert haben.
* Validates the user’s credentials. * Returns an authenticated `Authentication` object upon success. * Throws an `AuthenticationException` if authentication fails.
UserDetailsService: Der AuthenticationProvider verwendet UserDetailsService, um Benutzerdetails basierend auf dem Benutzernamen zu laden. Und wir stellen dies mit einer Implementierung von UserDetailsService
Anmeldeinformationsvalidierung: Es vergleicht das bereitgestellte Passwort mit dem in den Benutzerdetails gespeicherten (normalerweise mit einem PasswordEncoder).
package com.oauth.backend.services; import com.oauth.backend.entities.User; import com.oauth.backend.repositories.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; @Component public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if(user==null){ throw new UsernameNotFoundException(username); } return new UserDetailsImpl(user); } public UserDetails loadUserByEmail(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email); if(user==null){ throw new UsernameNotFoundException(email); } return new UserDetailsImpl(user); } }
Jetzt müssen all diese verschiedenen Filter und Beans konfiguriert werden, damit Spring Security weiß, was zu tun ist. Daher erstellen wir eine Konfigurationsklasse, in der wir die gesamte Konfiguration angeben.
@Component public class JWTFilter extends OncePerRequestFilter { private final JWTService jwtService; private final UserDetailsService userDetailsService; public JWTFilter(JWTService jwtService,UserDetailsService userDetailsService) { this.jwtService = jwtService; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); if(authHeader == null || !authHeader.startsWith("Bearer")) { filterChain.doFilter(request,response); return; } final String jwt = authHeader.substring(7); final String userName = jwtService.extractUserName(jwt); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(userName !=null && authentication == null) { //Authenticate UserDetails userDetails = userDetailsService.loadUserByUsername(userName); if(jwtService.isTokenValid(jwt,userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); SecurityContextHolder.getContext() .setAuthentication(authenticationToken); } } filterChain.doFilter(request,response); } }
Bis jetzt haben wir unsere Authentifizierung mit Hilfe von Spring Security verstanden und konfiguriert, jetzt ist es an der Zeit, sie zu testen.
Wir erstellen eine einfache App mit zwei Controllern: AuthController (verwaltet Anmeldung und Registrierung) und ProductController (geschützter Dummy-Controller)
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
* Validates the user’s credentials. * Returns an authenticated `Authentication` object upon success. * Throws an `AuthenticationException` if authentication fails.
package com.oauth.backend.services; import com.oauth.backend.entities.User; import com.oauth.backend.repositories.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; @Component public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if(user==null){ throw new UsernameNotFoundException(username); } return new UserDetailsImpl(user); } public UserDetails loadUserByEmail(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email); if(user==null){ throw new UsernameNotFoundException(email); } return new UserDetailsImpl(user); } }
@Component public class JWTFilter extends OncePerRequestFilter { private final JWTService jwtService; private final UserDetailsService userDetailsService; public JWTFilter(JWTService jwtService,UserDetailsService userDetailsService) { this.jwtService = jwtService; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); if(authHeader == null || !authHeader.startsWith("Bearer")) { filterChain.doFilter(request,response); return; } final String jwt = authHeader.substring(7); final String userName = jwtService.extractUserName(jwt); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(userName !=null && authentication == null) { //Authenticate UserDetails userDetails = userDetailsService.loadUserByUsername(userName); if(jwtService.isTokenValid(jwt,userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); SecurityContextHolder.getContext() .setAuthentication(authenticationToken); } } filterChain.doFilter(request,response); } }
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception{ return config.getAuthenticationManager(); }
@Bean public AuthenticationProvider authenticationProvider(){ DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(userDetailsServiceImpl); authenticationProvider.setPasswordEncoder(passwordEncoder); return authenticationProvider; }
Bisher haben wir eine Registrierung, Anmeldung und Verifizierung implementiert, aber was ist, wenn ich auch die Funktion „Anmeldung mit Google/Github“ hinzufügen möchte, dann können wir dies mit Hilfe von OAuth2.0 tun
OAuth 2.0 ist ein Protokoll zur Autorisierung, mit dem Benutzer Anwendungen Dritter Zugriff auf Ressourcen gewähren können, die auf anderen Plattformen (z. B. Google Drive, Github) gespeichert sind, ohne die Anmeldeinformationen dieser Plattformen weiterzugeben.
Es wird hauptsächlich zum Aktivieren sozialer Anmeldungen wie „Mit Google anmelden“ oder „Mit Github anmelden“ verwendet.
Plattformen wie Google, Facebook und Github bieten einen Autorisierungsserver, der das OAuth 2.0-Protokoll für diese soziale Anmeldung oder Autorisierung des Zugriffs implementiert.
Jetzt werden wir uns jedes Konzept einzeln ansehen
Ressourceneigentümer ist der Benutzer, der die Drittanbieteranwendung (Ihre Anwendung) autorisieren möchte.
Es ist Ihre (Drittanbieter-)Anwendung, die auf die Daten oder Ressourcen vom Ressourcenserver zugreifen möchte.
Es ist der Server, auf dem die Daten des Benutzers gespeichert werden und auf den die Drittanbieteranwendung zugreifen kann.
Der Server, der den Ressourcenbesitzer authentifiziert und Zugriffstokens an den Client ausgibt (z. B. Google-Konten).
Ein vom Autorisierungsserver an den Client ausgestellter Berechtigungsnachweis, der es ihm ermöglicht, im Namen des Benutzers auf den Ressourcenserver zuzugreifen. Es ist im Allgemeinen nur von kurzer Dauer und läuft sehr bald ab. Daher wird auch ein Aktualisierungstoken bereitgestellt, um dieses Zugriffstoken zu aktualisieren, sodass der Benutzer sich nicht erneut autorisieren muss.
Spezifische vom Benutzer gewährte Berechtigungen, die definieren, was der Client mit den Daten des Benutzers tun kann und was nicht. Für die Autorisierung benötigen wir beispielsweise nur Benutzerinformationen wie Profil, Name usw., für den Dateizugriff ist jedoch ein anderer Bereich erforderlich.
Es bezieht sich auf die Methoden, mit denen die Clientanwendung das Zugriffstoken vom Autorisierungsserver erhalten kann. Eine Gewährung definiert den Prozess und die Bedingungen, unter denen die Clientanwendung berechtigt ist, auf die geschützten Daten eines Ressourcenbesitzers zuzugreifen.
Es ist sicher, da wir unser Client-Geheimnis und andere Anmeldeinformationen nicht dem Browser preisgeben müssen
Es gibt zwei am häufigsten verwendete Grant-Typen, die von OAuth 2.0 bereitgestellt werden
Es ist die am häufigsten verwendete Art der Gewährung/Methode, die sicherste und eignet sich für serverseitige Anwendungen
Dabei wird vom Client ein Autorisierungscode an das Backend übergeben und das Backend gibt dem Client das Zugriffstoken.
Prozess:
Wird von Single-Page-Apps (SPAs) oder Anwendungen ohne Backend verwendet. Dabei wird das Zugriffstoken direkt im Browser selbst generiert und ausgegeben.
Prozess:
Wir werden beide zum vollständigen Verständnis separat implementieren, aber zuerst werden wir die Autorisierungscode-Gewährung implementieren, die wir dafür benötigen
Autorisierungsserver
Es kann eine Plattform sein (wie Google, Github) oder Sie können Ihre eigene erstellen, auch mit KeyCloak, oder Sie können auch Ihre eigene erstellen, die den OAuth 2.0-Standards entspricht (vielleicht machen wir das im nächsten Blog?)
Spring Boot-Anwendung
Dies wird unsere wichtigste Backend-Anwendung/-Dienst sein, die alle Vorgänge wie Codeaustausch, Überprüfung, Speichern von Benutzerdetails und Zuweisen von JWT-Tokens abwickelt
React-Anwendung (Frontend)
Dies wird unser Client sein, der den Benutzer zur Autorisierung an den Autorisierungsserver weiterleitet.
In unserer Implementierung werden wir also Folgendes tun: Das Frontend (Web/App) leitet unseren Benutzer zum Google-Login mit einer Umleitungs-URI zu unserem Backend-Endpunkt weiter, der die Kontrolle weiter übernimmt. Darüber werden wir später und zusammen mit der Redirect_URL sprechen Wir übergeben auch die Client-ID unserer App. All diese werden in den Abfrageparametern gesendet.
Nein, wenn sich der Benutzer erfolgreich bei Google anmeldet, leitet der Autorisierungsserver (Google) unsere Anfrage an den Backend-Enpoint weiter und dort tauschen wir den Autorisierungscode mit dem Autorisierungsserver aus, um Zugriffstoken und Aktualisierungstoken zu erhalten, und dann Wir können die Authentifizierung nach Belieben durchführen und schließlich eine Antwort an unser Frontend zurücksenden, das ein Cookie enthält und zu unserem Dashboard weiterleitet oder möglicherweise eine geschützte Seite ist.
Jetzt schauen wir uns den Code an, aber stellen Sie sicher, dass Sie die URL Ihres Backend-Endpunkts in den autorisierten Weiterleitungs-URLs im Google-Konsolen-Dashboard für den OAuth-Client hinzufügen.
* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks. * **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains. * **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
Und das ist alles, es wird gut funktionieren und zum Testen können Sie eine einfache Frontend-Anwendung erstellen, die nichts anderes bietet, als einen Kontext zu haben und Sie kennen die Anmelde- und Registrierungsfunktionen.
Vielen Dank, dass Sie bis hierhin gelesen haben. Wenn Sie einen Vorschlag haben, schreiben Sie ihn bitte in die Kommentare
Das obige ist der detaillierte Inhalt vonSpring Security und OAuth verstehen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!