KI-Chatbot mit Google Gemini
Hier ist ein funktionierender Chatbot, den ich mit der API von Google Gemini erstellt habe. Probier ihn gerne aus!
1. AI Chatbot zum Ausprobieren
So habe ich das gemacht
Dieses Projekt war eine spannende Reise in die Welt der künstlichen Intelligenz und der API-Sicherheit. Mein Ziel war es, nicht nur einen funktionierenden Chatbot zu bauen, sondern auch zu lernen, wie man sensible Daten wie einen privaten API-Schlüssel schützt, besonders wenn das Projekt auf einer öffentlichen Plattform wie GitHub Pages liegt.
Zusätzlich habe ich Sicherheitsmaßnahmen implementiert, um Missbrauch zu verhindern: einen Origin-Check, der nur Anfragen von meiner Website erlaubt, und ein Rate-Limiting von 20 Anfragen pro Minute pro Nutzer.
1. Die Benutzeroberfläche (Frontend)
Das Chatfenster selbst ist mit einfachem HTML und CSS aufgebaut. Ich habe eine aufgeräumte Struktur mit einem Anzeigebereich für die Nachrichten und einem Eingabefeld mit Sende-Button gewählt. Mit CSS habe ich die Nachrichten des Benutzers und des Bots unterschiedlich gestaltet, damit die Konversation leicht zu verfolgen ist.
Das JavaScript im Frontend hat zwei Hauptaufgaben:
- Es fängt die Eingaben des Nutzers ab, zeigt die Nachricht sofort im Chatfenster an und leert das Eingabefeld.
- Es sendet die Nutzernachricht an meine eigene "Proxy"-URL (mehr dazu im nächsten Abschnitt) und wartet auf die Antwort. Sobald die Antwort eintrifft, wird sie ebenfalls im Chatfenster angezeigt.
2. Das Kernproblem: Der private API-Schlüssel
Jede Anfrage an die Google AI API benötigt einen privaten API-Schlüssel. Würde ich diesen Schlüssel direkt in mein JavaScript auf der Webseite schreiben, könnte ihn jeder Besucher meiner Seite sehen, kopieren und auf meine Kosten benutzen. Das ist ein großes Sicherheitsrisiko.
Die Lösung ist ein Proxy-Server. Man kann sich das wie einen Vermittler vorstellen. Meine Webseite spricht nicht direkt mit Google, sondern nur mit meinem Vermittler. Dieser Vermittler nimmt die Anfrage entgegen, fügt den geheimen API-Schlüssel hinzu und leitet sie dann an Google weiter. So bleibt der Schlüssel immer geheim auf dem Server und ist niemals öffentlich sichtbar.
3. Die Lösung: Ein Cloudflare Worker als Proxy
Einen eigenen Server zu betreiben wäre für dieses Projekt übertrieben. Eine viel einfachere und kostenlose Lösung sind "Serverless Functions". Ich habe mich für Cloudflare Workers entschieden, ein JavaScript-Programm, das auf den Servern von Cloudflare läuft.
Ein großer Vorteil von Cloudflare Workers ist die Verwendung von Environment Variables (Umgebungsvariablen). Diese erlauben es, sensible Daten wie den API-Schlüssel GEMINI_API_KEY und die Liste der erlaubten Domains ALLOWED_ORIGINS sicher zu speichern, ohne sie im Code zu hinterlegen. Das bedeutet, ich kann den Code hier öffentlich teilen, ohne dass jemand meinen persönlichen API-Schlüssel sieht oder missbraucht. Die Variablen werden sicher in der Cloudflare Console verwaltet und sind nur für mich einsehbar.
Der Worker-Code
// Einfacher In-Memory Store für Rate-Limiting (wird bei Neustarts zurückgesetzt)
let requestStore = {};
export default {
async fetch(request, env) {
// CORS Preflight-Anfrage (OPTIONS) behandeln
if (request.method === 'OPTIONS') {
return handleOptions(request, env);
}
// Origin-Check
// Erlaubte Domains aus Environment Variables lesen
const allowedOrigins = env.ALLOWED_ORIGINS ? env.ALLOWED_ORIGINS.split(',').map(o => o.trim()) : [];
const origin = request.headers.get('Origin');
// Prüfe ob Origin erlaubt ist (nur wenn Origins konfiguriert sind)
if (origin && allowedOrigins.length > 0 && !allowedOrigins.includes(origin)) {
return new Response('Unerlaubte Herkunft', {
status: 403,
headers: getCorsHeaders(env, origin, allowedOrigins)
});
}
// Nur POST-Anfragen für die Chat-Logik erlauben
if (request.method !== 'POST') {
return new Response('Methode nicht erlaubt', {
status: 405,
headers: getCorsHeaders(env, origin, allowedOrigins)
});
}
// Einfaches Rate-Limiting ohne KV
const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown';
const now = Date.now();
// Bereinige alte Einträge (älter als 1 Minute)
for (const [ip, data] of Object.entries(requestStore)) {
if (now - data.timestamp > 60000) {
delete requestStore[ip];
}
}
// Zähle Anfragen dieser IP in den letzten 60 Sekunden
let requestCount = 0;
for (const [ip, data] of Object.entries(requestStore)) {
if (ip === clientIP && now - data.timestamp < 60000) {
requestCount += data.count;
}
}
// Prüfe das Limit (20 Anfragen pro Minute)
if (requestCount >= 20) {
return new Response(JSON.stringify({
error: 'Rate limit exceeded. Bitte versuche es in einer Minute erneut.'
}), {
status: 429,
headers: {
'Content-Type': 'application/json',
...getCorsHeaders(env, origin, allowedOrigins)
},
});
}
// Speichere diese Anfrage
if (!requestStore[clientIP]) {
requestStore[clientIP] = { count: 1, timestamp: now };
} else {
requestStore[clientIP].count += 1;
requestStore[clientIP].timestamp = now;
}
// Hauptlogik für die Chat-Nachricht
try {
const { message } = await request.json();
if (!message) {
return new Response(JSON.stringify({ error: 'Nachricht fehlt im Body' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
...getCorsHeaders(env, origin, allowedOrigins)
},
});
}
const googleApiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${env.GEMINI_API_KEY}`;
const apiResponse = await fetch(googleApiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: message }] }],
}),
});
if (!apiResponse.ok) {
const errorData = await apiResponse.json();
return new Response(JSON.stringify({ error: `Fehler von der Google API: ${errorData.error?.message || 'Unbekannter Fehler'}` }), {
status: apiResponse.status,
headers: {
'Content-Type': 'application/json',
...getCorsHeaders(env, origin, allowedOrigins)
},
});
}
const apiData = await apiResponse.json();
if (!apiData.candidates || apiData.candidates.length === 0) {
const blockReason = apiData.promptFeedback?.blockReason || 'Unbekannter Grund';
return new Response(JSON.stringify({ reply: `Ich kann darauf leider nicht antworten. Grund: ${blockReason}` }), {
headers: {
'Content-Type': 'application/json',
...getCorsHeaders(env, origin, allowedOrigins)
},
});
}
const botReply = apiData.candidates[0].content.parts[0].text;
return new Response(JSON.stringify({ reply: botReply }), {
headers: {
'Content-Type': 'application/json',
...getCorsHeaders(env, origin, allowedOrigins)
},
});
} catch (error) {
return new Response(JSON.stringify({ error: 'Ein interner Fehler im Worker ist aufgetreten.' }), {
status: 500,
headers: {
'Content-Type': 'application/json',
...getCorsHeaders(env, origin, allowedOrigins)
},
});
}
},
};
function getCorsHeaders(env, origin, allowedOrigins = null) {
if (!allowedOrigins) {
allowedOrigins = env.ALLOWED_ORIGINS ? env.ALLOWED_ORIGINS.split(',').map(o => o.trim()) : [];
}
// Wenn keine Origins konfiguriert sind, erlaube alle (nur für Entwicklung!)
if (allowedOrigins.length === 0) {
return {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
}
// Prüfe ob die anfragende Origin erlaubt ist
const allowOrigin = origin && allowedOrigins.includes(origin) ? origin : allowedOrigins[0];
return {
'Access-Control-Allow-Origin': allowOrigin,
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
}
function handleOptions(request, env) {
const origin = request.headers.get('Origin');
const allowedOrigins = env.ALLOWED_ORIGINS ? env.ALLOWED_ORIGINS.split(',').map(o => o.trim()) : [];
if (
origin &&
request.headers.get('Access-Control-Request-Method') !== null &&
request.headers.get('Access-Control-Request-Headers') !== null
) {
return new Response(null, {
headers: getCorsHeaders(env, origin, allowedOrigins)
});
} else {
return new Response(null, {
headers: {
'Allow': 'POST, OPTIONS',
...getCorsHeaders(env, origin, allowedOrigins)
}
});
}
}
Mein Worker-Skript erledigt die folgenden Schritte:
- Es wartet auf eine Anfrage (Fetch-Request) von meiner Webseite.
- Es prüft, ob die Anfrage von einer erlaubten Domain kommt (Origin-Check).
- Es überprüft, ob der Nutzer das Limit von 20 Anfragen pro Minute überschritten hat (Rate-Limiting).
- Es nimmt die Nachricht des Nutzers aus dieser Anfrage entgegen.
- Es holt den geheimen Google AI API-Schlüssel, den ich sicher in den Cloudflare-Einstellungen hinterlegt habe.
- Es schickt eine neue Anfrage, diesmal an die echte Google AI API, inklusive der Nutzernachricht und des geheimen Schlüssels.
- Es empfängt die Antwort von Google und leitet sie direkt an meine Webseite zurück.
Dieser ganze Prozess schützt meinen API-Schlüssel perfekt und sorgt dafür, dass die Kommunikation sicher abläuft.
4. Sicherheitsmaßnahmen
Um Missbrauch zu verhindern, habe ich zwei wichtige Sicherheitsmaßnahmen implementiert:
- Origin-Check: Der Worker akzeptiert nur Anfragen von meiner Portfolio-Website und blockiert alle anderen Domains.
- Rate-Limiting: Jeder Nutzer ist auf 20 Anfragen pro Minute beschränkt, um excessive Nutzung und Missbrauch zu verhindern.
Das Rate-Limiting wurde mit einem speziellen Algorithmus implementiert, der ohne teure Cloudflare KV-Datenbanken auskommt und trotzdem im Free Plan funktioniert.
5. Zusammenspiel von allem
Der fertige Ablauf sieht also so aus:
- Du gibst eine Nachricht in das Chatfenster ein und klickst auf "Senden".
- JavaScript (Webseite) zeigt deine Nachricht an und schickt sie an die URL meines Cloudflare Workers.
- Cloudflare Worker (Proxy) prüft die Herkunft der Anfrage und das Rate-Limit.
- Wenn alles in Ordnung ist, empfängt der Worker die Nachricht, fügt den geheimen API-Schlüssel hinzu und fragt bei der Google Gemini API an.
- Google Gemini API verarbeitet die Anfrage und sendet die KI-generierte Antwort zurück an den Worker.
- Cloudflare Worker leitet die Antwort an meine Webseite weiter.
- JavaScript (Webseite) empfängt die Antwort und zeigt die Nachricht des Bots im Chatfenster an.
Dieses Projekt war eine tolle Übung, um zu verstehen, wie Frontend und ein sicheres Backend zusammenarbeiten, um eine vollständige und sichere Anwendung zu erstellen.