DEV Community

Cover image for Профессиональная реализация авторизации в веб-приложении с помощью Supabase от Богдана Новотарского

Профессиональная реализация авторизации в веб-приложении с помощью Supabase от Богдана Новотарского

За годы своей практики я понял одну важную вещь: правильно реализованная авторизация — фундамент любого серьезного проекта. И это один из тех компонентов, который разработчики часто недооценивают, пока не столкнутся с проблемами в продакшене. Сегодня хочу рассказать о 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)
Enter fullscreen mode Exit fullscreen mode

Обратите внимание на проверку наличия переменных окружения — мелочь, но многие забывают об этом, получая странные ошибки в процессе разработки.

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Аналогично реализуем форму входа:

// 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Теперь мы можем оборачивать наше приложение в 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);
Enter fullscreen mode Exit fullscreen mode

Теперь при любых операциях с таблицей 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} />
}
Enter fullscreen mode Exit fullscreen mode

Социальные провайдеры и 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Защищенные маршруты в клиентском приложении

В 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
}
Enter fullscreen mode Exit fullscreen mode

Продвинутые сценарии и 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',
      }
    }
  }
)
Enter fullscreen mode Exit fullscreen mode

Проблема: реализация двухфакторной аутентификации

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)
}
Enter fullscreen mode Exit fullscreen mode

Тут важно понимать, что для полноценной 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: "Неверные учетные данные" }
}
Enter fullscreen mode Exit fullscreen mode

Оптимизация производительности и мониторинг

Система авторизации — критически важная часть приложения, поэтому стоит уделить внимание ее производительности и мониторингу.

Кеширование пользовательских данных

// 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 }
}
Enter fullscreen mode Exit fullscreen mode

Мониторинг ошибок авторизации

Для крупных проектов важно отслеживать ошибки авторизации:

// 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))
}
Enter fullscreen mode Exit fullscreen mode

Заключение и дальнейшие шаги

Мы рассмотрели основные аспекты реализации авторизации с помощью Supabase, но это только начало пути. В реальных проектах часто требуется:

  1. Управление ролями и правами — расширение базовой системы авторизации
  2. Интеграция с внешними сервисами — например, с CRM или аналитикой
  3. Комплексная безопасность — защита от брутфорса, спама и других атак

Я уверен, что грамотная реализация авторизации — это инвестиция в будущее проекта. Она позволяет избежать переписывания кода, утечек данных и проблем с масштабированием.

Supabase предоставляет отличный набор инструментов, но, как и всегда в разработке, важно понимать, что происходит под капотом. Надеюсь, эта статья поможет вам реализовать профессиональную систему авторизации в ваших проектах.

А если у вас возникнут дополнительные вопросы или интересные кейсы по работе с Supabase, делитесь в комментариях. Я постараюсь ответить на все!

Автор: Богдан Новотарский – разработчик, изучающий Fullstack-разработку и делящийся своим опытом в IT.

Следите за новыми статьями:
GitHub: https://github.com/bogdan-novotarskij
Twitter: https://x.com/novotarskijb

Top comments (0)

OSZAR »