IR
irwinrodriguez.dev
Zurück zur Dokumentation

Middleware

Die Middlewares von FoxServer sind Teile des Hosts, in X# geschrieben, die die Anfrage vor und nach dem Controller verarbeiten. Sie kommen mit dem Server und werden im Manifest eingeschaltet und eingestellt: JWT, Anfrageprotokollierung, SSE, Webhooks, Status, OpenAPI, JSON-RPC und MCP, unter anderen.

Zwei Feinheiten, die einen Nachmittag sparen. Aus VFP heraus lässt sich keine Middleware schreiben: Ihr Projekt kann BeforeEndpoint und AfterEndpoint je Controller verwenden und den ApplicationLifecycle für das, was dem ganzen Projekt gehört. Und CORS ist keine Middleware: es wird vor der Kette aufgelöst und lässt sich daher nicht mit den übrigen ein- und ausschalten.

JWT-Authentifizierung

Sie schützt die Endpunkte, die in ihrem HELP kein public tragen. Der Client sendet Authorization: Bearer <Token>, und die Middleware prüft es, bevor Ihr Code läuft.

Im Manifest:

"middleware": {
  "auth": {
    "enabled": true,
    "type": "jwt",
    "secret": "<cifrado con middleware-set, nunca en claro>",
    "tokenexpirationseconds": 86400,
    "loginendpoint": "auth/login"
  }
}
Das Geheimnis wird verschlüsselt abgelegt auth.secret wird verschlüsselt gespeichert und mit foxserver middleware-set geschrieben. Ein Klartextgeheimnis im Manifest lässt den Motor mit 500 antworten („not a valid Base-64 string“). Zum Verschlüsseln muss die Coclass FoxServer.SecurityHelper registriert sein: der Installer erledigt das, bei einer ZIP-Installation registrieren Sie bin\SecurityHelper.comhost.dll mit dem 32-bittigen regsvr32.

Der Vertrag des Login-Endpunkts

Das lässt sich nicht erraten, und genau hier bleibt jeder beim ersten Mal hängen: das Token stellt die Middleware aus, nicht Ihr Code. Ihr Controller prüft die Anmeldedaten und sagt dem Motor, ob sie stimmen; der Motor signiert das JWT.

Die RouteMuss mit middleware.auth.loginendpoint übereinstimmen und im HELP public tragen
Der RumpfDarf nicht leer sein
X-Auth-StatusGenau "true" oder "false". Kein anderer Wert zählt
Bei "true"Das JSON muss ein Objekt in data mitbringen, was "responseformat": "foxserver" erzwingt: das freie Format hat keine Hülle
Innerhalb von dataDrei Pflichtfelder: id, name und role. Fehlt eines oder ist es leer, gibt es kein Token
Alles Übrige in dataReist als zusätzlicher Claim im Token und wird mit req.GetClaim() gelesen
Auf diesem Endpunktthis.Ok() und this.Fail() funktionieren nicht: sie bauen ihre eigene Antwort, der Header geht verloren, und der Motor antwortet mit 406. Dort verwendet man res.status(n).json(...)

Das ausgestellte Token trägt sub (die id), name, role, iat, exp und die zusätzlichen Claims. iat und exp sind NumericDate nach RFC 7519, ganze Sekunden, und ihre Differenz ist genau tokenexpirationseconds.

Der Login, vollständig:

pLogin: Anmeldedaten prüfen, damit die Middleware das JWT ausstellt
PROCEDURE pLogin(req, res) HELP "POST: auth/login public"
    LOCAL loRes, loData, lcEmail, lcPassword, loR

    loRes = THIS.newObject("status,data,message")

    TRY
        * Der rohe Rumpf plus ParseJson: mehr Kontrolle als req.json,
        * wenn der Rumpf auch fehlen kann.
        LOCAL loBody
        loBody = THIS.ParseJson(req.GetRawBody())
        IF ISNULL(loBody)
            loRes.status  = "error"
            loRes.message = "Ein JSON-Rumpf ist erforderlich"
            res.AddHeader("X-Auth-Status", "false")
            loR = res.status(400)
            loR.json(THIS.ToJson(loRes))
            EXIT
        ENDIF

        lcEmail    = LOWER(ALLTRIM(NVL(loBody.email, "")))
        lcPassword = ALLTRIM(NVL(loBody.password, ""))

        IF !THIS.AnmeldedatenGueltig(lcEmail, lcPassword)
            loRes.status  = "error"
            loRes.message = "Invalid credentials"
            res.AddHeader("X-Auth-Status", "false")
            loR = res.status(401)
            loR.json(THIS.ToJson(loRes))
            EXIT
        ENDIF

        * Die drei Felder sind Pflicht. Alles Weitere, das Sie loData
        * hinzufuegen, reist als Claim im Token.
        loData      = THIS.newObject("id,name,role")
        loData.id   = ALLTRIM(benutzer.USR_ID)
        loData.name = ALLTRIM(benutzer.USR_NAME)
        loData.role = ALLTRIM(benutzer.USR_ROLLE)

        loRes.status  = "ok"
        loRes.data    = loData
        loRes.message = "Successful login"

        * Diese Zeile stellt das Token aus. Das JWT signiert die
        * Middleware; hier wird nichts erzeugt.
        res.AddHeader("X-Auth-Status", "true")
        loR = res.status(200)
        loR.json(THIS.ToJson(loRes))
    CATCH TO loEx
        loRes.status  = "error"
        loRes.message = loEx.Message
        THIS.LogFromException(loEx)
        res.AddHeader("X-Auth-Status", "false")
        loR = res.status(500)
        loR.json(THIS.ToJson(loRes))
    ENDTRY
