spring-boot-jwt/
│
├── src/main/java/
│ └── murraco
│ ├── configuration
│ │ └── SwaggerConfig.java
│ │
│ ├── controller
│ │ └── UserController.java
│ │
│ ├── dto
│ │ ├── UserDataDTO.java
│ │ └── UserResponseDTO.java
│ │
│ ├── exception
│ │ ├── CustomException.java
│ │ └── GlobalExceptionController.java
│ │
│ ├── model
│ │ ├── AppUserRole.java
│ │ └── AppUser.java
│ │
│ ├── repository
│ │ └── UserRepository.java
│ │
│ ├── security
│ │ ├── JwtTokenFilter.java
│ │ ├── JwtTokenFilterConfigurer.java
│ │ ├── JwtTokenProvider.java
│ │ ├── MyUserDetails.java
│ │ └── WebSecurityConfig.java
│ │
│ ├── service
│ │ └── UserService.java
│ │
│ └── JwtAuthServiceApp.java
│
├── src/main/resources/
│ └── application.yml
│
├── .gitignore
├── LICENSE
├── mvnw/mvnw.cmd
├── README.md
└── pom.xml
Nur um ein paar Hintergrundinformationen einzubringen, haben wir eine wunderbare Einführung, mit freundlicher Genehmigung von jwt.io ! Werfen wir einen Blick darauf:
JSON Web Token (JWT) ist ein offener Standard (RFC 7519), der eine kompakte und eigenständige Möglichkeit zur sicheren Übertragung von Informationen zwischen Parteien als JSON-Objekt definiert. Diese Informationen können überprüft und vertrauenswürdig sein, da sie digital signiert sind. JWTs können mit einem Geheimnis (mit dem HMAC-Algorithmus) oder einem öffentlichen/privaten Schlüsselpaar mit RSA signiert werden.
Lassen Sie uns einige Konzepte dieser Definition näher erläutern.
Kompakt : Aufgrund ihrer geringeren Größe können JWTs über eine URL, einen POST-Parameter oder innerhalb eines HTTP-Headers gesendet werden. Darüber hinaus bedeutet die geringere Größe eine schnelle Übertragung.
Eigenständig : Die Nutzlast enthält alle erforderlichen Informationen über den Benutzer, sodass die Datenbank nicht mehr als einmal abgefragt werden muss.
Hier sind einige Szenarien, in denen JSON-Web-Tokens nützlich sind:
Authentifizierung : Dies ist das häufigste Szenario für die Verwendung von JWT. Sobald der Benutzer angemeldet ist, enthält jede nachfolgende Anfrage das JWT, sodass der Benutzer auf Routen, Dienste und Ressourcen zugreifen kann, die mit diesem Token zulässig sind. Single Sign On ist eine Funktion, die JWT heutzutage aufgrund des geringen Overheads und der einfachen Verwendung in verschiedenen Domänen häufig verwendet.
Informationsaustausch : JSON-Web-Tokens sind eine gute Möglichkeit, Informationen sicher zwischen Parteien zu übertragen. Da JWTs beispielsweise mit öffentlichen/privaten Schlüsselpaaren signiert werden können, können Sie sicher sein, dass die Absender die sind, für die sie sich ausgeben. Da die Signatur anhand des Headers und der Nutzlast berechnet wird, können Sie außerdem überprüfen, ob der Inhalt nicht manipuliert wurde.
JSON-Web-Tokens bestehen aus drei durch Punkte (.) getrennten Teilen:
Daher sieht ein JWT normalerweise wie folgt aus.
xxxxx
. yyyyy
. zzzzz
Lassen Sie uns die verschiedenen Teile aufschlüsseln.
Kopfzeile
Der Header besteht typischerweise aus zwei Teilen: dem Typ des Tokens, also JWT, und dem verwendeten Hashing-Algorithmus, etwa HMAC SHA256 oder RSA.
Zum Beispiel:
{
"alg" : " HS256 " ,
"typ" : " JWT "
}
Anschließend wird dieser JSON Base64Url-codiert, um den ersten Teil des JWT zu bilden.
Nutzlast
Der zweite Teil des Tokens ist die Nutzlast, die die Ansprüche enthält. Ansprüche sind Aussagen über eine Entität (typischerweise den Benutzer) und zusätzliche Metadaten. Es gibt drei Arten von Ansprüchen: vorbehaltene, öffentliche und private Ansprüche.
Beachten Sie, dass die Anspruchsnamen nur drei Zeichen lang sind, da JWT kompakt sein soll.
Öffentliche Ansprüche : Diese können von Benutzern von JWTs nach Belieben definiert werden. Um jedoch Kollisionen zu vermeiden, sollten sie in der IANA JSON Web Token Registry definiert werden oder als URI definiert werden, der einen kollisionsresistenten Namespace enthält.
Private Ansprüche : Hierbei handelt es sich um benutzerdefinierte Ansprüche, die erstellt werden, um Informationen zwischen Parteien auszutauschen, die sich auf deren Verwendung einigen.
Ein Beispiel für Nutzlast könnte sein:
{
"sub" : " 1234567890 " ,
"name" : " John Doe " ,
"admin" : true
}
Die Nutzlast wird dann Base64Url-codiert, um den zweiten Teil des JSON-Web-Tokens zu bilden.
Unterschrift
Um den Signaturteil zu erstellen, müssen Sie den codierten Header, die codierte Nutzlast, ein Geheimnis und den im Header angegebenen Algorithmus nehmen und diesen signieren.
Wenn Sie beispielsweise den HMAC SHA256-Algorithmus verwenden möchten, wird die Signatur wie folgt erstellt:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
Die Signatur wird verwendet, um zu überprüfen, ob der Absender des JWT derjenige ist, für den er sich ausgibt, und um sicherzustellen, dass die Nachricht unterwegs nicht geändert wurde. Alles zusammenfügen
Die Ausgabe besteht aus drei durch Punkte getrennten Base64-Strings, die problemlos in HTML- und HTTP-Umgebungen übergeben werden können und im Vergleich zu XML-basierten Standards wie SAML kompakter sind.
Das Folgende zeigt ein JWT, bei dem der vorherige Header und die Nutzdaten codiert sind und das mit einem Geheimnis signiert ist. Verschlüsseltes JWT
Wenn sich der Benutzer bei der Authentifizierung erfolgreich mit seinen Anmeldeinformationen anmeldet, wird ein JSON-Web-Token zurückgegeben und muss lokal gespeichert werden (normalerweise im lokalen Speicher, es können jedoch auch Cookies verwendet werden), anstatt wie beim herkömmlichen Ansatz eine Sitzung im zu erstellen Server und gibt ein Cookie zurück.
Immer wenn der Benutzer auf eine geschützte Route oder Ressource zugreifen möchte, sollte der Benutzeragent das JWT senden, normalerweise im Authorization-Header unter Verwendung des Bearer-Schemas. Der Inhalt des Headers sollte wie folgt aussehen:
Authorization: Bearer <token>
Dies ist ein zustandsloser Authentifizierungsmechanismus, da der Benutzerstatus niemals im Serverspeicher gespeichert wird. Die geschützten Routen des Servers prüfen, ob im Autorisierungsheader ein gültiges JWT vorhanden ist, und wenn es vorhanden ist, wird dem Benutzer der Zugriff auf geschützte Ressourcen gestattet. Da JWTs in sich geschlossen sind, sind alle notwendigen Informationen vorhanden, sodass die Datenbank nicht mehr mehrmals abgefragt werden muss.
Dadurch können Sie sich vollständig auf zustandslose Daten-APIs verlassen und sogar Anfragen an nachgelagerte Dienste stellen. Es spielt keine Rolle, welche Domänen Ihre APIs bereitstellen, daher stellt Cross-Origin Resource Sharing (CORS) kein Problem dar, da keine Cookies verwendet werden.
Das folgende Diagramm zeigt diesen Vorgang:
Tokenbasierte Authentifizierungsschemata erfreuen sich in jüngster Zeit großer Beliebtheit, da sie im Vergleich zu Sitzungen/Cookies wichtige Vorteile bieten:
Bei diesem Ansatz müssen einige Kompromisse eingegangen werden:
Der JWT-Authentifizierungsablauf ist sehr einfach
Es ist wichtig zu beachten, dass Autorisierungsansprüche im Zugriffstoken enthalten sind. Warum ist das wichtig? Nehmen wir an, dass Autorisierungsansprüche (z. B. Benutzerrechte in der Datenbank) während der Lebensdauer des Zugriffstokens geändert werden. Diese Änderungen werden erst wirksam, wenn ein neues Zugriffstoken ausgestellt wird. In den meisten Fällen stellt dies kein großes Problem dar, da Zugriffstoken nur von kurzer Dauer sind. Andernfalls verwenden Sie das undurchsichtige Token-Muster.
Sehen wir uns an, wie wir die JWT-Token-basierte Authentifizierung mit Java und Spring implementieren und gleichzeitig versuchen können, das Standardverhalten der Spring-Sicherheit wiederzuverwenden, wo immer es möglich ist. Das Spring Security-Framework enthält Plug-in-Klassen, die sich bereits mit Autorisierungsmechanismen wie Sitzungscookies, HTTP Basic und HTTP Digest befassen. Dennoch fehlt es an nativer Unterstützung für JWT, und wir müssen uns die Hände schmutzig machen, damit es funktioniert.
Diese Demo verwendet derzeit eine H2-Datenbank namens test_db, sodass Sie sie ohne große Konfiguration schnell und sofort ausführen können. Wenn Sie eine Verbindung zu einer anderen Datenbank herstellen möchten, müssen Sie die Verbindung in der Datei application.yml
im Ressourcenverzeichnis angeben. Beachten Sie, dass hibernate.hbm2ddl.auto=create-drop
bei jeder Bereitstellung eine saubere Datenbank löscht und erstellt (möglicherweise möchten Sie dies ändern, wenn Sie dies in einem echten Projekt verwenden). Hier ist das Beispiel aus dem Projekt. Sehen Sie, wie einfach Sie Kommentare zu den url
und dialect
austauschen können, um Ihre eigene MySQL-Datenbank zu verwenden:
spring :
datasource :
url : jdbc:h2:mem:test_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
# url: jdbc:mysql://localhost:3306/user_db
username : root
password : root
tomcat :
max-wait : 20000
max-active : 50
max-idle : 20
min-idle : 15
jpa :
hibernate :
ddl-auto : create-drop
properties :
hibernate :
dialect : org.hibernate.dialect.H2Dialect
# dialect: org.hibernate.dialect.MySQL8Dialect
format_sql : true
id :
new_generator_mappings : false
JwtTokenFilter
JwtTokenFilterConfigurer
JwtTokenProvider
MyUserDetails
WebSecurityConfig
JwtTokenFilter
Der JwtTokenFilter
-Filter wird auf jede API ( /**
) angewendet, mit Ausnahme des Anmeldetoken-Endpunkts ( /users/signin
) und des Singup-Endpunkts ( /users/signup
).
Dieser Filter hat folgende Aufgaben:
JwtTokenProvider
andernfalls lösen Sie eine Authentifizierungsausnahme aus Stellen Sie sicher, dass chain.doFilter(request, response)
nach erfolgreicher Authentifizierung aufgerufen wird. Sie möchten, dass die Verarbeitung der Anforderung zum nächsten Filter übergeht, da der allerletzte Filter FilterSecurityInterceptor#doFilter dafür verantwortlich ist, tatsächlich die Methode in Ihrem Controller aufzurufen, die die angeforderte API-Ressource verarbeitet.
String token = jwtTokenProvider . resolveToken (( HttpServletRequest ) req );
if ( token != null && jwtTokenProvider . validateToken ( token )) {
Authentication auth = jwtTokenProvider . getAuthentication ( token );
SecurityContextHolder . getContext (). setAuthentication ( auth );
}
filterChain . doFilter ( req , res );
JwtTokenFilterConfigurer
Fügt den JwtTokenFilter
zur DefaultSecurityFilterChain
der Spring Boot-Sicherheit hinzu.
JwtTokenFilter customFilter = new JwtTokenFilter ( jwtTokenProvider );
http . addFilterBefore ( customFilter , UsernamePasswordAuthenticationFilter . class );
JwtTokenProvider
Der JwtTokenProvider
hat die folgenden Verantwortlichkeiten:
MeineBenutzerdetails
Implementiert UserDetailsService
, um unsere eigene benutzerdefinierte Funktion „loadUserbyUsername“ zu definieren. Die UserDetailsService
Schnittstelle wird zum Abrufen benutzerbezogener Daten verwendet. Es verfügt über eine Methode namens „loadUserByUsername“ , die eine Benutzerentität basierend auf dem Benutzernamen findet und überschrieben werden kann, um den Prozess der Benutzersuche anzupassen.
Es wird vom DaoAuthenticationProvider
verwendet, um während der Authentifizierung Details über den Benutzer zu laden.
WebSecurityConfig
Die WebSecurityConfig
-Klasse erweitert WebSecurityConfigurerAdapter
um eine benutzerdefinierte Sicherheitskonfiguration bereitzustellen.
Folgende Beans werden in dieser Klasse konfiguriert und instanziiert:
JwtTokenFilter
PasswordEncoder
Außerdem konfigurieren wir innerhalb WebSecurityConfig#configure(HttpSecurity http)
-Methode Muster, um geschützte/ungeschützte API-Endpunkte zu definieren. Bitte beachten Sie, dass wir den CSRF-Schutz deaktiviert haben, da wir keine Cookies verwenden.
// Disable CSRF (cross site request forgery)
http . csrf (). disable ();
// No session will be created or used by spring security
http . sessionManagement (). sessionCreationPolicy ( SessionCreationPolicy . STATELESS );
// Entry points
http . authorizeRequests () //
. antMatchers ( "/users/signin" ). permitAll () //
. antMatchers ( "/users/signup" ). permitAll () //
// Disallow everything else..
. anyRequest (). authenticated ();
// If a user try to access a resource without having enough permissions
http . exceptionHandling (). accessDeniedPage ( "/login" );
// Apply JWT
http . apply ( new JwtTokenFilterConfigurer ( jwtTokenProvider ));
// Optional, if you want to test the API from a browser
// http.httpBasic();
Stellen Sie sicher, dass Sie Java 8 und Maven installiert haben
Forken Sie dieses Repository und klonen Sie es
$ git clone https://github.com/<your-user>/spring-boot-jwt
$ cd spring-boot-jwt
$ mvn install
$ mvn spring-boot:run
http://localhost:8080/swagger-ui.html
um zu überprüfen, ob alles ordnungsgemäß funktioniert. Sie können den Standardport in der Datei application.yml
ändern server :
port : 8080
/users/me
um zu überprüfen, ob Sie nicht authentifiziert sind. Sie sollten eine Antwort mit einer 403
mit der Meldung Access Denied
erhalten, da Sie Ihr gültiges JWT-Token noch nicht festgelegt haben $ curl -X GET http://localhost:8080/users/me
/users/signin
mit dem Standardadministratorbenutzer, den wir programmgesteuert erstellt haben, um ein gültiges JWT-Token zu erhalten $ curl -X POST 'http://localhost:8080/users/signin?username=admin&password=admin'
/users/me
$ curl -X GET http://localhost:8080/users/me -H 'Authorization: Bearer <JWT_TOKEN>'
{
"id" : 1 ,
"username" : "admin" ,
"email" : "[email protected]" ,
"roles" : [
"ROLE_ADMIN"
]
}