Zum Hauptinhalt springen

Authentifizierung zu deiner Supabase-Anwendung hinzufügen

Supabase-Grundlagen

Supabase nutzt Postgres' Row-Level Security, um Datenzugriffsberechtigungen zu steuern. Einfach ausgedrückt, können wir durch das Erstellen von Row-Level-Security-Richtlinien für Tabellen in der Datenbank einschränken und verwalten, wer Daten in einer Tabelle lesen, schreiben und aktualisieren kann.

Angenommen, du hast eine Tabelle namens "posts" in deiner Datenbank mit folgendem Inhalt:

Posts-Tabelle

Das user_id-Feld in der Tabelle repräsentiert den Benutzer, dem die jeweiligen Post-Daten gehören. Du kannst einschränken, dass jeder Benutzer nur auf seine eigenen Post-Daten basierend auf dem user_id-Feld zugreifen kann.

Bevor dies jedoch implementiert werden kann, muss Supabase in der Lage sein, den aktuellen Benutzer zu identifizieren, der auf die Datenbank zugreift.

Benutzerdaten zu den Supabase-Anfragen hinzufügen

Dank der Unterstützung von JWT durch Supabase können wir, wenn unsere Anwendung mit Supabase interagiert, ein JWT generieren, das Benutzerdaten enthält, indem wir das von Supabase bereitgestellte JWT-Geheimnis verwenden. Wir verwenden dieses JWT dann als Authentifizierungsheader bei Anfragen. Bei Erhalt der Anfrage überprüft Supabase automatisch die Gültigkeit des JWT und erlaubt den Zugriff auf die darin enthaltenen Daten während der nachfolgenden Prozesse.

Zuerst können wir das von Supabase bereitgestellte JWT-Geheimnis aus den "Projekteinstellungen" im Supabase-Dashboard erhalten:

Supabase API-Einstellungen-Seite

Dann, wenn wir das Supabase SDK verwenden, um Anfragen an Supabase zu stellen, nutzen wir dieses Geheimnis, um unser JWT zu generieren und es als Authentifizierungsheader an die Anfrage anzuhängen. (Bitte beachte, dass dieser Prozess innerhalb des Backend-Dienstes deiner Anwendung stattfindet und das JWT-Geheimnis niemals Dritten offengelegt werden sollte).

import { createClient } from '@supabase/supabase-js';
import { sign } from 'jsonwebtoken';

/*
* Hinweis:
* Du kannst die SUPABASE_URL, SUPABASE_ANON_KEY an derselben Stelle finden, an der du das JWT-Geheimnis findest.
*/
const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY;

const SUPABASE_JWT_SECRET = process.env.SUPABASE_JWT_SECRET;

export const getSupabaseClient = (userId) => {
const jwtPayload = {
userId,
};

const jwt = sign(jwtPayload, SUPABASE_JWT_SECRET, {
expiresIn: '1h', // Nur zur Demonstration
});

const client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
global: {
headers: {
Authorization: `Bearer ${jwt}`,
},
},
});

return client;
};

Navigiere als Nächstes zum SQL-Editor im Supabase-Dashboard und erstelle eine Funktion, um die userId abzurufen, die in der Anfrage übermittelt wird:

Funktion zum Abrufen der Benutzer-ID erstellen

Der im Bild verwendete Code lautet wie folgt:

create or replace function auth.user_id() returns text as $$
select nullif(current_setting('request.jwt.claims', true)::json->>'userId', '')::text;
$$ language sql stable;

Wie der Code zeigt, kannst du in Supabase die Nutzlast des von uns generierten JWT abrufen, indem du request.jwt.claims aufrufst. Das userId-Feld innerhalb der Nutzlast ist der von uns gesetzte Wert.

Mit dieser Funktion kann Supabase den Benutzer bestimmen, der derzeit auf die Datenbank zugreift.

Row-Level-Security-Richtlinie erstellen

Als Nächstes können wir eine Row-Level-Security-Richtlinie erstellen, um jeden Benutzer darauf zu beschränken, nur auf seine eigenen Post-Daten basierend auf dem user_id-Feld in der Posts-Tabelle zuzugreifen.

  1. Navigiere zur Tabellen-Editor-Seite im Supabase-Dashboard und wähle die Posts-Tabelle aus.
  2. Klicke oben in der Tabelle auf "RLS-Richtlinie hinzufügen".
  3. Klicke im angezeigten Fenster auf "Richtlinie erstellen".
  4. Gib einen Richtliniennamen ein und wähle den SELECT-Richtlinienbefehl.
  5. Gib im using-Block des folgenden Codes ein:
auth.user_id() = user_id

RLS-Richtlinie erstellen

