Il primo articolo ha presentato TsgcHTTPRESTServer e la sua gestione delle richieste. Questo tratta i tre componenti complementari che trasformano un endpoint funzionante in qualcosa che puoi davvero mettere davanti ai clienti: un archivio utenti, la multi-tenancy e le metriche che il team operativo chiederà fin dal primo giorno.
Tutti e tre sono componenti separati. Crei quelli che ti servono e li assegni al server, e tutto ciò che lasci non assegnato costa un singolo test di puntatore per richiesta.
Un archivio utenti al posto di una TStringList
TsgcHTTPServer_Users conserva gli account con hash della password salati e iterati. La parte importante è quanto poco cablaggio richiede: lo assegni alle opzioni di autenticazione del server e il controllo HTTP Basic risolve le credenziali su di esso da solo.
uses
sgcHTTP_REST_Server, sgcHTTP_REST_Server_Users;
FUsers := TsgcHTTPServer_Users.Create(self);
FServer := TsgcHTTPRESTServer.Create(self);
FServer.Authentication.Users := FUsers;
È tutto qui. Non serve un handler OnAuthentication perché l'autenticazione Basic funzioni, il controllo esegue da sé la ricerca e il confronto degli hash.
Gli account si aggiungono con AddUser, che restituisce l'Id utente generato, oppure una stringa vuota quando il nome utente è vuoto o già in uso:
FUsers.AddUser('alice', 'secret123', 'admin,reader');
FUsers.AddUser('bob', 'secret456', 'reader');
Il resto dell'interfaccia è quello che ti aspetteresti, e ogni ricerca è thread safe:
if FUsers.ValidateCredentials('alice', 'secret123') then
...
FUsers.SetPassword('bob', 'newsecret');
FUsers.EnableUser('bob', False);
FUsers.DeleteUser('bob');
Ruoli
I ruoli sono un elenco separato da virgole sull'account, con metodi di supporto per leggerli e verificarli. Un controllo di ruolo in un gestore è una singola chiamata:
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
Le password vengono sottoposte a hash con SHA-512 su 10000 iterazioni di default, e ogni account porta con sé il proprio salt casuale. È proprio quel salt per account a far sì che due password identiche producano hash diversi, quindi raramente avrai bisogno di intervenire qui:
FUsers.Hashing.Algorithm := uhaSHA512;
FUsers.Hashing.Iterations := 10000;
La proprietà Salt è una cosa diversa e va letta con attenzione. È un pepper opzionale mescolato in ogni hash oltre al salt per account, ed è vuota di default per una ragione precisa: un valore inventato al momento della costruzione sarebbe diverso all'esecuzione successiva, e ogni password corretta verrebbe poi rifiutata senza alcun errore che lo spieghi. Impostala solo quando può arrivare da un posto che non sia l'archivio utenti stesso, per esempio una variabile d'ambiente o un vault di chiavi. Un pepper conservato accanto agli hash che protegge non aggiunge nulla. Cambiarlo invalida ogni password già memorizzata.
Persistenza
L'archivio è in memoria di default. Puntalo a un file e sopravvive a un riavvio:
FUsers.Storage.StorageType := ustFile;
FUsers.Storage.FileName := 'sgcRESTUsers.dat';
FUsers.Storage.EncryptAtRest := True;
FUsers.Storage.EncryptionKey := GetKeyFromEnvironment;
FUsers.Storage.AutoSaveSeconds := 30;
FUsers.LoadUsers;
Con AutoSaveSeconds impostato l'archivio si scrive periodicamente da solo; altrimenti chiama SaveUsers o SaveToFile quando ti fa comodo. Per un archivio che vive già nel tuo database, imposta StorageType a ustCustom e rispondi invece agli eventi, dove OnValidateCredentials diventa la fonte autorevole:
procedure TForm1.UsersValidateCredentials(Sender: TObject;
const aUsername, aPassword: string; var Valid: Boolean);
begin
Valid := MyDatabase.CheckLogin(aUsername, aPassword);
end;
Un dettaglio che conta se costruisci una schermata di amministrazione: GetUserByIndex azzera PasswordHash e Salt prima di restituire il record, così una rotta di enumerazione non può far trapelare una credenziale nemmeno per sbaglio. FindUser non lo fa. Restituisce il record esattamente com'è memorizzato, campi delle credenziali inclusi, perché è la ricerca che il controllo di autenticazione stesso usa. Leggi da esso i campi che ti servono, e non serializzare mai l'intero record nel corpo di una risposta.
Multi-tenancy
TsgcHTTPServer_Tenancy risponde a una domanda per richiesta: per quale cliente è? Risolve una stringa di tenant prima che il tuo gestore venga eseguito, e il server la espone sulla proprietà a sola lettura Tenant.
FTenancy := TsgcHTTPServer_Tenancy.Create(self);
FTenancy.Resolution := trHeader;
FTenancy.HeaderName := 'X-Tenant-Id';
FTenancy.DefaultTenant := 'public';
FServer.Tenancy := FTenancy;
Ci sono cinque modalità di risoluzione:
| Risoluzione | Origine | Configurata con |
|---|---|---|
trNone | disattivata, Tenant è sempre vuota | — |
trHost | il nome host | HostSuffix |
trPath | un segmento del percorso della richiesta | PathSegmentIndex |
trHeader | un header della richiesta | HeaderName |
trJWTClaim | un claim del bearer token | ClaimName |
Con trHost e HostSuffix impostato a .example.com, una richiesta a acme.example.com si risolve in acme. Con trPath e PathSegmentIndex uguale a 0, /acme/api/orders si risolve allo stesso modo. Quando l'origine configurata non produce nulla, viene usato DefaultTenant.
Leggerla in un gestore è solo una proprietà:
procedure TForm1.ServerCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
AResponseInfo.ResponseNo := 200;
AResponseInfo.ContentType := 'application/json';
AResponseInfo.ContentText := '{"tenant":"' + FServer.Tenant + '"}';
end;
Tenant è valida dentro OnBeforeCommand, OnCommandGet e OnCommandOther, ed è locale al thread, quindi un server carico che serve molti tenant contemporaneamente non li confonde mai tra loro.
Su trJWTClaim c'è un punto da chiarire. Il claim viene letto dal payload del token senza validare la firma, perché il tenant è solo un suggerimento di instradamento. La firma viene comunque verificata dall'autenticazione JWT quando la abiliti. Non trattare il tenant come prova di identità in sé.
Se nessuna delle cinque modalità è adatta, OnResolveTenant ti consegna tutte le origini insieme e ti lascia decidere:
procedure TForm1.TenancyResolveTenant(Sender: TObject;
const aHost, aPath, aHeaderValue, aJWTPayload: string; var aTenant: string);
begin
aTenant := LookupTenantForHost(aHost);
end;
Metriche e health
TsgcHTTPServerStats conta quello che il server ha fatto e può pubblicarlo su due endpoint. Entrambi sono disattivati finché non li abiliti singolarmente, così nulla diventa raggiungibile solo perché hai rilasciato il componente sulla form.
FStats := TsgcHTTPServerStats.Create(self);
FStats.Endpoints.Metrics.Enabled := True;
FStats.Endpoints.Health.Enabled := True;
FServer.ServerStats := FStats;
I contatori sono leggibili anche da codice, il che è comodo per una schermata di stato interna:
lblRequests.Caption := IntToStr(FStats.TotalRequests);
lblErrors.Caption := IntToStr(FStats.Status5xx);
lblLatency.Caption := IntToStr(FStats.LatencyAvgMs) + ' ms';
lblUptime.Caption := IntToStr(FStats.UptimeSeconds) + ' s';
/metrics risponde direttamente nel formato di esposizione testuale di Prometheus, senza alcun exporter intermedio. Sono inclusi i contatori per endpoint, con la cardinalità limitata a 256 percorsi distinti così che una rotta contenente un id non possa far esplodere il numero di serie. Tutto ciò che supera il limite viene raccolto sotto 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
Se sono collegati i componenti firewall, rate limiter, circuit breaker o gestore delle API key, le loro metriche vengono aggiunte allo stesso output:
FStats.RateLimiter := FRateLimiter;
FStats.Firewall := FFirewall;
/health risponde con un documento JSON compatto. Il suo status vale ok, oppure degraded quando è collegato un circuit breaker che ha dei breaker aperti, il che lo rende direttamente utilizzabile come sonda di un load balancer.
Infine, OnStats scatta con l'intero oggetto delle statistiche se preferisci inviare tu stesso i numeri da qualche parte:
procedure TForm1.StatsEvent(Sender: TObject;
const aStats: TsgcHTTPServerStats);
begin
MyTelemetry.Send(aStats.TotalRequests, aStats.LatencyAvgMs);
end;
Mettendo tutto insieme
Un server con tutti e tre collegati è circa una dozzina di righe, e ogni componente resta indipendente dagli altri:
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;
Il prossimo articolo mette un contratto OpenAPI davanti a questo server, così che instradamento, validazione e documentazione arrivino tutti dalla specifica.
Scarica l'ultima build dalla pagina di download di sgcWebSockets.
