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"
}
}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 Route | Muss mit middleware.auth.loginendpoint übereinstimmen und im HELP public tragen |
| Der Rumpf | Darf nicht leer sein |
X-Auth-Status | Genau "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 data | Drei Pflichtfelder: id, name und role. Fehlt eines oder ist es leer, gibt es kein Token |
Alles Übrige in data | Reist als zusätzlicher Claim im Token und wird mit req.GetClaim() gelesen |
| Auf diesem Endpunkt | this.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:
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
ENDPROCEin geschützter Endpunkt: einfach public weglassen
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)
ENDPROCUnd 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"
}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 FehlerDas 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.
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.
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
ENDDEFINEApplicationLifecycle, der beim Starten und Stoppen des Servers läuft. Weiter: Lizenzierung →