A. A.
Eintrag
Die JWT-Authentifizierung (JSON Web Token) ist ein ziemlich einheitlicher, konsistenter Authentifizierungs- und Authentifizierungsmechanismus zwischen Server und Clients. Der Vorteil von JWT besteht darin, dass wir den Zustand weniger verwalten und gut skalieren können. Es ist nicht überraschend, dass die Autorisierung und Authentifizierung mit ihrer Hilfe zunehmend in modernen Webanwendungen verwendet wird.
Bei der Entwicklung von Anwendungen mit JWT stellt sich häufig die Frage: Wo und wie wird empfohlen, das Token zu speichern? Wenn wir eine Webanwendung entwickeln, haben wir zwei der häufigsten Optionen:
- HTML5-Webspeicher (localStorage oder sessionStorage)
- Cookies
Wenn wir diese Methoden vergleichen, können wir sagen, dass beide Werte im Browser des Clients speichern, beide recht einfach zu verwenden sind und eine gemeinsame Speicherung von Schlüssel-Wert-Paaren darstellen. Der Unterschied liegt in der Speicherumgebung.
Auf den Webspeicher (localStorage / sessionStorage) kann über JavaScript in derselben Domäne zugegriffen werden. Dies bedeutet, dass jeder JavaScript-Code in Ihrer Anwendung Zugriff auf Web Storage hat. Dies führt zu einer Sicherheitsanfälligkeit für XSS-Angriffe (Cross-Site Scripting). Als Speicher-Engine bietet Web Storage keine Möglichkeit, Ihre Daten während der Speicherung und Freigabe zu sichern. Wir können es nur für Zusatzdaten verwenden, die wir beim Aktualisieren (F5) oder Schließen der Registerkarte behalten möchten: Status und Seitenzahl, Filter usw.
Token können auch über Browser-Cookies übertragen werden. Mit dem
httpOnly- Flag verwendete Cookies
sind von XSS nicht betroffen. httpOnly ist ein Flag für den Zugriff auf das Lesen, Schreiben und Löschen von Cookies nur auf dem Server. Auf sie kann auf dem Client nicht über JavaScript zugegriffen werden, sodass der Client nichts über das Token weiß und die Autorisierung auf der Serverseite vollständig verarbeitet wird.
Wir können auch ein
sicheres Flag setzen, um sicherzustellen, dass Cookies nur über HTTPS übertragen werden. Angesichts dieser Vorteile fiel meine Wahl auf Cookies.
Dieser Artikel beschreibt einen Ansatz zum Implementieren der Authentifizierung und Authentifizierung mithilfe von httpOnly Secure Cookies + JSON Web Token in ASP.NET Core Web Api in Verbindung mit SPA. Es wird die Option berücksichtigt, bei der Server und Client unterschiedlichen Ursprungs sind.
Einrichten Ihrer lokalen Entwicklungsumgebung
Um Client-Server-Beziehungen über HTTPS korrekt zu konfigurieren und zu debuggen, empfehle ich dringend, die lokale Entwicklungsumgebung sofort so zu konfigurieren, dass sowohl der Client als auch der Server über eine HTTPS-Verbindung verfügen.
Wenn Sie dies nicht sofort tun und versuchen, Beziehungen ohne HTTPS-Verbindung aufzubauen, werden in Zukunft viele Details angezeigt, ohne die sichere Cookies und zusätzliche Sicherheitsrichtlinien in der Produktion mit HTTPS nicht ordnungsgemäß funktionieren.
Ich werde ein Beispiel für die Konfiguration von HTTPS unter Windows 10, Server - ASP.NET Core, SPA - React zeigen.
Sie können HTTPS in ASP.NET Core mithilfe des Flags "
Für HTTPS konfigurieren" beim Erstellen eines Projekts
konfigurieren oder, falls dies beim Erstellen nicht der Fall war, die entsprechende Option in den Eigenschaften aktivieren.
Um SPA zu konfigurieren, müssen Sie das Skript auf
"Start" ändern und auf
"HTTPS = true setzen" setzen . Mein Setup ist wie folgt:
'start': 'set HTTPS=true&&rimraf ./build&&react-scripts start'
Ich empfehle Ihnen, HTTPS für die Entwicklungsumgebung in anderen Umgebungen unter
create-react-app.dev/docs/using-https-in-development einzurichtenKonfigurieren Sie ASP.NET Core Server
JWT-Setup
In diesem Fall die häufigste Implementierung von JWT aus der Dokumentation oder einem Artikel mit zusätzlichen Einstellungsoptionen.RequireHttpsMetadata
options.RequireHttpsMetadata = true;
Da unsere Entwicklungsumgebung HTTPS verwendet:
ConfigureServices services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { options.RequireHttpsMetadata = true; options.SaveToken = true; options.TokenValidationParameters = new TokenValidationParameters {
Konfigurieren einer CORS-Richtlinie
Wichtig : Die CORS-Richtlinie muss
AllowCredentials()
. Dies ist erforderlich, um eine Anforderung von
XMLHttpRequest.withCredentials zu erhalten und Cookies an den Client zurückzusenden. Mehr dazu wird später geschrieben. Andere Optionen werden abhängig von den Anforderungen des Projekts konfiguriert.
Wenn sich der Server und der Client auf demselben Ursprung befinden, wird die gesamte unten stehende Konfiguration nicht benötigt.
ConfigureServices services.AddCors();
Konfigurieren app.UseCors(x => x .WithOrigins("https://localhost:3000")
Festlegen der Cookie-Richtlinie
Erzwinge die Cookie-Richtlinie auf httpOnly und sicher.
Wenn möglich, setzen Sie
MinimumSameSitePolicy = SameSiteMode.Strict;
- Dies verbessert die Cookie-Sicherheit für Anwendungstypen, die nicht auf der Verarbeitung von Ursprungsanfragen beruhen.
Konfigurieren app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Strict, HttpOnly = HttpOnlyPolicy.Always, Secure = CookieSecurePolicy.Always });
Die Idee eines sicheren Token-Austauschs
Dieser Teil ist ein Konzept. Wir werden zwei Dinge tun:
- Werfen Sie ein Token mithilfe von httpOnly und sicheren Flags in eine HTTP-Anforderung.
- Empfangen und validieren Sie Clientanwendungstoken von einer HTTP-Anforderung.
Dazu brauchen wir:
- Schreiben Sie das Token beim Anmelden in das httpOnly-Cookie und löschen Sie es beim Anmelden von dort.
- Wenn Cookies ein Token enthalten, ersetzen Sie das Token im HTTP-Header jeder nachfolgenden Anforderung.
- Wenn Cookies kein Token enthalten, ist der Header leer und die Anforderung wird nicht autorisiert.
Middleware
Die Hauptidee besteht darin, benutzerdefinierte Middleware zu implementieren, um ein Token in eine eingehende HTTP-Anforderung einzufügen. Nach der Benutzerautorisierung speichern wir das Cookie unter einem bestimmten Schlüssel, zum Beispiel:
".AspNetCore.Application.Id" . Ich empfehle, einen Namen festzulegen, der nichts mit Autorisierung oder Token zu tun hat. In diesem Fall sieht ein Cookie mit einem Token wie eine
unauffällige Systemkonstante für die AspNetCore-Anwendung aus. Es besteht also eine höhere Wahrscheinlichkeit, dass der Angreifer viele Systemvariablen sieht und, ohne zu verstehen, welcher Autorisierungsmechanismus verwendet wird, weiter geht. Natürlich, wenn er diesen Artikel nicht liest und nicht speziell auf eine solche Konstante achtet.
Als nächstes müssen wir dieses Token in alle nachfolgenden eingehenden HTTP-Anforderungen einfügen. Dazu schreiben wir einige Zeilen Middleware-Code. Dies ist nichts anderes als eine HTTP-Pipeline.
Konfigurieren app.Use(async (context, next) => { var token = context.Request.Cookies[".AspNetCore.Application.Id"]; if (!string.IsNullOrEmpty(token)) context.Request.Headers.Add("Authorization", "Bearer " + token); await next(); }); app.UseAuthentication();
Wir können diese Logik auf einen separaten Middleware-Dienst übertragen, um Startup.cs nicht zu verstopfen. Die Idee wird sich nicht ändern.
Um den Wert in Cookies zu schreiben, müssen wir der Autorisierungslogik nur die folgende Zeile hinzufügen:
if (result.Succeeded) HttpContext.Response.Cookies.Append(".AspNetCore.Application.Id", token, new CookieOptions { MaxAge = TimeSpan.FromMinutes(60) });
Unter Verwendung unserer Cookie-Richtlinien werden diese Cookies automatisch als httpOnly und sicher gesendet. Sie müssen ihre Richtlinien in den Cookie-Optionen nicht neu definieren.
In CookieOptions können Sie MaxAge so einstellen, dass eine Lebensdauer angegeben wird. Es ist nützlich, bei der Ausgabe des Tokens zusammen mit JWT Lifetime anzugeben, damit das Cookie nach einer Weile verschwindet. Andere Eigenschaften von CookieOptions werden abhängig von den Anforderungen des Projekts konfiguriert.
Aus Sicherheitsgründen empfehle ich, Middleware die folgenden Header hinzuzufügen:
context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); context.Response.Headers.Add("X-Xss-Protection", "1"); context.Response.Headers.Add("X-Frame-Options", "DENY");
- Der Header X-Content-Type-Options wird zum Schutz vor MIME-Sniffing-Schwachstellen verwendet. Diese Sicherheitsanfälligkeit kann auftreten, wenn Benutzer auf einer Website Inhalte herunterladen können, der Benutzer jedoch einen bestimmten Dateityp als etwas anderes tarnt. Dies kann Angreifern die Möglichkeit geben, standortübergreifende Skriptskripte auszuführen oder eine Website zu gefährden.
- Alle modernen Browser verfügen über integrierte XSS-Filterfunktionen, die versuchen, XSS-Schwachstellen zu erkennen, bevor die Seite uns vollständig angezeigt wird. Standardmäßig sind sie im Browser aktiviert, aber der Benutzer ist möglicherweise schwieriger und deaktiviert sie. Mithilfe des X-XSS-Protection- Headers können wir den Browser anweisen, die Aktionen des Benutzers zu ignorieren und den integrierten Filter anzuwenden.
- X-Frame-Options teilt dem Browser mit, dass nichts angezeigt wird, wenn Ihre Site in einem HTML-Frame platziert ist. Dies ist sehr wichtig, wenn Sie versuchen, sich vor Clickjacking-Versuchen zu schützen.
Ich habe weit entfernt von allen Schlagzeilen beschrieben. Es gibt viel mehr Möglichkeiten, um die Sicherheit von Webanwendungen zu erhöhen. Ich empfehle Ihnen, sich auf die Sicherheitscheckliste aus der Ressource securityheaders.com zu konzentrieren.
SPA-Client-Setup
Wenn sich Client und Server an einem anderen Ursprung befinden, ist auf dem Client auch eine zusätzliche Konfiguration erforderlich. Sie müssen jede Anforderung mit
XMLHttpRequest.withCredentials umschließen .
Ich habe meine Methoden wie folgt verpackt:
import axios from "axios"; const api = axios.create({ baseURL: process.env.REACT_APP_API_URL }); api.interceptors.request.use(request => requestInterceptor(request)) const requestInterceptor = (request) => { request.withCredentials = true; return request; } export default api;
Wir können unsere Anforderungskonfiguration auf jede Art und Weise
umbrechen. Hauptsache,
withCredentials = true ist da .
Die Eigenschaft
XMLHttpRequest.withCredentials bestimmt, ob domänenübergreifende Anforderungen mithilfe von Anmeldeinformationen wie Cookies, Autorisierungsheadern oder TLS-Zertifikaten generiert werden sollen.
Dieses Flag wird auch verwendet, um zu bestimmen, ob in der Antwort gesendete Cookies ignoriert werden. XMLHttpRequest von einer anderen Domäne kann kein Cookie für eine eigene Domäne setzen, wenn das withCredentials-Flag vor dem Erstellen dieser Anforderung nicht auf true gesetzt ist.
Mit anderen Worten, wenn Sie dieses Attribut nicht angeben, wird unser Cookie nicht vom Browser gespeichert, d. H. Wir können das Cookie nicht an den Server zurücksenden, und der Server findet das gewünschte Cookie mit JWT nicht und signiert das Bearer Token nicht in unserer HTTP-Pipeline.
Wofür ist das alles?
Oben habe ich eine XSS-resistente Methode zum Austausch von Token beschrieben. Lassen Sie uns das Ergebnis der implementierten Funktionalität betrachten.
Wenn Sie in die Entwicklertools gehen, sehen wir die
begehrten Flags
nur und
sicher :

