OBS/Kostenpflichtige Module/RESTServer/Scripting: Unterschied zwischen den Versionen
Keine Bearbeitungszusammenfassung |
Keine Bearbeitungszusammenfassung |
||
| (Eine dazwischenliegende Version desselben Benutzers wird nicht angezeigt) | |||
| Zeile 96: | Zeile 96: | ||
Da der Präfix ''_OBS_'' für von aussen gelieferte Header- und Query-Parameter gesperrt ist, sind diese Werte nicht durch den Client fälschbar. Ein vollständiges Beispiel zeigt [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4 - Pfad-Parameter]]. | Da der Präfix ''_OBS_'' für von aussen gelieferte Header- und Query-Parameter gesperrt ist, sind diese Werte nicht durch den Client fälschbar. Ein vollständiges Beispiel zeigt [[OBS/Kostenpflichtige Module/RESTServer/Beispiel4|Beispiel 4 - Pfad-Parameter]]. | ||
===Datei-Uploads=== | |||
Ist der Endpunkt für Uploads freigeschaltet (<code>re_upload = 1</code>, siehe | |||
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]]), nimmt der Server | |||
hochgeladene Dateien entgegen, legt sie in einem temporären Verzeichnis ab und | |||
übergibt dem Skript Pfad, Originalname und Content-Type über reservierte | |||
Parameter. Das Skript entscheidet selbst über die weitere Verarbeitung (z.B. | |||
DMS-Ablage). Der Body wird in diesem Fall '''nicht''' als JSON geparst - ''oBody'' | |||
ist ''nil''. Es gilt nicht das JSON-Body-Limit (10 MB), sondern die pro | |||
Endpunkt konfigurierte Grösse (''re_upload_size'', Standard 25 MB; | |||
Überschreitung -> 413). | |||
Beide Übertragungsarten - <code>multipart/form-data</code> und der resumable | |||
<code>Content-Range</code>-Upload - liefern dem Skript dieselben Parameter: | |||
{| class="wikitable" | |||
! Parameter !! Inhalt | |||
|- | |||
| _OBS_UPLOAD_PATH || Vollständiger Pfad zur temporären Datei auf dem Server | |||
|- | |||
| _OBS_UPLOAD_NAME || Originaldateiname | |||
|- | |||
| _OBS_UPLOAD_CONTENTTYPE || Content-Type der Datei (Default <code>application/octet-stream</code>, wenn der Client keinen angibt) | |||
|} | |||
Der '''Dateiname ist Pflicht'''. Fehlt er, lehnt der Server den Upload mit | |||
'''400 Bad Request''' ab und das Skript wird nicht ausgeführt - das Skript kann | |||
sich also darauf verlassen, dass ''_OBS_UPLOAD_PATH'' und ''_OBS_UPLOAD_NAME'' | |||
gesetzt sind, sobald es läuft. | |||
{{Hinweis|Die temporäre Datei wird nicht automatisch verschoben. Das Skript muss sie an ihren Zielort (DMS, Verzeichnis, ...) übernehmen.}} | |||
====Einfacher Upload (multipart/form-data)==== | |||
Eine Datei pro Request. Name und Content-Type stammen aus der | |||
<code>Content-Disposition</code> der Datei-Partie (<code>filename=</code>). Der | |||
Server speichert die erste Datei-Partie und ruft das Skript auf: | |||
<syntaxhighlight lang="pascal" line> | |||
function Post(oParams: TStrings; oBody: TJSONObject): string; | |||
var oRes : TJSONObject; | |||
cPfad: string; | |||
cName: string; | |||
cTyp : string; | |||
begin | |||
oRes := TJSONObject.Create(); | |||
try | |||
cPfad := oParams.Values['_OBS_UPLOAD_PATH']; | |||
cName := oParams.Values['_OBS_UPLOAD_NAME']; | |||
cTyp := oParams.Values['_OBS_UPLOAD_CONTENTTYPE']; | |||
// ... Datei aus cPfad ins DMS / Zielverzeichnis uebernehmen ... | |||
oRes.AddPair('status' , 'ok'); | |||
oRes.AddPair('dateiname', cName); | |||
result := oRes.ToJSON(); | |||
finally | |||
MyFreeAndNil(oRes); | |||
end; | |||
end; | |||
</syntaxhighlight> | |||
====Resumable/Chunked Upload (Content-Range)==== | |||
Für grosse Dateien überträgt der Client die Datei in Teilstücken (Chunks) mit dem | |||
Header <code>Content-Range: bytes START-END/TOTAL</code>. Der Server hängt die | |||
Chunks '''sequentiell''' an eine Session-Datei an und behandelt unvollständige | |||
Uploads selbst - das Endpunkt-Skript läuft '''erst beim vollständigen Upload''' | |||
(dann mit gesetztem ''_OBS_UPLOAD_PATH'', ''_OBS_UPLOAD_NAME'' und | |||
''_OBS_UPLOAD_CONTENTTYPE''). | |||
Name und Content-Type sind im <code>Content-Range</code>-Protokoll nicht enthalten; | |||
der Client liefert sie deshalb '''im ersten Chunk''' über den Header | |||
<code>Upload-Metadata</code> (Format wie bei tus: kommagetrennte Paare | |||
<code>schlüssel base64wert</code>, Werte Base64-kodiert): | |||
<pre> | |||
Upload-Metadata: filename ZmlsZS5wZGY=,filetype YXBwbGljYXRpb24vcGRm | |||
</pre> | |||
Erkannt werden die Schlüssel <code>filename</code> (Pflicht) und | |||
<code>filetype</code> (optional). Fehlt <code>filename</code> im ersten Chunk, | |||
wird der Upload mit '''400''' abgelehnt, ohne dass eine Datei angelegt wird. | |||
Folge-Chunks und Statusabfragen müssen die Metadaten nicht erneut senden. | |||
Ablauf (vom Client gesteuert, der Server antwortet jeweils): | |||
{| class="wikitable" | |||
! Request !! Server-Antwort | |||
|- | |||
| Erster Chunk <code>Content-Range: bytes 0-1048575/5000000</code> + <code>Upload-Metadata</code> || '''308''' Resume Incomplete + Header <code>Upload-Id</code>, <code>Upload-Offset</code>, <code>Range</code> | |||
|- | |||
| weitere Chunks (jeweils <code>Upload-Id</code> mitsenden) || '''308''' mit aktualisiertem <code>Upload-Offset</code> | |||
|- | |||
| letzter Chunk (Bereich erreicht TOTAL) || normale Skript-Antwort (z.B. '''200'''/'''201''') | |||
|- | |||
| Statusabfrage <code>Content-Range: bytes */5000000</code> || aktueller <code>Upload-Offset</code>, ohne anzuhängen (Resume) | |||
|} | |||
{| class="wikitable" | |||
! Header !! Richtung !! Bedeutung | |||
|- | |||
| Content-Range || Request || <code>bytes START-END/TOTAL</code>; <code>bytes */TOTAL</code> nur als Statusabfrage | |||
|- | |||
| Upload-Metadata || Request || tus-Metadaten im ersten Chunk: <code>filename</code> (Pflicht), <code>filetype</code> (optional), Base64-kodiert | |||
|- | |||
| Upload-Id || Request/Response || Session-Kennung. Fehlt sie im ersten Request, erzeugt der Server eine und gibt sie zurück; alle Folge-Chunks müssen sie mitsenden. | |||
|- | |||
| Upload-Offset || Response || Anzahl der bereits gespeicherten Bytes (= Startoffset des nächsten Chunks) | |||
|- | |||
| Range || Response || Bereits gespeicherter Bereich (<code>bytes=0-N</code>) | |||
|} | |||
Der Server hängt einen Chunk '''nur an, wenn START dem bereits gespeicherten Stand | |||
entspricht'''. Bei Abweichung (doppelter Chunk, Lücke) wird nicht angehängt, | |||
sondern der aktuelle ''Upload-Offset'' zurückgemeldet - der Client setzt ab dort | |||
neu auf. Eine separate Statusabfrage ist für ein Resume daher nicht zwingend nötig. | |||
Überschreitet ein Chunk bzw. die Gesamtgrösse das Limit (''re_upload_size''), | |||
antwortet der Server mit '''413 Payload Too Large'''. | |||
{{Hinweis|Der Upload-Status liegt prozesslokal im Speicher. Nach einem Neustart des Servers muss ein unvollständiger Upload neu begonnen werden.}} | |||
==Rückgabe== | ==Rückgabe== | ||
Aktuelle Version vom 30. Juni 2026, 12:28 Uhr
- A Preise aktualisieren
- C Personen übertragen
- E Kategorien verwalten
- G Kataloge verwalten
- I Merkliste übertragen
- K Varianten übertragen
- L Artikelvarianten übertragen
- M Referenzarten übertragen
- N Lagerbestände verwalten
- U Bestellungen einlesen
- V leere Passworte füllen
- W Update-Informationen zurücksetzen
- X Konfiguration
- Z Protokoll
Anleitung für Endpunkt-Skripte
Jede Anfrage an einen Endpunkt wird nach erfolgreicher Authentifizierung und Autorisierung an das hinterlegte Skript weitergegeben. Das Skript ist in Object Pascal geschrieben und greift auf die OBS-Bibliothek (DB-Zugriff, Hilfsfunktionen) zu.
Sicherheitshinweise
- Eingaben gegen erwartete Werte prüfen (z.B. Pflichtfelder, erlaubte Typen, Wertebereiche).
- Nur das zurückgeben, was der Konsument wirklich braucht - keine internen IDs, keine Sys-Felder, keine Passwörter.
- Bei Fehlern keine internen Details an den Konsumenten zurückgeben; stattdessen schreibt der Server ohnehin Detail-Einträge in RESTSRV_PROTO.
Methoden-Signatur
Pro HTTP-Methode wird im Skript eine gleichnamige Funktion implementiert. Der Server ruft genau die Funktion auf, die zur Methode des eingehenden Requests passt.
function Get (oParams: TStrings; oBody: TJSONObject): string; function Post (oParams: TStrings; oBody: TJSONObject): string; function Put (oParams: TStrings; oBody: TJSONObject): string; function Delete(oParams: TStrings; oBody: TJSONObject): string; function Patch (oParams: TStrings; oBody: TJSONObject): string;
Nicht implementierte Methoden liefern automatisch 405-ähnliche Fehler über die generische Skript-Antwort.
Parameter
oParams (TStrings)
Enthält alle Query-Parameter, POST-Parameter (form-urlencoded) sowie die durchgereichten HTTP-Header. Werte sind immer Strings.
Filterung durch den Server:
- Geblockte Header werden nicht durchgereicht: authorization, cookie, proxy-authorization, x-forwarded-for, x-real-ip, apikey, api_key.
- Parameter mit Präfix _OBS_ können von aussen nicht gesetzt werden; sie sind für interne Werte reserviert.
- Werte werden auf max. 1024 Zeichen begrenzt.
- Null-Bytes und Steuerzeichen (ausser Tab, CR, LF) werden entfernt.
Zugriff im Skript:
if (not Empty(oParams.Values['kundennr'])) then begin
cKundenNr := oParams.Values['kundennr'];
end;
oBody (TJSONObject)
Enthält den Request-Body, sofern dieser ein gültiges JSON-Objekt ist. Bei leerem oder nicht-JSON-Body wird nil übergeben.
cUser := ;
cPass := ;
if (Assigned(oBody)) then begin
oVal := oBody.GetValue('username');
if (Assigned(oVal)) then begin
cUser := oVal.Value;
end;
oVal := oBody.GetValue('password');
if (Assigned(oVal)) then begin
cPass := oVal.Value;
end;
end;
Maximale Body-Grösse: 10 MB.
Reservierte _OBS_-Parameter
Bei aktiver JWT-Authentifizierung stehen die Token-Claims als Parameter zur Verfügung:
| Parameter | Inhalt |
|---|---|
| _OBS_JWT_ID | JWT-Id (jti-Claim) - typisch die User-Id |
| _OBS_JWT_SUBJECT | Subject (sub-Claim) - typisch Benutzername |
| _OBS_JWT_AUDIENCE | Audience (aud-Claim) - typisch Mandant / Rolle |
| _OBS_JWT_CLAIM_<name> | Beliebiger Custom-Claim des Tokens (z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles); im Folge-Skript lesbar |
| _OBS_TRACE_ID | Korrelations-ID des Requests (auch als Response-Header X-Trace-Id) |
Diese Werte werden vom Server gesetzt und können vom Skript für Berechtigungs- und Mandantenprüfungen verwendet werden.
Pfad-Parameter
Stammt der Endpunkt aus einem Pfad-Template mit Platzhaltern (siehe Endpunkte), stehen die aus den Platzhaltern erfassten Werte als reservierte Parameter mit Präfix _OBS_PATH_ in oParams bereit:
| Template | Zugriff im Skript |
|---|---|
/orders/{uid} |
oParams.Values['_OBS_PATH_uid'] |
/orders/{uid}/modules/{code} |
oParams.Values['_OBS_PATH_uid'], oParams.Values['_OBS_PATH_code'] |
Da der Präfix _OBS_ für von aussen gelieferte Header- und Query-Parameter gesperrt ist, sind diese Werte nicht durch den Client fälschbar. Ein vollständiges Beispiel zeigt Beispiel 4 - Pfad-Parameter.
Datei-Uploads
Ist der Endpunkt für Uploads freigeschaltet (re_upload = 1, siehe
Endpunkte), nimmt der Server
hochgeladene Dateien entgegen, legt sie in einem temporären Verzeichnis ab und
übergibt dem Skript Pfad, Originalname und Content-Type über reservierte
Parameter. Das Skript entscheidet selbst über die weitere Verarbeitung (z.B.
DMS-Ablage). Der Body wird in diesem Fall nicht als JSON geparst - oBody
ist nil. Es gilt nicht das JSON-Body-Limit (10 MB), sondern die pro
Endpunkt konfigurierte Grösse (re_upload_size, Standard 25 MB;
Überschreitung -> 413).
Beide Übertragungsarten - multipart/form-data und der resumable
Content-Range-Upload - liefern dem Skript dieselben Parameter:
| Parameter | Inhalt |
|---|---|
| _OBS_UPLOAD_PATH | Vollständiger Pfad zur temporären Datei auf dem Server |
| _OBS_UPLOAD_NAME | Originaldateiname |
| _OBS_UPLOAD_CONTENTTYPE | Content-Type der Datei (Default application/octet-stream, wenn der Client keinen angibt)
|
Der Dateiname ist Pflicht. Fehlt er, lehnt der Server den Upload mit 400 Bad Request ab und das Skript wird nicht ausgeführt - das Skript kann sich also darauf verlassen, dass _OBS_UPLOAD_PATH und _OBS_UPLOAD_NAME gesetzt sind, sobald es läuft.
Einfacher Upload (multipart/form-data)
Eine Datei pro Request. Name und Content-Type stammen aus der
Content-Disposition der Datei-Partie (filename=). Der
Server speichert die erste Datei-Partie und ruft das Skript auf:
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes : TJSONObject;
cPfad: string;
cName: string;
cTyp : string;
begin
oRes := TJSONObject.Create();
try
cPfad := oParams.Values['_OBS_UPLOAD_PATH'];
cName := oParams.Values['_OBS_UPLOAD_NAME'];
cTyp := oParams.Values['_OBS_UPLOAD_CONTENTTYPE'];
// ... Datei aus cPfad ins DMS / Zielverzeichnis uebernehmen ...
oRes.AddPair('status' , 'ok');
oRes.AddPair('dateiname', cName);
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
Resumable/Chunked Upload (Content-Range)
Für grosse Dateien überträgt der Client die Datei in Teilstücken (Chunks) mit dem
Header Content-Range: bytes START-END/TOTAL. Der Server hängt die
Chunks sequentiell an eine Session-Datei an und behandelt unvollständige
Uploads selbst - das Endpunkt-Skript läuft erst beim vollständigen Upload
(dann mit gesetztem _OBS_UPLOAD_PATH, _OBS_UPLOAD_NAME und
_OBS_UPLOAD_CONTENTTYPE).
Name und Content-Type sind im Content-Range-Protokoll nicht enthalten;
der Client liefert sie deshalb im ersten Chunk über den Header
Upload-Metadata (Format wie bei tus: kommagetrennte Paare
schlüssel base64wert, Werte Base64-kodiert):
Upload-Metadata: filename ZmlsZS5wZGY=,filetype YXBwbGljYXRpb24vcGRm
Erkannt werden die Schlüssel filename (Pflicht) und
filetype (optional). Fehlt filename im ersten Chunk,
wird der Upload mit 400 abgelehnt, ohne dass eine Datei angelegt wird.
Folge-Chunks und Statusabfragen müssen die Metadaten nicht erneut senden.
Ablauf (vom Client gesteuert, der Server antwortet jeweils):
| Request | Server-Antwort |
|---|---|
Erster Chunk Content-Range: bytes 0-1048575/5000000 + Upload-Metadata |
308 Resume Incomplete + Header Upload-Id, Upload-Offset, Range
|
weitere Chunks (jeweils Upload-Id mitsenden) |
308 mit aktualisiertem Upload-Offset
|
| letzter Chunk (Bereich erreicht TOTAL) | normale Skript-Antwort (z.B. 200/201) |
Statusabfrage Content-Range: bytes */5000000 |
aktueller Upload-Offset, ohne anzuhängen (Resume)
|
| Header | Richtung | Bedeutung |
|---|---|---|
| Content-Range | Request | bytes START-END/TOTAL; bytes */TOTAL nur als Statusabfrage
|
| Upload-Metadata | Request | tus-Metadaten im ersten Chunk: filename (Pflicht), filetype (optional), Base64-kodiert
|
| Upload-Id | Request/Response | Session-Kennung. Fehlt sie im ersten Request, erzeugt der Server eine und gibt sie zurück; alle Folge-Chunks müssen sie mitsenden. |
| Upload-Offset | Response | Anzahl der bereits gespeicherten Bytes (= Startoffset des nächsten Chunks) |
| Range | Response | Bereits gespeicherter Bereich (bytes=0-N)
|
Der Server hängt einen Chunk nur an, wenn START dem bereits gespeicherten Stand entspricht. Bei Abweichung (doppelter Chunk, Lücke) wird nicht angehängt, sondern der aktuelle Upload-Offset zurückgemeldet - der Client setzt ab dort neu auf. Eine separate Statusabfrage ist für ein Resume daher nicht zwingend nötig.
Überschreitet ein Chunk bzw. die Gesamtgrösse das Limit (re_upload_size), antwortet der Server mit 413 Payload Too Large.
Rückgabe
Die Rückgabe ist immer ein JSON-String. Wird ein leerer String zurückgegeben, antwortet der Server automatisch mit {}. Der Server setzt Content-Type auf application/json; charset=utf-8 und Status auf 200, sofern das Skript nicht selbst einen Fehler signalisiert.
Einfaches Beispiel:
function Get(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
begin
oRes := TJSONObject.Create();
try
oRes.AddPair('wert_string', '123');
oRes.AddPair('wert_int' , 456);
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
Antwort steuern: Statuscode, Header, traceId
Ein Skript kann den HTTP-Statuscode und beliebige Response-Header Über reservierte Felder in der Antwort setzen. Der Server wertet sie aus und entfernt sie vor dem Senden aus dem Body.
| Antwort-Feld | Wirkung |
|---|---|
| _OBS_HTTP_STATUS (Zahl) | HTTP-Statuscode (z.B. 201, 204, 400, 409, 422). Ohne Angabe: 200. Bei 204 wird kein Body gesendet. |
| _OBS_HEADERS (Objekt) | Beliebige Response-Header, z.B. ETag, Location, Retry-After. CR/LF in Namen/Werten werden entfernt (Schutz vor Header-Injection). |
Zusätzlich trÄgt jede Antwort den Header X-Trace-Id (Korrelations-ID). Dieselbe ID liegt dem Skript als oParams.Values['_OBS_TRACE_ID'] vor und erscheint in jeder Server-Fehler-Logzeile in RESTSRV_PROTO - so lÄsst sich ein Fehler ohne GerÄtezugriff im Log wiederfinden.
Beispiel: Anlegen mit 201, Location und ETag
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
begin
oRes := TJSONObject.Create();
try
// ... Datensatz anlegen, neue UID + Version (ETag) ermitteln ...
oHdr := TJSONObject.Create();
oHdr.AddPair('Location', '/v1/orders/' + cNeueUid);
oHdr.AddPair('ETag', '1');
oRes.AddPair('_OBS_HTTP_STATUS', 201);
oRes.AddPair('_OBS_HEADERS', oHdr);
oRes.AddPair('uid', cNeueUid);
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
Beispiel: Strukturierter Fehler mit Statuscode und traceId
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oErr: TJSONObject;
begin
oRes := TJSONObject.Create();
try
oErr := TJSONObject.Create();
oErr.AddPair('code', 'VALIDATION_FAILED');
oErr.AddPair('message', 'Feld "menge" fehlt');
oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);
oRes.AddPair('_OBS_HTTP_STATUS', 422);
oRes.AddPair('error', oErr);
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
Beispiel: Optimistic Concurrency (ETag / If-Match)
Mit Statuscode, Headern und dem lesbaren Header If-Match fÜhrt das Skript je Datensatz einen VersionszÄhler und lehnt veraltete Schreibzugriffe mit 409 ab:
function Put(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
nAktuell, nIfMatch: integer;
begin
oRes := TJSONObject.Create();
try
nAktuell := AuftragVersion(oParams.Values['_OBS_PATH_uid']);
nIfMatch := iVal(oParams.Values['if-match']);
if (nIfMatch <> nAktuell) then begin
oHdr := TJSONObject.Create();
oHdr.AddPair('ETag', xStr(nAktuell));
oRes.AddPair('_OBS_HTTP_STATUS', 409);
oRes.AddPair('_OBS_HEADERS', oHdr);
oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'VERSION_CONFLICT'));
result := oRes.ToJSON();
exit;
end;
// ... speichern, Version hochzÄhlen ...
oHdr := TJSONObject.Create();
oHdr.AddPair('ETag', xStr(nAktuell + 1));
oRes.AddPair('_OBS_HEADERS', oHdr);
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
JWT-Authentifizierungs-Skript
Bei Zugängen mit aktiver JWT-Pflicht wird über den JWT-Endpunkt das im Zugang hinterlegte Authentifizierungs-Skript aufgerufen (F7 in der Zugänge-Liste). Das Skript muss eine Methode Authenticate bereitstellen:
function Authenticate(oParams: TStrings; oBody: TJSONObject): string;
var oRes : TJSONObject;
cUser : string;
cPass : string;
begin
oRes := TJSONObject.Create();
try
// Eingangsdaten lesen (Body oder Query-Param)
cUser := '';
cPass := '';
if (Assigned(oBody)) then begin
oVal := oBody.GetValue('username');
if (Assigned(oVal)) then begin
cUser := oVal.Value;
end;
oVal := oBody.GetValue('password');
if (Assigned(oVal)) then begin
cPass := oVal.Value;
end;
end;
// Prüfung gegen eigene Tabelle, Hash-Verfahren, LDAP, ...
if (PasswortPasst(cUser, cPass)) then begin
oRes.AddPair('status' , 1);
oRes.AddPair('_OBS_JWT_ID' , cUser);
oRes.AddPair('_OBS_JWT_SUBJECT', cUser);
oRes.AddPair('_OBS_JWT_AUDIENCE', 'mandant1');
end else begin
oRes.AddPair('status', 9);
oRes.AddPair('error' , 'Login fehlgeschlagen');
end;
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
Rückgabewerte:
| status | Bedeutung |
|---|---|
| 1 | Erfolgreich, der Server erzeugt aus den _OBS_JWT_*-Werten einen Token (HS256) |
| 9 | Misserfolg, der Server antwortet mit 403 und protokolliert den Wert von error |
Custom-Claims, serverTime und Refresh
Das Authenticate-Skript kann dem Token zusÄtzlich beliebige Custom-Claims mitgeben - z.B. Mandant und Rollen - und optional ein Refresh-Token anstoßen:
| RÜckgabefeld | Bedeutung |
|---|---|
| _OBS_JWT_CLAIM_<name> | Beliebiger Custom-Claim, z.B. _OBS_JWT_CLAIM_tenant, _OBS_JWT_CLAIM_roles. Im Folge-Skript lesbar als oParams.Values['_OBS_JWT_CLAIM_<name>']. |
| _OBS_JWT_REFRESH_ID | (optional) jti des Refresh-Tokens. Nur wenn gesetzt, stellt der Server ein Refresh-Token aus. |
| _OBS_JWT_REFRESH_EXP | (optional) Lebensdauer des Refresh-Tokens in Minuten (Default 90 Tage = 129600). |
Jedes Token trÄgt automatisch den Claim token_use (access bzw. refresh). Die Antwort enthÄlt bei Erfolg serverTime (ISO-8601 mit Offset), bei ausgestelltem Refresh zusÄtzlich refreshToken:
{"token":"...", "refreshToken":"...", "serverTime":"2026-06-29T15:30:12+02:00"}
Refresh-Methode
Der Refresh lÄuft Über denselben JWT-Endpunkt und dasselbe Skript. Liegt am JWT-Endpunkt ein Authorization: Bearer <token> vor, ruft der Server statt Authenticate die Methode Refresh auf (sonst Login). Der Server verifiziert den Refresh-Token zuvor (Signatur, Ablauf, token_use=refresh); das alte Refresh-jti steht als oParams.Values['_OBS_JWT_ID'] bereit. Das Skript prÜft/rotiert seine Sperrtabelle und liefert wie Authenticate neue Claims zurÜck. Der Server stellt daraufhin ein rotiertes Token-Paar aus; ein bereits benutztes Refresh-Token wird mit 401 abgelehnt.
function Refresh(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
cAltesJti, cNeuesJti: string;
begin
oRes := TJSONObject.Create();
try
cAltesJti := oParams.Values['_OBS_JWT_ID'];
// altes Refresh-jti gegen eigene Sperrtabelle prÜfen
if (not RefreshJtiGueltig(cAltesJti)) then begin
oRes.AddPair('status', 9);
oRes.AddPair('error' , 'AUTH_EXPIRED');
result := oRes.ToJSON();
exit;
end;
cNeuesJti := GlobalUID();
RefreshJtiRotieren(cAltesJti, cNeuesJti); // altes sperren, neues ablegen
oRes.AddPair('status' , 1);
oRes.AddPair('_OBS_JWT_ID' , GlobalUID());
oRes.AddPair('_OBS_JWT_SUBJECT' , oParams.Values['_OBS_JWT_SUBJECT']);
oRes.AddPair('_OBS_JWT_CLAIM_tenant', oParams.Values['_OBS_JWT_CLAIM_tenant']);
oRes.AddPair('_OBS_JWT_CLAIM_roles' , oParams.Values['_OBS_JWT_CLAIM_roles']);
oRes.AddPair('_OBS_JWT_REFRESH_ID' , cNeuesJti);
result := oRes.ToJSON();
finally
MyFreeAndNil(oRes);
end;
end;
Fehlerbehandlung im Skript
Tritt im Skript eine Exception auf oder schlägt die Syntax-Prüfung fehl, antwortet der Server mit 500 Interner Fehler und protokolliert die Detail-Meldung in RESTSRV_PROTO (mit Skript-Fehlertext). Der Konsument sieht keine internen Details.
Will das Skript einen spezifischen Fehler an den Konsumenten zurückgeben, ist eine eigene JSON-Struktur zu liefern:
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
begin
oRes := TJSONObject.Create();
try
if (Empty(oParams.Values['kundennr'])) then begin
oRes.AddPair('status', 'error');
oRes.AddPair('msg' , 'Parameter ''kundennr'' fehlt');
result := oRes.ToJSON();
exit;
end;
// ...
finally
MyFreeAndNil(oRes);
end;
end;
Skript-Cache
Der Server cached das kompilierte Skript pro Endpunkt (Schlüssel: sys_date des Endpunkts). Zusätzlich werden die Endpunkt-Definitionen pro Server-Profil in einem TTL-Cache (Standard 60 s) gehalten. Eine Skript-Änderung über F7 wird daher erst nach Ablauf dieses TTL (bzw. nach einer Cache-Invalidierung) wirksam - typischerweise innerhalb einer Minute, nicht zwingend sofort.
Verfügbare Bibliotheken
Im Skript können alle OBS-Standard-Bibliotheken verwendet werden. Typische Einstiegspunkte:
- Base.Tools - String-, Datum-, IIF-, Empty-Helper
- Base.DB / Base.xQuery - Datenbank-Operationen, DB_SQLVal, DB_SOpen, DB_LSeek
- Base.qSqlReg - qSqlInit, qSet, SaveData
- System.JSON - JSON-Objekte und -Arrays
- Base.ToolsConst - Konstanten wie CRLF, SINGELQUOTE, EMPTY_DATE
Konkrete Beispiele: