За годы своей практики я понял одну важную вещь: правильно реализованная авторизация — фундамент любого серьезного проекта. И это один из тех компонентов, который разработчики часто недооценивают, пока не столкнутся с проблемами в продакшене. Сегодня хочу рассказать о Supabase — инструменте, который существенно упрощает внедрение надежной авторизации в современные веб-приложения.
Когда я впервые начал использовать Supabase для авторизации, был приятно удивлен сочетанием гибкости и простоты. Но как оказалось, под капотом скрывается множество нюансов, которые требуют особого внимания. Давайте разберемся со всеми тонкостями профессиональной реализации авторизации на Supabase.
Основы Supabase Auth и почему это мощный выбор
Supabase — это открытая альтернатива Firebase, предлагающая комбинацию PostgreSQL, аутентификации, хранилища файлов и функций. Ключевая особенность платформы — производительный PostgreSQL с рядом расширений, среди которых — встроенная система аутентификации, основанная на JWT-токенах.
Я выбрал Supabase для нескольких последних проектов, и вот почему:
- Открытый исходный код — можно самостоятельно хостить и модифицировать
- PostgreSQL вместо NoSQL — надежная реляционная модель с транзакциями и схемами
- Row-Level Security (RLS) — детальный контроль доступа на уровне строк БД
- Множество методов аутентификации — от почты/пароля до OAuth провайдеров
- Готовые компоненты интерфейса — но с возможностью полной кастомизации
- Серверные функции на Edge — для безопасной обработки чувствительной логики
От теории к практике: пошаговая интеграция Supabase Auth
Хватит теории, давайте писать код. Для примера я буду использовать React, но принципы применимы к любому современному фреймворку.
1. Настройка проекта и базовая интеграция
Начнем с инициализации Supabase клиента:
// lib/supabase.js
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error('Отсутствуют переменные окружения для Supabase')
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
Обратите внимание на проверку наличия переменных окружения — мелочь, но многие забывают об этом, получая странные ошибки в процессе разработки.
2. Регистрация и вход с помощью почты/пароля
Теперь реализуем формы авторизации. Важный момент — не усложняйте интерфейс на старте. Вот минимальная реализация формы регистрации:
// components/SignUpForm.jsx
import { useState } from 'react'
import { supabase } from '../lib/supabase'
export function SignUpForm() {
const [loading, setLoading] = useState(false)
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
async function handleSignUp(e) {
e.preventDefault()
setError(null)
setLoading(true)
try {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
// Чтобы сразу не закидывать пользователя в приложение
emailRedirectTo: `${window.location.origin}/auth/callback`,
}
})
if (error) throw error
// Сообщаем о необходимости подтверждения
if (data) {
alert('Проверьте вашу почту для подтверждения аккаунта')
}
} catch (error) {
setError(error.message)
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSignUp}>
<h2>Регистрация</h2>
{error && <div className="error">{error}</div>}
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Пароль</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Подождите...' : 'Зарегистрироваться'}
</button>
</form>
)
}
Аналогично реализуем форму входа:
// components/SignInForm.jsx
import { useState } from 'react'
import { supabase } from '../lib/supabase'
export function SignInForm() {
const [loading, setLoading] = useState(false)
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
async function handleSignIn(e) {
e.preventDefault()
setError(null)
setLoading(true)
try {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) throw error
// Успешный вход - можно редиректить на защищенную страницу
// или обновлять состояние приложения
window.location.href = '/dashboard'
} catch (error) {
setError(error.message)
} finally {
setLoading(false)
}
}
// Форма аналогична форме регистрации
return (
<form onSubmit={handleSignIn}>
{/* ... */}
</form>
)
}
3. Обработка состояния авторизации в приложении
Одна из ключевых задач — отслеживание состояния авторизации. Мой подход:
// contexts/AuthContext.jsx
import { createContext, useContext, useEffect, useState } from 'react'
import { supabase } from '../lib/supabase'
const AuthContext = createContext({})
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
// Получаем текущего пользователя
const getCurrentUser = async () => {
const { data: sessionData } = await supabase.auth.getSession()
if (sessionData?.session) {
const { data } = await supabase.auth.getUser()
setUser(data.user)
}
setLoading(false)
}
// Слушаем изменения авторизации
const { data: authListener } = supabase.auth.onAuthStateChange(
async (event, session) => {
console.log(`Auth event: ${event}`)
if (session?.user) {
setUser(session.user)
} else {
setUser(null)
}
setLoading(false)
}
)
getCurrentUser()
// Очистка слушателя
return () => {
authListener?.subscription?.unsubscribe()
}
}, [])
return (
<AuthContext.Provider value={{ user, loading }}>
{children}
</AuthContext.Provider>
)
}
export const useAuth = () => useContext(AuthContext)
Теперь мы можем оборачивать наше приложение в AuthProvider
и получать доступ к состоянию авторизации через хук useAuth()
.
Расширенные возможности и особенности безопасности
Здесь мы переходим к самому интересному — тем аспектам, которые отличают любительскую реализацию от профессиональной. Я, Богдан Новотарский, на этом набил не одну шишку.
Row-Level Security — ключ к безопасности данных
RLS — это механизм PostgreSQL, который позволяет контролировать доступ на уровне отдельных строк. Supabase делает настройку RLS максимально удобной.
Например, вот политика, которая гарантирует, что пользователи могут видеть только свои задачи:
-- Создаем таблицу
CREATE TABLE tasks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES auth.users(id) NOT NULL,
title TEXT NOT NULL,
description TEXT,
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Включаем RLS
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
-- Политика для SELECT
CREATE POLICY "Пользователи видят только свои задачи"
ON tasks FOR SELECT
USING (auth.uid() = user_id);
-- Политика для INSERT
CREATE POLICY "Пользователи могут создавать только свои задачи"
ON tasks FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Политика для UPDATE
CREATE POLICY "Пользователи могут обновлять только свои задачи"
ON tasks FOR UPDATE
USING (auth.uid() = user_id);
-- Политика для DELETE
CREATE POLICY "Пользователи могут удалять только свои задачи"
ON tasks FOR DELETE
USING (auth.uid() = user_id);
Теперь при любых операциях с таблицей tasks
через API Supabase система автоматически применит фильтрацию по текущему пользователю.
Обработка токенов и рефреш сессий
Supabase использует JWT (JSON Web Tokens) для авторизации. По умолчанию токен доступа действует 1 час, а рефреш-токен — 1 неделю. Клиентская библиотека Supabase автоматически обновляет токены, но важно понимать, как это работает:
// pages/_app.js или другая точка входа
import { useEffect } from 'react'
import { supabase } from '../lib/supabase'
function MyApp({ Component, pageProps }) {
useEffect(() => {
const setupRefreshTokenTimer = () => {
// Проверяем и обновляем токен каждые 45 минут
// (меньше времени жизни токена - 1 час)
const REFRESH_INTERVAL = 45 * 60 * 1000
const refreshTimer = setInterval(async () => {
const { error } = await supabase.auth.refreshSession()
if (error) {
console.error('Ошибка обновления сессии:', error)
} else {
console.log('Сессия успешно обновлена')
}
}, REFRESH_INTERVAL)
return () => clearInterval(refreshTimer)
}
// Запускаем таймер только если у нас есть активная сессия
supabase.auth.getSession().then(({ data }) => {
if (data?.session) {
return setupRefreshTokenTimer()
}
})
}, [])
return <Component {...pageProps} />
}
Социальные провайдеры и OAuth
Интеграция с социальными сетями через Supabase — одна из самых простых частей:
// components/SocialAuth.jsx
import { supabase } from '../lib/supabase'
export function SocialAuth() {
async function signInWithProvider(provider) {
try {
const { error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${window.location.origin}/auth/callback`,
// Дополнительные скоупы, если нужны
scopes: provider === 'github' ? 'repo' : '',
}
})
if (error) throw error
} catch (error) {
console.error('Ошибка OAuth авторизации:', error)
alert(error.message)
}
}
return (
<div className="social-auth">
<button onClick={() => signInWithProvider('google')}>
Войти через Google
</button>
<button onClick={() => signInWithProvider('github')}>
Войти через GitHub
</button>
{/* Другие провайдеры... */}
</div>
)
}
Защищенные маршруты в клиентском приложении
В React или Next.js вы можете создать компонент для защиты приватных страниц:
// components/ProtectedRoute.jsx
import { useRouter } from 'next/router'
import { useAuth } from '../contexts/AuthContext'
export function ProtectedRoute({ children }) {
const { user, loading } = useAuth()
const router = useRouter()
useEffect(() => {
if (!loading && !user) {
// Сохраняем страницу, на которую пытался попасть пользователь
const returnUrl = encodeURIComponent(router.asPath)
router.push(`/login?returnUrl=${returnUrl}`)
}
}, [user, loading])
// Показываем загрузку, пока проверяем авторизацию
if (loading) {
return <div>Загрузка...</div>
}
// Возвращаем детей только если пользователь авторизован
return user ? children : null
}
Продвинутые сценарии и Edge-кейсы
Теперь о том, с чем я реально сталкивался в проектах. Здесь-то и кроется черт!
Проблема: разные доменные имена для API и клиента
Если ваш фронтенд и Supabase находятся на разных доменах, вы можете столкнуться с проблемами CORS и cookie:
// lib/supabase.js
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true,
// Указываем домен для cookie
cookieOptions: {
domain: process.env.NODE_ENV === 'production'
? '.yourdomain.com'
: 'localhost',
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
}
}
}
)
Проблема: реализация двухфакторной аутентификации
Supabase не предоставляет встроенной 2FA, но это можно реализовать через дополнительные проверки:
async function setupTwoFactor(userId) {
// 1. Генерируем секрет
const secret = generateTOTPSecret()
// 2. Сохраняем в отдельной защищенной таблице
const { error } = await supabase
.from('user_2fa')
.insert({
user_id: userId,
secret,
verified: false,
created_at: new Date()
})
// 3. Возвращаем QR-код для настройки
return generateTOTPQRCode(secret)
}
async function verifyTwoFactor(userId, token) {
// 1. Получаем секрет пользователя
const { data, error } = await supabase
.from('user_2fa')
.select('secret')
.eq('user_id', userId)
.single()
if (error || !data) return false
// 2. Проверяем токен
return verifyTOTP(data.secret, token)
}
Тут важно понимать, что для полноценной 2FA потребуется дополнительная обработка в middleware или через Supabase Functions.
Проблема: миграция пользователей из другой системы
Часто требуется перенести пользователей из старой системы:
async function migrateUser(email, oldPasswordHash) {
// 1. Создаем пользователя через Admin API
const adminClient = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY
)
const { data, error } = await adminClient.auth.admin.createUser({
email,
password: generateTemporaryPassword(), // Генерируем временный пароль
email_confirm: true, // Подтверждаем почту
user_metadata: {
migrated: true,
oldPasswordHash // Сохраняем старый хеш для последующей проверки
}
})
return { data, error }
}
// На стороне API создаем endpoint для проверки старого пароля
// при первом входе мигрированного пользователя
async function signInMigratedUser(email, password) {
const { data: user } = await supabase.auth.admin.getUserByEmail(email)
if (user?.user_metadata?.migrated) {
// Проверяем пароль против старого хеша
const isValid = checkOldPassword(password, user.user_metadata.oldPasswordHash)
if (isValid) {
// Обновляем пароль на новый и авторизуем
await supabase.auth.admin.updateUserById(user.id, {
password: password, // Теперь пароль будет храниться в формате Supabase
user_metadata: {
migrated: false // Убираем флаг миграции
}
})
// Создаем сессию
return supabase.auth.admin.createSession(user.id)
}
}
// Если не мигрированный или неверный пароль
return { error: "Неверные учетные данные" }
}
Оптимизация производительности и мониторинг
Система авторизации — критически важная часть приложения, поэтому стоит уделить внимание ее производительности и мониторингу.
Кеширование пользовательских данных
// hooks/useUserProfile.js
import { useState, useEffect } from 'react'
import { supabase } from '../lib/supabase'
import { useAuth } from '../contexts/AuthContext'
export function useUserProfile() {
const { user } = useAuth()
const [profile, setProfile] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
let mounted = true
if (user) {
// Используем кеш SessionStorage для профиля
const cachedProfile = sessionStorage.getItem(`profile_${user.id}`)
if (cachedProfile) {
setProfile(JSON.parse(cachedProfile))
setLoading(false)
}
// В любом случае обновляем из БД
async function fetchProfile() {
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.single()
if (!error && mounted) {
setProfile(data)
// Обновляем кеш
sessionStorage.setItem(`profile_${user.id}`, JSON.stringify(data))
}
if (mounted) setLoading(false)
}
fetchProfile()
}
return () => {
mounted = false
}
}, [user])
return { profile, loading }
}
Мониторинг ошибок авторизации
Для крупных проектов важно отслеживать ошибки авторизации:
// lib/supabaseMonitoring.js
import { supabase } from './supabase'
// Перехватываем ошибки авторизации
const originalSignIn = supabase.auth.signInWithPassword
supabase.auth.signInWithPassword = async (...args) => {
try {
const result = await originalSignIn.apply(supabase.auth, args)
if (result.error) {
// Отправляем в систему мониторинга
captureAuthError({
type: 'signIn',
error: result.error,
email: args[0]?.email,
timestamp: new Date(),
userAgent: navigator.userAgent
})
}
return result
} catch (error) {
captureAuthError({
type: 'signInException',
error,
email: args[0]?.email,
timestamp: new Date(),
userAgent: navigator.userAgent
})
throw error
}
}
// Аналогично для других методов авторизации...
function captureAuthError(errorData) {
// Отправка в Sentry, LogRocket или вашу систему мониторинга
console.error('Auth error:', errorData)
// Пример отправки в свою систему
fetch('/api/log-auth-error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorData)
}).catch(e => console.error('Failed to log auth error:', e))
}
Заключение и дальнейшие шаги
Мы рассмотрели основные аспекты реализации авторизации с помощью Supabase, но это только начало пути. В реальных проектах часто требуется:
- Управление ролями и правами — расширение базовой системы авторизации
- Интеграция с внешними сервисами — например, с CRM или аналитикой
- Комплексная безопасность — защита от брутфорса, спама и других атак
Я уверен, что грамотная реализация авторизации — это инвестиция в будущее проекта. Она позволяет избежать переписывания кода, утечек данных и проблем с масштабированием.
Supabase предоставляет отличный набор инструментов, но, как и всегда в разработке, важно понимать, что происходит под капотом. Надеюсь, эта статья поможет вам реализовать профессиональную систему авторизации в ваших проектах.
А если у вас возникнут дополнительные вопросы или интересные кейсы по работе с Supabase, делитесь в комментариях. Я постараюсь ответить на все!
Автор: Богдан Новотарский – разработчик, изучающий Fullstack-разработку и делящийся своим опытом в IT.
Следите за новыми статьями:
GitHub: https://github.com/bogdan-novotarskij
Twitter: https://x.com/novotarskijb
Top comments (0)