Serwer REST: użytkownicy, wielodostępność i metryki | Blog eSeGeCe

Serwer REST: użytkownicy, wielodostępność i metryki

· Komponenty
sgcWebSockets REST server users, tenancy and metrics

Pierwszy artykuł przedstawił TsgcHTTPRESTServer i sposób obsługi żądań. Ten opisuje trzy komponenty towarzyszące, które zamieniają działający punkt końcowy w coś, co można naprawdę udostępnić klientom: magazyn użytkowników, wielodostępność oraz metryki, o które zespół utrzymania poprosi już pierwszego dnia.

Wszystkie trzy to osobne komponenty. Tworzysz te, których potrzebujesz, i przypisujesz je do serwera, a każdy pozostawiony bez przypisania kosztuje jedno sprawdzenie wskaźnika na żądanie.

Magazyn użytkowników zamiast TStringList

TsgcHTTPServer_Users przechowuje konta z solonymi, wielokrotnie iterowanymi skrótami haseł. Najważniejsze jest to, jak mało konfiguracji wymaga: przypisz go do opcji uwierzytelniania serwera, a bramka HTTP Basic sama sprawdzi w nim poświadczenia.

uses
  sgcHTTP_REST_Server, sgcHTTP_REST_Server_Users;

FUsers := TsgcHTTPServer_Users.Create(self);
FServer := TsgcHTTPRESTServer.Create(self);
FServer.Authentication.Users := FUsers;

To wszystko. Aby uwierzytelnianie Basic działało, nie potrzebujesz procedury obsługi OnAuthentication, bramka sama wykonuje wyszukiwanie i porównanie skrótu.

Konta dodaje się metodą AddUser, która zwraca wygenerowany identyfikator użytkownika albo pusty ciąg, gdy nazwa użytkownika jest pusta lub już zajęta:

FUsers.AddUser('alice', 'secret123', 'admin,reader');
FUsers.AddUser('bob', 'secret456', 'reader');

Reszta interfejsu jest taka, jakiej można się spodziewać, a każde wyszukiwanie jest bezpieczne wątkowo:

if FUsers.ValidateCredentials('alice', 'secret123') then
  ...

FUsers.SetPassword('bob', 'newsecret');
FUsers.EnableUser('bob', False);
FUsers.DeleteUser('bob');

Role

Role to lista rozdzielona przecinkami, zapisana na koncie, wraz z metodami pomocniczymi do ich odczytu i sprawdzania. Bramka roli w procedurze obsługi to jedno wywołanie:

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;

Haszowanie

Hasła są domyślnie haszowane algorytmem SHA-512 w 10000 iteracjach, a każde konto ma własną losową sól. To właśnie ta sól przypisana do konta sprawia, że dwa identyczne hasła dają różne skróty, więc rzadko trzeba tu cokolwiek zmieniać:

FUsers.Hashing.Algorithm := uhaSHA512;
FUsers.Hashing.Iterations := 10000;

Właściwość Salt to coś zupełnie innego i warto przeczytać o niej uważnie. Jest to opcjonalny pieprz domieszany do każdego skrótu ponad sól przypisaną do konta, a domyślnie jest pusty i to celowo: wartość wymyślona w momencie tworzenia obiektu byłaby inna przy kolejnym uruchomieniu, a wtedy każde poprawne hasło zostałoby odrzucone bez żadnego komunikatu wyjaśniającego dlaczego. Ustaw ją tylko wtedy, gdy może pochodzić z miejsca innego niż sam magazyn użytkowników, na przykład ze zmiennej środowiskowej albo z sejfu kluczy. Pieprz przechowywany obok skrótów, które ma chronić, niczego nie wnosi. Jego zmiana unieważnia każde zapisane wcześniej hasło.

Trwałość

Magazyn domyślnie działa w pamięci. Wskaż mu plik, a przetrwa restart:

FUsers.Storage.StorageType := ustFile;
FUsers.Storage.FileName := 'sgcRESTUsers.dat';
FUsers.Storage.EncryptAtRest := True;
FUsers.Storage.EncryptionKey := GetKeyFromEnvironment;
FUsers.Storage.AutoSaveSeconds := 30;
FUsers.LoadUsers;

Przy ustawionym AutoSaveSeconds magazyn okresowo zapisuje się sam; w przeciwnym razie wywołaj SaveUsers lub SaveToFile, kiedy Ci to pasuje. Jeśli magazyn żyje już w Twojej własnej bazie danych, ustaw StorageType na ustCustom i obsłuż zamiast tego zdarzenia, gdzie OnValidateCredentials staje się rozstrzygające:

procedure TForm1.UsersValidateCredentials(Sender: TObject;
  const aUsername, aPassword: string; var Valid: Boolean);
begin
  Valid := MyDatabase.CheckLogin(aUsername, aPassword);
end;

Jeden szczegół, który ma znaczenie, jeśli budujesz ekran administracyjny: GetUserByIndex czyści PasswordHash i Salt przed zwróceniem rekordu, więc trasa wyliczająca konta nie może ujawnić poświadczeń nawet przez przypadek. FindUser tego nie robi. Zwraca rekord dokładnie w takiej postaci, w jakiej jest przechowywany, razem z polami poświadczeń, ponieważ to właśnie z tego wyszukiwania korzysta sama bramka uwierzytelniania. Odczytaj z niego potrzebne pola i nigdy nie serializuj całego rekordu do treści odpowiedzi.

Wielodostępność

TsgcHTTPServer_Tenancy odpowiada na jedno pytanie przy każdym żądaniu: dla którego klienta ono jest? Rozpoznaje ciąg dzierżawcy, zanim uruchomi się Twoja procedura obsługi, a serwer udostępnia go we właściwości Tenant tylko do odczytu.

FTenancy := TsgcHTTPServer_Tenancy.Create(self);
FTenancy.Resolution := trHeader;
FTenancy.HeaderName := 'X-Tenant-Id';
FTenancy.DefaultTenant := 'public';
FServer.Tenancy := FTenancy;

Dostępnych jest pięć trybów rozpoznawania:

RozpoznawanieŹródłoKonfigurowane przez
trNonewyłączone, Tenant jest zawsze pusty
trHostnazwa hostaHostSuffix
trPathsegment ścieżki żądaniaPathSegmentIndex
trHeadernagłówek żądaniaHeaderName
trJWTClaimoświadczenie tokenu bearerClaimName

Przy trHost i HostSuffix ustawionym na .example.com żądanie do acme.example.com zostanie rozpoznane jako acme. Przy trPath i PathSegmentIndex równym 0 tak samo zostanie rozpoznane /acme/api/orders. Gdy skonfigurowane źródło nic nie zwróci, używana jest wartość DefaultTenant.

Odczyt w procedurze obsługi to po prostu właściwość:

procedure TForm1.ServerCommandGet(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
  AResponseInfo.ResponseNo := 200;
  AResponseInfo.ContentType := 'application/json';
  AResponseInfo.ContentText := '{"tenant":"' + FServer.Tenant + '"}';
end;

Tenant jest poprawny wewnątrz OnBeforeCommand, OnCommandGet i OnCommandOther oraz jest lokalny dla wątku, więc obciążony serwer obsługujący jednocześnie wielu dzierżawców nigdy ich nie pomiesza.

W przypadku trJWTClaim jedna rzecz wymaga jasnego postawienia sprawy. Oświadczenie jest odczytywane z ładunku tokenu bez weryfikacji podpisu, ponieważ dzierżawca jest tylko wskazówką do trasowania. Podpis nadal jest sprawdzany przez uwierzytelnianie JWT, gdy je włączysz. Nie traktuj dzierżawcy jako samodzielnego dowodu tożsamości.

Jeśli żaden z pięciu trybów nie pasuje, OnResolveTenant przekazuje Ci wszystkie źródła naraz i pozwala zdecydować:

procedure TForm1.TenancyResolveTenant(Sender: TObject;
  const aHost, aPath, aHeaderValue, aJWTPayload: string; var aTenant: string);
begin
  aTenant := LookupTenantForHost(aHost);
end;

Metryki i kondycja

TsgcHTTPServerStats zlicza to, co robił serwer, i może opublikować te dane na dwóch punktach końcowych. Oba są wyłączone, dopóki nie włączysz ich pojedynczo, więc nic nie staje się osiągalne tylko dlatego, że upuściłeś komponent na formularzu.

FStats := TsgcHTTPServerStats.Create(self);
FStats.Endpoints.Metrics.Enabled := True;
FStats.Endpoints.Health.Enabled := True;
FServer.ServerStats := FStats;

Liczniki można też odczytać z kodu, co przydaje się przy wewnętrznym ekranie stanu:

lblRequests.Caption := IntToStr(FStats.TotalRequests);
lblErrors.Caption := IntToStr(FStats.Status5xx);
lblLatency.Caption := IntToStr(FStats.LatencyAvgMs) + ' ms';
lblUptime.Caption := IntToStr(FStats.UptimeSeconds) + ' s';

/metrics zwraca dane bezpośrednio w tekstowym formacie ekspozycji Prometheus, bez eksportera pośredniego. Zawarte są liczniki dla poszczególnych punktów końcowych, przy czym liczność jest ograniczona do 256 różnych ścieżek, aby trasa zawierająca identyfikator nie wysadziła liczby serii. Wszystko powyżej limitu trafia do kubełka 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

Jeśli podłączone są komponenty zapory, ogranicznika przepustowości, wyłącznika awaryjnego lub menedżera kluczy API, ich własne metryki są dodawane do tego samego wyniku:

FStats.RateLimiter := FRateLimiter;
FStats.Firewall := FFirewall;

/health zwraca zwięzły dokument JSON. Jego status przyjmuje wartość ok albo degraded, gdy podłączony jest wyłącznik awaryjny i ma otwarte obwody, co czyni go bezpośrednio przydatnym jako sonda load balancera.

Na koniec, zdarzenie OnStats zgłasza cały obiekt statystyk, jeśli wolisz sam wypchnąć te liczby gdzie indziej:

procedure TForm1.StatsEvent(Sender: TObject;
  const aStats: TsgcHTTPServerStats);
begin
  MyTelemetry.Send(aStats.TotalRequests, aStats.LatencyAvgMs);
end;

Wszystko razem

Serwer z podłączonymi wszystkimi trzema komponentami to około tuzina linii, a każdy komponent pozostaje niezależny od pozostałych:

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;

Następny artykuł stawia przed tym serwerem kontrakt OpenAPI, dzięki czemu trasowanie, walidacja i dokumentacja pochodzą ze specyfikacji.

Pobierz najnowszą wersję ze strony pobierania sgcWebSockets.