ENDPROC

Ein geschützter Endpunkt: einfach public weglassen

pMeinProfil: ein per JWT geschützter Endpunkt
PROCEDURE pMeinProfil(req, res) HELP "GET: profile"
    LOCAL loRes

    * Die Middleware hat das Token schon geprueft. Der Benutzer kommt
    * aufgeloest an, zusaetzliche Claims liest man einzeln.
    loRes = THIS.newObject("id,name,role,firma")
    loRes.id    = req.AuthenticatedUser.id
    loRes.name  = req.AuthenticatedUser.name
    loRes.role  = req.AuthenticatedUser.role
    loRes.firma = req.GetClaim("firma", "")

    RETURN THIS.Ok(loRes)
ENDPROC

Und fehlt das Token, ist es abgelaufen oder falsch signiert, antwortet die Middleware von selbst:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="foxserver", error="invalid_token",
                  error_description="The access token expired"
Content-Type: application/problem+json; charset=utf-8

{"type":"about:blank","title":"Unauthorized","status":401,
 "detail":"Invalid token","instance":"/profile","code":"unauthorized"}

Der Header WWW-Authenticate ist der aus RFC 6750 und unterscheidet die drei Fälle: ohne vorgelegte Anmeldedaten trägt er kein error=, so wie §3.1 es verlangt. Der Rumpf sind Problem Details (RFC 9457), wie bei jedem Fehler des Motors.

CORS

Beantwortet Preflight-Anfragen mit OPTIONS, bevor sie den Controller erreichen, und setzt die Header in allen Antworten. Eingestellt wird es im Manifest:

{
  "allowedorigins": "https://miapp.com,https://admin.miapp.com",
  "allowedmethods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
  "allowedheaders": "Content-Type,Authorization,X-Requested-With"
}
CORS wird vor der Middleware-Kette aufgelöst und steht deshalb nicht in der Liste der ein- und ausschaltbaren Middlewares. Und allowedorigins ist keine Authentifizierung: wenn Sie MCP oder JSON-RPC ohne Auth veröffentlichen, warnt der Motor beim Start.

Anfrageprotokollierung

Schreibt jede Anfrage in Tagesdateien, in den Ordner, den logging.path nennt. Der Dateiname besteht aus Präfix, Typ und Datum:

logging.path\
  api_Request_20260915.log     bediente Anfragen
  api_Error_20260915.log       Fehler

Das Protokoll des Hosts ist ein anderes: logs\FoxServer_<Datum>.log, im Wurzelordner der Installation, eines pro Tag, bei 10 MB geteilt und nach 30 Tagen gelöscht. Die Stufe stellt FOXSERVER_LOG_LEVEL ein, die Aufbewahrung FOXSERVER_LOG_RETENTION_DAYS.

Die Windows-Ereignisanzeige erhält Ihre Protokolle nicht Auch im Dienstmodus nicht. Dort wird nur dann ein Eintrag geschrieben, wenn das Schreiben der Protokolldatei selbst scheitert, damit ein Protokollfehler nicht unprotokolliert bleibt.

Was Sie selbst schreiben können: die Haken des Controllers

Es gibt keine API, um aus VFP eine Middleware zu schreiben, aber jeder Controller hat zwei Haken, die der Motor um jeden seiner Endpunkte herum aufruft. Dort öffnet man seine Tabellen und schließt sie wieder.

BeforeEndpoint und AfterEndpoint: Datenbank öffnen und schließen
DEFINE CLASS ProdukteController AS ApiController OLEPUBLIC

    * Vor JEDEM Endpunkt dieses Controllers.
    * Ein .F. bricht ab: der Endpunkt laeuft gar nicht erst.
    FUNCTION BeforeEndpoint(req, res)
        SET DELETED ON
        SET EXCLUSIVE OFF
        OPEN DATABASE (THIS.cPath + "data\laden.dbc") SHARED
        RETURN .T.
    ENDFUNC

    * Nach JEDEM Endpunkt, ob er gut ging oder nicht.
    PROCEDURE AfterEndpoint(req, res)
        CLOSE DATABASES ALL
    ENDPROC

ENDDEFINE
Sie gelten je Controller: es gibt keinen „Wurzel-Controller“ mit Haken für das ganze Projekt. Dem ganzen Projekt gehört dagegen der ApplicationLifecycle, der beim Starten und Stoppen des Servers läuft.

Weiter: Lizenzierung →