Der erste Artikel hat TsgcHTTPRESTServer und seine Request-Verarbeitung vorgestellt. Dieser hier behandelt die drei Begleitkomponenten, die aus einem funktionierenden Endpunkt etwas machen, das Sie tatsächlich vor Kunden stellen können: einen Benutzerspeicher, Mandantenfähigkeit und die Metriken, nach denen das Betriebsteam schon am ersten Tag fragen wird.
Alle drei sind eigenständige Komponenten. Sie erzeugen die, die Sie brauchen, und weisen sie dem Server zu; alles, was Sie nicht zuweisen, kostet einen einzigen Zeigervergleich pro Request.
Ein Benutzerspeicher statt einer TStringList
TsgcHTTPServer_Users verwaltet Konten mit gesalzenen, iterierten Passwort-Hashes. Das Wesentliche ist, wie wenig Verdrahtung nötig ist: Weisen Sie die Komponente den Authentifizierungsoptionen des Servers zu, und die HTTP-Basic-Prüfung löst Credentials von selbst dagegen auf.
uses
sgcHTTP_REST_Server, sgcHTTP_REST_Server_Users;
FUsers := TsgcHTTPServer_Users.Create(self);
FServer := TsgcHTTPRESTServer.Create(self);
FServer.Authentication.Users := FUsers;
Mehr ist es nicht. Sie brauchen keinen OnAuthentication-Handler, damit Basic-Authentifizierung funktioniert, die Prüfung übernimmt die Suche und den Hash-Vergleich.
Konten werden mit AddUser angelegt, was die erzeugte Benutzer-Id zurückgibt, oder eine leere Zeichenkette, wenn der Benutzername leer oder bereits vergeben ist:
FUsers.AddUser('alice', 'secret123', 'admin,reader');
FUsers.AddUser('bob', 'secret456', 'reader');
Der Rest der Schnittstelle ist das, was Sie erwarten würden, und jede Suche ist threadsicher:
if FUsers.ValidateCredentials('alice', 'secret123') then
...
FUsers.SetPassword('bob', 'newsecret');
FUsers.EnableUser('bob', False);
FUsers.DeleteUser('bob');
Rollen
Rollen sind eine kommagetrennte Liste am Konto, mit Hilfsmethoden zum Lesen und Prüfen. Eine Rollenprüfung in einem Handler ist ein einziger Aufruf:
FUsers.AddRole('bob', 'admin');
if not FUsers.UserHasRole(vUser, 'admin') then
begin
AResponseInfo.ResponseNo := 403;
AResponseInfo.ContentType := 'application/json';
AResponseInfo.ContentText := '{"error":"forbidden"}';
Exit;
end;
Hashing
Passwörter werden standardmäßig mit SHA-512 über 10000 Iterationen gehasht, wobei jedes Konto sein eigenes zufälliges Salt trägt. Genau dieses kontospezifische Salt sorgt bereits dafür, dass zwei identische Passwörter unterschiedlich hashen, Sie müssen hier also selten etwas anfassen:
FUsers.Hashing.Algorithm := uhaSHA512;
FUsers.Hashing.Iterations := 10000;
Die Eigenschaft Salt ist etwas anderes und lohnt genaues Lesen. Sie ist ein optionaler Pepper, der zusätzlich zum kontospezifischen Salt in jeden Hash einfließt, und sie ist mit Absicht standardmäßig leer: ein zur Konstruktionszeit erfundener Wert wäre beim nächsten Start ein anderer, und jedes korrekte Passwort würde dann ohne erklärende Fehlermeldung abgewiesen. Setzen Sie sie nur, wenn der Wert von woanders kommen kann als aus dem Benutzerspeicher selbst, etwa aus einer Umgebungsvariablen oder einem Key Vault. Ein Pepper, der neben den Hashes liegt, die er schützen soll, bringt nichts. Ihn zu ändern macht jedes bereits gespeicherte Passwort ungültig.
Persistenz
Der Speicher liegt standardmäßig im Arbeitsspeicher. Verweisen Sie ihn auf eine Datei, und er übersteht einen Neustart:
FUsers.Storage.StorageType := ustFile;
FUsers.Storage.FileName := 'sgcRESTUsers.dat';
FUsers.Storage.EncryptAtRest := True;
FUsers.Storage.EncryptionKey := GetKeyFromEnvironment;
FUsers.Storage.AutoSaveSeconds := 30;
FUsers.LoadUsers;
Ist AutoSaveSeconds gesetzt, schreibt sich der Speicher regelmäßig selbst weg; andernfalls rufen Sie SaveUsers oder SaveToFile auf, wann es Ihnen passt. Für einen Speicher, der bereits in Ihrer eigenen Datenbank liegt, setzen Sie StorageType auf ustCustom und beantworten stattdessen die Ereignisse, wobei OnValidateCredentials maßgeblich wird:
procedure TForm1.UsersValidateCredentials(Sender: TObject;
const aUsername, aPassword: string; var Valid: Boolean);
begin
Valid := MyDatabase.CheckLogin(aUsername, aPassword);
end;
Ein Detail, das zählt, wenn Sie eine Verwaltungsmaske bauen: GetUserByIndex leert PasswordHash und Salt, bevor die Methode den Datensatz zurückgibt, eine Auflistungsroute kann also nicht einmal versehentlich Zugangsdaten preisgeben. FindUser tut das nicht. Diese Methode gibt den Datensatz genau so zurück, wie er gespeichert ist, samt der Felder mit den Zugangsdaten, denn sie ist die Suche, die die Authentifizierungsprüfung selbst verwendet. Lesen Sie daraus die Felder, die Sie brauchen, und serialisieren Sie niemals den gesamten Datensatz in einen Antwort-Body.
Mandantenfähigkeit
TsgcHTTPServer_Tenancy beantwortet eine Frage pro Request: für welchen Kunden ist das? Es ermittelt eine Mandanten-Zeichenkette, bevor Ihr Handler läuft, und der Server stellt sie über die schreibgeschützte Eigenschaft Tenant bereit.
FTenancy := TsgcHTTPServer_Tenancy.Create(self);
FTenancy.Resolution := trHeader;
FTenancy.HeaderName := 'X-Tenant-Id';
FTenancy.DefaultTenant := 'public';
FServer.Tenancy := FTenancy;
Es gibt fünf Auflösungsmodi:
| Resolution | Quelle | Konfiguriert mit |
|---|---|---|
trNone | aus, Tenant ist immer leer | — |
trHost | der Hostname | HostSuffix |
trPath | ein Segment des Anfragepfads | PathSegmentIndex |
trHeader | ein Request-Header | HeaderName |
trJWTClaim | ein Claim des Bearer-Tokens | ClaimName |
Mit trHost und HostSuffix auf .example.com gesetzt löst eine Anfrage an acme.example.com zu acme auf. Mit trPath und PathSegmentIndex gleich 0 löst /acme/api/orders genauso auf. Liefert die konfigurierte Quelle nichts, wird DefaultTenant verwendet.
Das Auslesen in einem Handler ist einfach eine Eigenschaft:
procedure TForm1.ServerCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
AResponseInfo.ResponseNo := 200;
AResponseInfo.ContentType := 'application/json';
AResponseInfo.ContentText := '{"tenant":"' + FServer.Tenant + '"}';
end;
Tenant ist innerhalb von OnBeforeCommand, OnCommandGet und OnCommandOther gültig und ist threadlokal, ein ausgelasteter Server, der viele Mandanten gleichzeitig bedient, bringt sie also nie durcheinander.
Zu trJWTClaim gibt es eines klarzustellen. Der Claim wird aus dem Token-Payload gelesen, ohne die Signatur zu prüfen, denn der Mandant ist nur ein Routing-Hinweis. Die Signatur wird weiterhin von der JWT-Authentifizierung geprüft, wenn Sie diese aktivieren. Behandeln Sie den Mandanten nicht für sich allein als Identitätsnachweis.
Passt keiner der fünf Modi, übergibt Ihnen OnResolveTenant alle Quellen auf einmal und überlässt Ihnen die Entscheidung:
procedure TForm1.TenancyResolveTenant(Sender: TObject;
const aHost, aPath, aHeaderValue, aJWTPayload: string; var aTenant: string);
begin
aTenant := LookupTenantForHost(aHost);
end;
Metriken und Health
TsgcHTTPServerStats zählt, was der Server getan hat, und kann es auf zwei Endpunkten veröffentlichen. Beide sind aus, bis Sie sie einzeln aktivieren, es wird also nichts erreichbar, nur weil Sie die Komponente auf das Formular gezogen haben.
FStats := TsgcHTTPServerStats.Create(self);
FStats.Endpoints.Metrics.Enabled := True;
FStats.Endpoints.Health.Enabled := True;
FServer.ServerStats := FStats;
Die Zähler sind auch aus dem Code lesbar, was für eine interne Statusanzeige praktisch ist:
lblRequests.Caption := IntToStr(FStats.TotalRequests);
lblErrors.Caption := IntToStr(FStats.Status5xx);
lblLatency.Caption := IntToStr(FStats.LatencyAvgMs) + ' ms';
lblUptime.Caption := IntToStr(FStats.UptimeSeconds) + ' s';
/metrics antwortet direkt im Prometheus Text Exposition Format, ohne Exporter dazwischen. Zähler pro Endpunkt sind enthalten, wobei die Kardinalität auf 256 verschiedene Pfade begrenzt ist, damit eine Route mit einer Id darin die Zahl der Zeitreihen nicht sprengen kann. Alles jenseits der Grenze wird unter other zusammengefasst.
# HELP sgc_server_endpoint_requests_total Requests per endpoint
# TYPE sgc_server_endpoint_requests_total counter
sgc_server_endpoint_requests_total{endpoint="/api/orders"} 3120
sgc_server_endpoint_requests_total{endpoint="/api/users"} 845
Sind die Komponenten für Firewall, Rate Limiter, Circuit Breaker oder API-Key-Verwaltung angehängt, werden deren eigene Metriken derselben Ausgabe hinzugefügt:
FStats.RateLimiter := FRateLimiter;
FStats.Firewall := FFirewall;
/health antwortet mit einem kompakten JSON-Dokument. Sein status lautet ok, oder degraded, wenn ein Circuit Breaker angehängt ist und offene Breaker hat, was es direkt als Load-Balancer-Prüfung nutzbar macht.
Schließlich feuert OnStats mit dem gesamten Statistikobjekt, falls Sie die Zahlen lieber selbst irgendwohin schicken wollen:
procedure TForm1.StatsEvent(Sender: TObject;
const aStats: TsgcHTTPServerStats);
begin
MyTelemetry.Send(aStats.TotalRequests, aStats.LatencyAvgMs);
end;
Alles zusammengesetzt
Ein Server mit allen dreien angehängt sind etwa ein Dutzend Zeilen, und jede Komponente bleibt von den anderen unabhängig:
FStats := TsgcHTTPServerStats.Create(self);
FStats.Endpoints.Health.Enabled := True;
FStats.Endpoints.Metrics.Enabled := True;
FTenancy := TsgcHTTPServer_Tenancy.Create(self);
FTenancy.Resolution := trHeader;
FUsers := TsgcHTTPServer_Users.Create(self);
FUsers.Storage.StorageType := ustFile;
FUsers.Storage.FileName := 'users.dat';
FUsers.LoadUsers;
FServer := TsgcHTTPRESTServer.Create(self);
FServer.ServerStats := FStats;
FServer.Tenancy := FTenancy;
FServer.Authentication.Users := FUsers;
FServer.Authentication.Enabled := True;
FServer.Port := 5876;
FServer.Active := True;
Der nächste Artikel stellt diesem Server einen OpenAPI-Vertrag voran, sodass Routing, Validierung und Dokumentation alle aus der Spezifikation stammen.
Laden Sie den aktuellen Build von der sgcWebSockets Download-Seite herunter.