Durch die Nutzung solcher Richtlinien wird die Datenzugriffskontrolle innerhalb von Supabase erreicht.

In realen Anwendungen würdest du verschiedene Richtlinien erstellen, um Benutzeraktionen wie das Einfügen und Ändern von Daten einzuschränken. Dies liegt jedoch außerhalb des Umfangs dieses Artikels. Für weitere Informationen zur Row-Level-Security (RLS) siehe Sichere deine Daten mit Postgres Row Level Security.

Grundlegender Integrationsprozess mit Logto

Wie bereits erwähnt, da Supabase RLS für seine Zugriffskontrolle nutzt, liegt der Schlüssel zur Integration mit Logto (oder einem anderen Authentifizierungsdienst) darin, die Benutzer-ID des autorisierten Benutzers zu erhalten und an Supabase zu senden. Der gesamte Prozess wird im folgenden Diagramm veranschaulicht:

Als Nächstes erklären wir, wie Logto basierend auf diesem Prozessdiagramm mit Supabase integriert wird.

Logto-Integration

Logto bietet Integrationsanleitungen für verschiedene Frameworks und Programmiersprachen.

Im Allgemeinen fallen Apps, die mit diesen Frameworks und Sprachen erstellt wurden, in Kategorien wie Native Apps, SPA (Single-Page-Apps), traditionelle Web-Apps und M2M (Maschine-zu-Maschine)-Apps. Du kannst die Logto-Schnellstartseite besuchen, um Logto basierend auf dem von dir verwendeten Tech-Stack in deine Anwendung zu integrieren. Anschließend folge den unten stehenden Anweisungen, um Logto basierend auf dem Typ deiner Anwendung in dein Projekt zu integrieren.

Native App oder SPA

Sowohl Native Apps als auch SPAs laufen auf deinem Gerät, und die nach der Anmeldung erhaltenen Anmeldeinformationen (Zugangstoken) werden lokal auf deinem Gerät gespeichert.

Daher musst du bei der Integration deiner App mit Supabase über deinen Backend-Dienst mit Supabase interagieren, da du keine sensiblen Informationen (wie das Supabase JWT-Geheimnis) auf dem Gerät jedes Benutzers offenlegen kannst.

Angenommen, du erstellst deine SPA mit React und Express. Du hast Logto erfolgreich in deine Anwendung integriert, indem du der Logto React SDK-Anleitung gefolgt bist (du kannst den Code in unserem React-Beispiel einsehen). Zusätzlich hast du die Logto-Zugangstoken-Validierung zu deinem Backend-Server gemäß der Schütze deine API auf Node (Express) Dokumentation hinzugefügt.

Als Nächstes verwendest du das von Logto erhaltene Zugangstoken, um Benutzerdaten von deinem Backend-Server anzufordern:

import { useLogto } from '@logto/react';
import { useState, useEffect } from 'react';
import PostList from './PostList';

const endpoint = '<https://www.mysite.com/api/posts>';
const resource = '<https://www.mysite.com/api>';

function PostPage() {
const { isAuthenticated, getAccessToken } = useLogto();
const [posts, setPosts] = useState();

useEffect(() => {
const fetchPosts = async () => {
const response = await fetch(endpoint, {
headers: {
Authorization: `Bearer ${await getAccessToken(resource)}`,
},
});
setPosts(response.json());
};

if (isAuthenticated) {
void fetchPosts();
}
}, [isAuthenticated, getAccessToken]);

return <PostList posts={posts} />;
}

export default PostPage;

Auf deinem Backend-Server hast du bereits die ID des angemeldeten Benutzers aus dem Zugangstoken mithilfe von Middleware extrahiert:

// auth-middleware.ts
import { createRemoteJWKSet, jwtVerify } from 'jose';

//...

export const verifyAuthFromRequest = async (ctx, next) => {
// Extrahiere das Token
const token = extractBearerTokenFromHeaders(ctx.request.headers);

const { payload } = await jwtVerify(
token, // Das rohe Bearer-Token, das aus dem Anfrage-Header extrahiert wurde
createRemoteJWKSet(new URL('https://<your-logto-domain>/oidc/jwks')), // Erzeuge ein jwks mithilfe der jwks_uri, die vom Logto-Server abgefragt wurde
{
// Erwarteter Aussteller des Tokens, sollte vom Logto-Server ausgestellt werden
issuer: 'https://<your-logto-domain>/oidc',
// Erwartetes Zielgruppen-Token, sollte der Ressourcenindikator der aktuellen API sein
audience: '<your request listener resource indicator>',
}
);

// wenn du RBAC verwendest
assert(payload.scope.includes('some_scope'));

// benutzerdefinierte Nutzlastlogik
ctx.auth = {
userId: payload.sub,
};

return next();
};

Jetzt kannst du den oben beschriebenen getSupabaseClient verwenden, um die userId an das JWT anzuhängen, das in nachfolgenden Anfragen an Supabase verwendet wird. Alternativ kannst du eine Middleware erstellen, um einen Supabase-Client für Anfragen zu erstellen, die mit Supabase interagieren müssen:

export const withSupabaseClient = async (ctx, next) => {
ctx.supabase = getSupabaseClient(ctx.auth.userId);

return next();
};

Im nachfolgenden Verarbeitungsfluss kannst du direkt ctx.supabase aufrufen, um mit Supabase zu interagieren:

const fetchPosts = async (ctx) => {
const { data } = await ctx.supabase.from('posts').select('*');

return data;
};

In diesem Code gibt Supabase nur die Post-Daten zurück, die dem aktuellen Benutzer basierend auf den zuvor festgelegten Richtlinien gehören.

Traditionelle Web-App

Der Hauptunterschied zwischen einer traditionellen Web-App und einer Native App oder SPA besteht darin, dass eine traditionelle Web-App Seiten ausschließlich auf dem Webserver rendert und aktualisiert. Daher werden Benutzeranmeldeinformationen direkt vom Webserver verwaltet, während sie in Native Apps und SPAs auf dem Gerät des Benutzers gespeichert sind.

Bei der Integration von Logto mit einer traditionellen Web-App in Supabase kannst du die ID des angemeldeten Benutzers direkt vom Backend abrufen.

Nehmen wir ein Next.js-Projekt als Beispiel: Nachdem du Logto gemäß der Next.js SDK-Anleitung in dein Projekt integriert hast, kannst du das Logto SDK verwenden, um Benutzerinformationen abzurufen und das entsprechende JWT für die Interaktion mit Supabase zu erstellen.

import { getLogtoContext } from '@logto/next-server-actions';
import { logtoConfig } from '@/logto';
import { getSupabaseClient } from '@/utils';
import PostList from './PostList';

export default async function PostPage() {
const { claims } = await getLogtoContext(logtoConfig);

// Der `sub`-Wert in `claims` ist die Benutzer-ID.
const supabase = getSupabaseClient(claims.sub);

const { data: posts } = await supabase.from('posts').select('*');

return <PostList posts={posts} />;
}

Maschine-zu-Maschine-App

Maschine-zu-Maschine (M2M) wird häufig verwendet, wenn deine App direkt mit Ressourcendiensten kommunizieren muss, wie z. B. ein statischer Dienst, der tägliche Posts abruft usw.

Du kannst die Maschine-zu-Maschine: Auth mit Logto Anleitung für die Authentifizierung von Maschine-zu-Maschine-Apps verwenden. Die Integration zwischen Supabase und Maschine-zu-Maschine-Apps ähnelt der von Native Apps und SPAs (wie im Abschnitt "Native App oder SPA" beschrieben). Es geht darum, ein Zugangstoken von Logto zu erhalten und es dann über eine geschützte Backend-API zu validieren.

Es ist jedoch wichtig zu beachten, dass Native Apps und SPAs typischerweise für Endbenutzer konzipiert sind, sodass die erhaltene Benutzer-ID den Benutzer selbst repräsentiert. Das Zugangstoken für Maschine-zu-Maschine-Apps repräsentiert jedoch die Anwendung selbst, und das sub-Feld in der Nutzlast des Zugangstokens ist die Client-ID der M2M-App, nicht ein spezifischer Benutzer. Daher ist es während der Entwicklung entscheidend, zu unterscheiden, welche Daten für M2M-Apps bestimmt sind.

Darüber hinaus, wenn du möchtest, dass eine bestimmte M2M-App im Namen des gesamten Dienstes auf Supabase zugreift, um RLS-Einschränkungen zu umgehen, kannst du das service_role-Geheimnis von Supabase verwenden, um einen Supabase-Client zu erstellen. Dies ist nützlich, wenn du einige administrative oder automatisierte Aufgaben ausführen möchtest, die Zugriff auf alle Daten erfordern, ohne durch die für einzelne Benutzer festgelegten Row-Level-Security-Richtlinien eingeschränkt zu werden.

Das service_role-Geheimnis findest du auf derselben Seite wie das JWT-Geheimnis:

Service-Rolle-Geheimnis

Beim Erstellen eines Supabase-Clients verwende das service_role-Geheimnis, dann kann dieser Client auf alle Daten in der Datenbank zugreifen:

import { createClient } from '@supabase/supabase-js';

// ...
const SUPABASE_SERVICE_ROLE_SECRET = process.env.SUPABASE_SERVICE_ROLE_SECRET;

const client = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_SECRET, {
// ...Optionen
});