Sichere APIs mit Token-Authentifizierung und OpenAPI erstellen
Warum ich API-Tokens benötigte
Ich habe eine Web-App mit einem Dashboard zur Verwaltung von Inhalten erstellt. Alles funktionierte einwandfrei über die Benutzeroberfläche, aber ich begann über Automatisierung nachzudenken. Was wäre, wenn ich ChatGPT zur Verwaltung von Inhalten nutzen wollte? Oder wiederkehrende Aufgaben mit Skripten automatisieren?
Ich benötigte eine API mit ordnungsgemäßer Authentifizierung. Etwas Sicheres, einfach zu bedienendes und gut dokumentiertes.
Hier ist, was ich gebaut habe und was ich gelernt habe.
Die Anforderungen
Bevor ich mich in den Code stürzte, habe ich aufgelistet, was ich tatsächlich benötigte:
- Sichere Token-Generierung: Keine erratbaren Muster, kryptografisch sicher
- Einmalige Anzeige: Das Token nur einmal anzeigen, nie wieder (wie bei GitHub)
- Niemals Klartext speichern: Alles hashen, bevor es in die Datenbank gelangt
- Widerrufsunterstützung: Benutzern ermöglichen, nicht mehr benötigte Tokens zu löschen
- Nutzungsverfolgung: Anzeigen, wann und wie Tokens verwendet werden
- OpenAPI-Dokumentation: Es KI-Assistenten leicht machen, die API zu verstehen
- Interaktives Testen: Swagger UI, damit Entwickler Endpunkte ausprobieren können
Klingt nach viel, oder? Lassen Sie mich aufschlüsseln, wie jedes Stück funktioniert.
Teil 1: Sichere Tokens generieren
Die erste Herausforderung bestand darin, Tokens zu erstellen, die sowohl sicher als auch benutzerfreundlich sind. Ich habe mich für dieses Format entschieden:
imk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6Lassen Sie mich jeden Teil erklären:
imk- Mein Produktpräfix (Kurzform für "Imerkar")live- Umgebung (live vs. Test)a1b2c3...- 32 zufällige Zeichen
Dieses Format ist von den API-Schlüsseln von Stripe inspiriert, und das hat einen guten Grund:
- Präfix-Scannen – Wenn jemand versehentlich einen Token in GitHub eincheckt, können Tools dies erkennen, indem sie nach dem Präfix
imk_suchen - Umgebungs-Trennung – Man erkennt auf einen Blick, ob es sich um einen Test- oder Live-Schlüssel handelt
- Benutzerfreundliche Identifizierung – Benutzer können Schlüssel anhand ihres Präfixes identifizieren, auch wenn der vollständige Token verborgen ist
Der Generierungscode
Ich habe die Web Crypto API für die sichere Zufallsgenerierung verwendet:
async function generateApiKey(): Promise<string> {
// 24 Bytes (192 Bit) an Zufallsdaten generieren
const randomBytes = new Uint8Array(24);
crypto.getRandomValues(randomBytes);
// In base64url-Kodierung umwandeln (URL-sicher, keine Auffüllung)
const randomString = btoa(String.fromCharCode(...randomBytes))
.replace(/\+/g, '-')
.replace(/\/g, '_')
.replace(/=/g, '');
return `imk_live_${randomString}`;
}Dies generiert kryptografisch sichere Zufalls-Tokens. Die Funktion crypto.getRandomValues() verwendet die Entropiequelle des Betriebssystems, was die Tokens unvorhersehbar macht. Ich verwende die base64url-Kodierung, um sicherzustellen, dass der Token URL-sicher ist und die volle Entropie beibehält.
Teil 2: Tokens sicher speichern
Hier ist die goldene Regel: Speichern Sie API-Tokens niemals im Klartext.
Stellen Sie es sich wie bei Passwörtern vor. Wenn Ihre Datenbank kompromittiert wird, möchten Sie nicht, dass Angreifer funktionierende API-Schlüssel erhalten. Daher hashen wir die Tokens vor der Speicherung, genau wie bei Passwörtern.
Das Datenbankschema
CREATE TABLE api_keys (
id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL,
name TEXT NOT NULL,
key_prefix TEXT NOT NULL, -- Präfix zur Anzeige (imk_live_xxx)
key_hash TEXT NOT NULL, -- SHA-256 Hash
scopes TEXT NOT NULL, -- JSON-Array der Berechtigungen
last_used_at TEXT, -- ISO 8601 Zeitstempel
last_used_ip TEXT,
expires_at TEXT, -- ISO 8601 Zeitstempel
revoked INTEGER DEFAULT 0,
created_at TEXT NOT NULL -- ISO 8601 Zeitstempel
);
-- Index für schnelle Authentifizierungs-Lookups
CREATE INDEX idx_api_keys_hash ON api_keys(key_hash);
-- Index zum Auflisten der Schlüssel eines Kunden
CREATE INDEX idx_api_keys_customer ON api_keys(customer_id);Beachten Sie, dass ich das key_prefix (den lesbaren Teil wie imk_live_abc) separat speichere. Dies ermöglicht es mir, Benutzern etwas wie Folgendes anzuzeigen:
imk_live_a1b2c3d4... (Erstellt am 15. Jan. 2025)Sie können erkennen, welcher Token welcher ist, ohne den vollständigen Wert zu sehen.
Die Tokens hashen
async function hashApiKey(apiKey: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(apiKey);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}Wenn ein Benutzer einen Token erstellt:
- Zufälligen Token generieren
- Mit SHA-256 hashen
- Hash in der Datenbank speichern
- Vollständigen Token einmalig an den Benutzer zurückgeben!
- Den vollständigen Token nie wieder anzeigen
Teil 3: Authentifizierungs-Middleware
Nachdem wir Tokens generieren und speichern können, müssen wir tatsächlich Anfragen authentifizieren. Ich habe Middleware erstellt, die mehrere Authentifizierungsmethoden unterstützt:
- Clerk JWT-Tokens – Für angemeldete Dashboard-Benutzer
- API-Tokens – Für externe Integrationen
Der Authentifizierungsablauf
async function authenticate(c: Context) {
const authHeader = c.req.header('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return c.json({ error: 'Fehlende Autorisierung' }, 401);
}
const token = authHeader.substring(7);
// API-Schlüssel-Authentifizierung versuchen
if (token.startsWith('imk_')) {
const customerId = await verifyApiKey(token);
if (customerId) {
c.set('customerId', customerId);
return;
}
}
// JWT-Authentifizierung versuchen
const customerId = await verifyJWT(token);
if (customerId) {
c.set('customerId', customerId);
return;
}
return c.json({ error: 'Ungültiger Token' }, 401);
}API-Tokens überprüfen
Wenn ein API-Schlüssel eingeht, passiert Folgendes:
async function verifyApiKey(apiKey: string): Promise<string | null> {
// Den bereitgestellten Schlüssel hashen
const keyHash = await hashApiKey(apiKey);
// Den Hash in der Datenbank nachschlagen
const key = await db.query(
'SELECT customer_id, revoked, expires_at FROM api_keys WHERE key_hash = ?',
[keyHash]
);
if (!key) return null;
if (key.revoked) return null;
if (key.expires_at && new Date(key.expires_at) < new Date()) return null;
// Zeitstempel der letzten Nutzung aktualisieren
await db.execute(
'UPDATE api_keys SET last_used_at = ?, last_used_ip = ? WHERE key_hash = ?',
[new Date().toISOString(), c.req.header('cf-connecting-ip'), keyHash]
);
return key.customer_id;
}Dies ist schnell, da wir die Spalte key_hash indizieren. Die Datenbankabfrage ist dank des Indexes O(1).
Teil 4: Die Dashboard-Benutzeroberfläche
Benutzer benötigen eine Möglichkeit, ihre Tokens zu erstellen und zu verwalten. Ich habe dies in die Einstellungsseite meines Dashboards integriert.
Die Benutzererfahrung
- Navigieren zu Einstellungen → API-Schlüssel
- Auf "Neuen API-Schlüssel erstellen" klicken
- Einen Namen eingeben (z. B. "Zapier-Integration")
- Auf Erstellen klicken
- Den vollständigen Token mit einer großen Warnung sehen:
⚠️ API-Schlüssel erstellt
imk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Speichern Sie diesen Schlüssel jetzt. Sie werden ihn nicht wieder sehen können!- In die Zwischenablage kopieren
- Nach dem Schließen nur noch das Präfix sehen:
Zapier-Integration
imk_live_a1b2c3d4...
Erstellt: 15. Jan. 2025
Zuletzt verwendet: 16. Jan. 2025
[Widerrufen]Die React-Komponente
Hier ist eine vereinfachte Version des Prozesses zur Schlüsselgenerierung:
function ApiKeysTab() {
const [keys, setKeys] = useState([]);
const [showCreateModal, setShowCreateModal] = useState(false);
const [newKey, setNewKey] = useState(null);
async function createKey(name: string) {
const response = await fetch('/api/api-keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
const data = await response.json();
setNewKey(data.key); // Vollständiger Schlüssel, nur einmal angezeigt
setShowCreateModal(false);
}
async function revokeKey(keyId: string) {
await fetch(`/api/api-keys/${keyId}`, { method: 'DELETE' });
loadKeys(); // Liste aktualisieren
}
return (
<>
{newKey && (
<Alert>
<AlertTitle>API-Schlüssel erstellt</AlertTitle>
<AlertDescription>
<code>{newKey}</code>
<p>Jetzt speichern. Sie werden ihn nicht wieder sehen!</p>
</AlertDescription>
</Alert>
)}
{keys.map(key => (
<KeyRow
key={key.id}
name={key.name}
prefix={key.key_prefix}
lastUsed={key.last_used_at}
onRevoke={() => revokeKey(key.id)}
/>
))}
</>
);
}Teil 5: OpenAPI-Dokumentation
Hier wird es richtig cool. Sobald Sie eine API haben, benötigen Sie eine Dokumentation. Aber nicht irgendeine Dokumentation. Maschinenlesbare Dokumentation, die KI-Assistenten verstehen können.
Dafür gibt es OpenAPI (früher Swagger).
Was ist OpenAPI?
OpenAPI ist eine Standardmethode zur Beschreibung von REST-APIs. Es ist eine JSON/YAML-Datei, die Ihnen mitteilt:
- Welche Endpunkte existieren
- Welche Parameter sie akzeptieren
- Was sie zurückgeben
- Wie man sich authentifiziert
- Welche Fehler auftreten können
Das Schöne daran ist, dass ChatGPT, Claude, Zapier und Hunderte anderer Tools OpenAPI-Spezifikationen lesen und Ihre API automatisch verstehen können.
Erstellen des OpenAPI-Dokuments
Ich habe eine OpenAPI 3.0-Spezifikation für meine API erstellt:
const openAPIDocument = {
openapi: '3.0.0',
info: {
title: 'Imerkar API',
version: '1.0.0',
description: 'Websites programmatisch erstellen und verwalten',
},
servers: [
{
url: 'https://app.imerkar.com',
description: 'Produktionsserver',
},
],
components: {
securitySchemes: {
BearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT oder API-Schlüssel',
description: 'Verwenden Sie Ihren API-Schlüssel mit der Bearer-Authentifizierung',
},
},
},
security: [{ BearerAuth: [] }],
paths: {
'/api/pages': {
get: {
summary: 'Alle Seiten auflisten',
tags: ['Seiten'],
responses: {
'200': {
description: 'Liste der Seiten',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
pages: {
type: 'array',
items: { $ref: '#/components/schemas/Page' }
}
}
}
}
}
}
}
},
post: {
summary: 'Neue Seite erstellen',
tags: ['Seiten'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['title'],
properties: {
title: { type: 'string' },
description: { type: 'string' },
}
}
}
}
},
responses: {
'201': {
description: 'Seite erstellt',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Page' }
}
}
}
}
}
}
}
};Das mag ausführlich erscheinen, ist aber unglaublich mächtig. Mit diesem einen Dokument kann jedes Tool Ihre gesamte API verstehen.
Bereitstellen der OpenAPI-Spezifikation
Ich habe zwei Endpunkte erstellt:
// JSON-Spezifikation bereitstellen
app.get('/api/docs/openapi.json', (c) => {
return c.json(openAPIDocument);
});
// YAML-Spezifikation bereitstellen (manche Tools bevorzugen YAML)
app.get('/api/docs/openapi.yaml', (c) => {
const yaml = convertToYAML(openAPIDocument);
return c.text(yaml, 200, {
'Content-Type': 'application/x-yaml'
});
});Teil 6: Swagger UI für interaktives Testen
OpenAPI-Spezifikationen sind großartig, aber Entwickler möchten Ihre API testen, nicht nur darüber lesen. Hier kommt Swagger UI ins Spiel.
Swagger UI ist eine Weboberfläche, die:
- Ihre OpenAPI-Spezifikation liest
- Interaktive Dokumentation generiert
- Entwicklern ermöglicht, Endpunkte direkt im Browser zu testen
- Anzeige von Anfrage-/Antwortbeispielen
Einrichten von Swagger UI
Ich habe eine einfache HTML-Seite erstellt, die Swagger UI von einem CDN lädt:
app.get('/api/docs', (c) => {
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Imerkar API Dokumentation</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.10.3/swagger-ui.css">
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5.10.3/swagger-ui-bundle.js"></script>
<script>
window.onload = function() {
SwaggerUIBundle({
url: '/api/docs/openapi.json',
dom_id: '#swagger-ui',
persistAuthorization: true,
tryItOutEnabled: true,
});
};
</script>
</body>
</html>
`;
return c.html(html);
});Wenn Entwickler nun https://app.imerkar.com/api/docs besuchen, sehen sie einen schönen, interaktiven API-Explorer.
Verwendung von Swagger UI
Der Arbeitsablauf ist einfach:
- Dokumentation öffnen unter
/api/docs - Auf "Autorisieren" klicken
- API-Schlüssel einfügen:
imk_live_... - Auf einen Endpunkt klicken (z. B. GET /api/pages)
- Auf "Try it out" klicken
- Auf "Execute" klicken
- Die tatsächliche Antwort Ihrer API sehen
Es ist wie Postman, aber in Ihre Dokumentation integriert.
Teil 7: Integration mit ChatGPT
Hier fügt sich alles zusammen. Mit einer OpenAPI-Spezifikation können Sie ein benutzerdefiniertes GPT erstellen, das Ihre API versteht.
Erstellen eines benutzerdefinierten GPT
- Zu ChatGPT → GPT erstellen gehen
- Eine Aktion hinzufügen
- Von URL importieren:
https://app.imerkar.com/api/docs/openapi.json - Authentifizierung auf Bearer Token einstellen
- Ihren API-Schlüssel hinzufügen
- Testen:
Sie: "Liste alle meine Seiten auf"
ChatGPT: Lassen Sie mich Ihre Seiten überprüfen...
[Macht GET /api/pages Anfrage]
ChatGPT: "Sie haben 5 Seiten:
1. Startseite
2. Über uns
3. Dienstleistungen
4. Kontakt
5. Blog"
Sie: "Erstelle eine neue Seite namens 'Preise' mit einer Beschreibung unserer Pläne"
ChatGPT: [Macht POST /api/pages Anfrage]
ChatGPT: "Fertig! Ich habe eine neue Seite namens 'Preise' mit Ihrer Beschreibung erstellt."Es funktioniert einfach. ChatGPT liest Ihre OpenAPI-Spezifikation und weiß, wie es Ihre API nutzen kann.
Teil 8: Nutzungsverfolgung und Ratenbegrenzung
Sobald Sie API-Tokens im Umlauf haben, müssen Sie verfolgen, wie sie verwendet werden, und Missbrauch verhindern.
Protokollierung der Nutzung
Ich habe eine einfache Tabelle zur Protokollierung der Nutzung erstellt:
CREATE TABLE api_key_usage_logs (
id TEXT PRIMARY KEY,
api_key_id TEXT NOT NULL,
endpoint TEXT NOT NULL,
method TEXT NOT NULL,
status_code INTEGER NOT NULL,
ip_address TEXT,
created_at TEXT NOT NULL -- ISO 8601 Zeitstempel
);
-- Index für Ratenbegrenzungsabfragen
CREATE INDEX idx_usage_logs_key_time ON api_key_usage_logs(api_key_id, created_at);Jede API-Anfrage wird protokolliert:
async function logApiUsage(keyId: string, req: Request, status: number) {
await db.execute(
`INSERT INTO api_key_usage_logs (id, api_key_id, endpoint, method, status_code, ip_address, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
generateId(),
keyId,
new URL(req.url).pathname,
req.method,
status,
req.headers.get('cf-connecting-ip'),
new Date().toISOString()
]
);
}Ratenbegrenzung
Ich habe eine einfache Ratenbegrenzung implementiert:
- 30 Anfragen pro Minute pro API-Schlüssel
- 1000 Anfragen pro Tag
async function checkRateLimit(keyId: string): Promise<boolean> {
const oneMinuteAgo = new Date(Date.now() - 60000).toISOString();
const count = await db.query(
'SELECT COUNT(*) as count FROM api_key_usage_logs WHERE api_key_id = ? AND created_at > ?',
[keyId, oneMinuteAgo]
);
return count.count < 30;
}Wenn das Limit überschritten wird, wird eine hilfreiche Fehlermeldung zurückgegeben:
{
"error": "Ratenlimit überschritten",
"code": "RATE_LIMIT_EXCEEDED",
"details": {
"limit": 30,
"window": "1 Minute",
"retryAfter": 42
}
}Was ich gelernt habe
Der Aufbau dieser Funktion hat mir viel beigebracht:
1. Sicherheit ist nicht optional
Speichern Sie niemals Klartext-Tokens. Hashen Sie alles. Verwenden Sie crypto.subtle für Zufälligkeit. Überlegen Sie, was passiert, wenn Ihre Datenbank durchsickert.
2. UX ist auch bei Entwickler-Tools wichtig
API-Tokens benötigen auch eine gute UX. Zeigen Sie sie einmal mit einer großen Warnung an. Ermöglichen Sie Benutzern, ihre Tokens zu benennen. Zeigen Sie an, wann sie zuletzt verwendet wurden. Machen Sie den Widerruf einfach.
3. OpenAPI ist eine Superkraft
Das Schreiben einer OpenAPI-Spezifikation fühlt sich zunächst nach Fleißarbeit an, ist aber unglaublich wertvoll. Es ermöglicht die ChatGPT-Integration, Swagger UI, die Generierung von Client-SDKs und die Testautomatisierung.
4. Einfach anfangen, iterieren
Ich habe nicht alles auf einmal gebaut. Version 1 bestand nur aus Token-Generierung und Authentifizierung. Dann habe ich die Dashboard-Benutzeroberfläche hinzugefügt. Dann OpenAPI-Dokumente. Dann Nutzungsverfolgung. Jede Iteration hat es verbessert.
5. Datenbankindizes sind entscheidend
Das Indizieren der Spalte key_hash macht die Authentifizierung schnell. Ohne sie würde jede Anfrage die gesamte Tabelle durchsuchen. Mit ihr sind Lookups sofort möglich.
Der Tech-Stack
Als Referenz habe ich Folgendes verwendet:
- Backend: Cloudflare Workers + Hono-Framework
- Datenbank: Cloudflare D1 (SQLite)
- Frontend: React + Tailwind CSS
- Auth (Sitzungen): Clerk
- OpenAPI: Handgeschriebene Spezifikation (könnte @hono/zod-openapi für Typsicherheit verwenden)
- Sprache: TypeScript
Nächste Schritte
Es gibt noch mehr, was ich hinzufügen möchte:
- Umfangsbasierten Berechtigungen – Im Moment haben Tokens vollen Zugriff. Ich möchte Bereiche wie
pages:read,pages:write,media:readusw. hinzufügen. - Webhook-Unterstützung – Benutzern ermöglichen, sich für Ereignisse anzumelden (page.created, page.published usw.)
- SDK-Generierung – Die OpenAPI-Spezifikation verwenden, um automatisch Client-Bibliotheken in Python, JavaScript, PHP usw. zu generieren
- Bessere Analysen – Detailliertere Nutzungsstatistiken mit Diagrammen und Trends
- IP-Whitelisting – Benutzern erlauben, Tokens auf bestimmte IP-Adressen zu beschränken
Ressourcen
Wenn Sie etwas Ähnliches erstellen, haben mir diese Ressourcen geholfen: