Het eerste artikel introduceerde TsgcHTTPRESTServer en de afhandeling van requests. Dit artikel behandelt de drie begeleidende componenten die van een werkend endpoint iets maken dat u echt aan klanten kunt voorzetten: een user store, multi-tenancy, en de metrics waar het operations-team op dag één om zal vragen.
Alle drie zijn aparte componenten. U maakt aan wat u nodig hebt en wijst het toe aan de server; alles wat u niet toewijst kost één pointertest per request.
Een user store in plaats van een TStringList
TsgcHTTPServer_Users bewaart accounts met gesalte, geïtereerde wachtwoordhashes. Het belangrijkste is hoe weinig bedrading ervoor nodig is: wijs de component toe aan de authenticatie-opties van de server en de HTTP Basic-poort zoekt de credentials er vanzelf tegen op.
uses
sgcHTTP_REST_Server, sgcHTTP_REST_Server_Users;
FUsers := TsgcHTTPServer_Users.Create(self);
FServer := TsgcHTTPRESTServer.Create(self);
FServer.Authentication.Users := FUsers;
Dat is alles. U hebt geen OnAuthentication-handler nodig om Basic-authenticatie te laten werken, de poort doet de lookup en de hashvergelijking.
Accounts voegt u toe met AddUser, dat de gegenereerde gebruikers-Id teruggeeft, of een lege string wanneer de gebruikersnaam leeg is of al bezet:
FUsers.AddUser('alice', 'secret123', 'admin,reader');
FUsers.AddUser('bob', 'secret456', 'reader');
De rest van het oppervlak is wat u zou verwachten, en elke lookup is thread-safe:
if FUsers.ValidateCredentials('alice', 'secret123') then
...
FUsers.SetPassword('bob', 'newsecret');
FUsers.EnableUser('bob', False);
FUsers.DeleteUser('bob');
Rollen
Rollen zijn een door komma's gescheiden lijst op het account, met helpers om ze te lezen en te testen. Een rolcontrole in een handler is één aanroep:
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
Wachtwoorden worden standaard gehasht met SHA-512 over 10000 iteraties, waarbij elk account zijn eigen willekeurige salt draagt. Die salt per account zorgt er al voor dat twee identieke wachtwoorden verschillend hashen, dus hier hoeft u zelden aan te komen:
FUsers.Hashing.Algorithm := uhaSHA512;
FUsers.Hashing.Iterations := 10000;
De eigenschap Salt is iets anders en verdient het om aandachtig gelezen te worden. Het is een optionele pepper die bovenop de salt per account in elke hash wordt gemengd, en hij is met opzet standaard leeg: een waarde die bij de constructie verzonnen wordt zou bij de volgende run anders zijn, en elk correct wachtwoord zou dan geweigerd worden zonder foutmelding die dat verklaart. Stel hem alleen in wanneer hij ergens vandaan kan komen waar de user store zelf niet is, bijvoorbeeld een omgevingsvariabele of een key vault. Een pepper die naast de hashes ligt die hij beschermt voegt niets toe. Hem wijzigen maakt elk reeds opgeslagen wachtwoord ongeldig.
Persistentie
De store zit standaard in het geheugen. Wijs hem naar een bestand en hij overleeft een herstart:
FUsers.Storage.StorageType := ustFile;
FUsers.Storage.FileName := 'sgcRESTUsers.dat';
FUsers.Storage.EncryptAtRest := True;
FUsers.Storage.EncryptionKey := GetKeyFromEnvironment;
FUsers.Storage.AutoSaveSeconds := 30;
FUsers.LoadUsers;
Met AutoSaveSeconds ingesteld schrijft de store zichzelf periodiek weg; anders roept u SaveUsers of SaveToFile aan wanneer het u uitkomt. Voor een store die al in uw eigen database leeft zet u StorageType op ustCustom en beantwoordt u in plaats daarvan de events, waarbij OnValidateCredentials doorslaggevend wordt:
procedure TForm1.UsersValidateCredentials(Sender: TObject;
const aUsername, aPassword: string; var Valid: Boolean);
begin
Valid := MyDatabase.CheckLogin(aUsername, aPassword);
end;
Eén detail dat van belang is als u een beheerscherm bouwt: GetUserByIndex maakt PasswordHash en Salt leeg voordat de record wordt teruggegeven, zodat een opsommingsroute zelfs per ongeluk geen credential kan lekken. FindUser doet dit niet. Die geeft de record precies terug zoals hij is opgeslagen, inclusief de credential-velden, omdat dit de opzoeking is die de authenticatiepoort zelf gebruikt. Lees daaruit de velden die u nodig hebt en serialiseer nooit de volledige record naar een response body.
Multi-tenancy
TsgcHTTPServer_Tenancy beantwoordt per request één vraag: voor welke klant is dit? De component bepaalt een tenant-string voordat uw handler draait, en de server stelt die beschikbaar via de alleen-lezen eigenschap Tenant.
FTenancy := TsgcHTTPServer_Tenancy.Create(self);
FTenancy.Resolution := trHeader;
FTenancy.HeaderName := 'X-Tenant-Id';
FTenancy.DefaultTenant := 'public';
FServer.Tenancy := FTenancy;
Er zijn vijf resolutiemodi:
| Resolution | Bron | Ingesteld met |
|---|---|---|
trNone | uit, Tenant is altijd leeg | — |
trHost | de hostnaam | HostSuffix |
trPath | een segment van het requestpad | PathSegmentIndex |
trHeader | een request-header | HeaderName |
trJWTClaim | een claim van het bearer-token | ClaimName |
Met trHost en HostSuffix ingesteld op .example.com resulteert een request naar acme.example.com in acme. Met trPath en een PathSegmentIndex van 0 levert /acme/api/orders hetzelfde op. Wanneer de ingestelde bron niets oplevert, wordt DefaultTenant gebruikt.
Het uitlezen in een handler is gewoon een eigenschap:
procedure TForm1.ServerCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
AResponseInfo.ResponseNo := 200;
AResponseInfo.ContentType := 'application/json';
AResponseInfo.ContentText := '{"tenant":"' + FServer.Tenant + '"}';
end;
Tenant is geldig binnen OnBeforeCommand, OnCommandGet en OnCommandOther, en is thread-lokaal, zodat een drukke server die veel tenants tegelijk bedient ze nooit door elkaar haalt.
Over trJWTClaim moet één ding duidelijk zijn. De claim wordt uit de payload van het token gelezen zonder de handtekening te valideren, want de tenant is alleen een routeringshint. De handtekening wordt nog steeds gecontroleerd door de JWT-authenticatie wanneer u die inschakelt. Beschouw de tenant op zichzelf niet als identiteitsbewijs.
Past geen van de vijf modi, dan geeft OnResolveTenant u alle bronnen tegelijk en laat u beslissen:
procedure TForm1.TenancyResolveTenant(Sender: TObject;
const aHost, aPath, aHeaderValue, aJWTPayload: string; var aTenant: string);
begin
aTenant := LookupTenantForHost(aHost);
end;
Metrics en health
TsgcHTTPServerStats telt wat de server gedaan heeft en kan dat publiceren op twee endpoints. Beide staan uit totdat u ze afzonderlijk inschakelt, dus er wordt niets bereikbaar alleen omdat u de component op het formulier hebt gezet.
FStats := TsgcHTTPServerStats.Create(self);
FStats.Endpoints.Metrics.Enabled := True;
FStats.Endpoints.Health.Enabled := True;
FServer.ServerStats := FStats;
De tellers zijn ook vanuit code uit te lezen, wat handig is voor een intern statusscherm:
lblRequests.Caption := IntToStr(FStats.TotalRequests);
lblErrors.Caption := IntToStr(FStats.Status5xx);
lblLatency.Caption := IntToStr(FStats.LatencyAvgMs) + ' ms';
lblUptime.Caption := IntToStr(FStats.UptimeSeconds) + ' s';
/metrics antwoordt rechtstreeks in het Prometheus text exposition-formaat, zonder exporter ertussen. Tellers per endpoint zitten erbij, met de cardinaliteit begrensd op 256 verschillende paden zodat een route met een id erin het aantal reeksen niet kan laten ontploffen. Alles voorbij die grens wordt ondergebracht onder other.
# 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
Als de componenten voor firewall, rate limiter, circuit breaker of API-sleutelbeheer gekoppeld zijn, worden hun eigen metrics aan dezelfde uitvoer toegevoegd:
FStats.RateLimiter := FRateLimiter;
FStats.Firewall := FFirewall;
/health antwoordt met een compact JSON-document. De status ervan leest ok, of degraded wanneer er een circuit breaker gekoppeld is met open breakers, waardoor het direct bruikbaar is als probe voor een load balancer.
Tot slot wordt OnStats afgevuurd met het volledige stats-object als u de cijfers liever zelf ergens naartoe stuurt:
procedure TForm1.StatsEvent(Sender: TObject;
const aStats: TsgcHTTPServerStats);
begin
MyTelemetry.Send(aStats.TotalRequests, aStats.LatencyAvgMs);
end;
Alles bij elkaar
Een server met alle drie gekoppeld is ongeveer een dozijn regels, en elke component blijft onafhankelijk van de andere:
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;
Het volgende artikel plaatst een OpenAPI-contract voor deze server, zodat routering, validatie en documentatie allemaal uit de spec komen.
Download de nieuwste build van de sgcWebSockets-downloadpagina.