Lassen Sie uns einen Crush-Test durchführen und versuchen, die Cookies aus dem Client herauszuholen:

Wir beobachten '', d.h. Auf Cookies kann nicht über den Dokumentbereich zugegriffen werden, sodass sie nicht mit Skripten gelesen werden können.
Wir können versuchen, diese Cookies mithilfe zusätzlicher Tools oder Erweiterungen abzurufen, aber alle Tools, die ich ausprobiert habe, haben die native Implementierung aus dem Dokumentbereich aufgerufen.
Demo-Projekt
Startanweisungen finden Sie in README.MD
UPD: Schutz gegen CSRF
Konfigurieren Sie ASP.NET Core Server
Middleware-DiensteXsrfProtectionMiddleware.cs public class XsrfProtectionMiddleware { private readonly IAntiforgery _antiforgery; private readonly RequestDelegate _next; public XsrfProtectionMiddleware(RequestDelegate next, IAntiforgery antiforgery) { _next = next; _antiforgery = antiforgery; } public async Task InvokeAsync(HttpContext context) { context.Response.Cookies.Append( ".AspNetCore.Xsrf", _antiforgery.GetAndStoreTokens(context).RequestToken, new CookieOptions {HttpOnly = false, Secure = true, MaxAge = TimeSpan.FromMinutes(60)}); await _next(context); } }
MiddlewareExtensions.cs public static class MiddlewareExtensions { public static IApplicationBuilder UseXsrfProtection(this IApplicationBuilder builder, IAntiforgery antiforgery) => builder.UseMiddleware<XsrfProtectionMiddleware>(antiforgery); }
ConfigureServices services.AddAntiforgery(options => { options.HeaderName = "x-xsrf-token"; }); services.AddMvc();
Konfigurieren app.UseAuthentication(); app.UseXsrfProtection(antiforgery);
SPA-Einrichtung
api.axios.js import axios from "axios"; import cookie from 'react-cookies'; const api = axios.create({ baseURL: process.env.REACT_APP_API_URL }); api.interceptors.request.use(request => requestInterceptor(request)) const requestInterceptor = (request) => { request.headers['x-xsrf-token'] = cookie.load('.AspNetCore.Xsrf') return request; } export default api;
Verwenden Sie
Um unsere API-Methoden zu schützen, müssen Sie das Attribut
[AutoValidateAntiforgeryToken]
für den Controller oder
[ValidateAntiForgeryToken]
für die Methode hinzufügen.