paint-brush
Использование аутентификации Firebase с новейшими функциями Next.jsк@awinogrodzki
23,089 чтения
23,089 чтения

Использование аутентификации Firebase с новейшими функциями Next.js

к Amadeusz Winogrodzki32m2024/04/04
Read on Terminal Reader

Слишком долго; Читать

Подробное пошаговое руководство по интеграции аутентификации Firebase с Next.js с использованием библиотеки next-firebase-auth-edge с нулевым размером пакета. Он включает в себя этапы регистрации пользователя, функции входа в систему и выхода из системы, а также логику перенаправления для обеспечения беспрепятственного взаимодействия с пользователем. В этом руководстве вы узнаете, как интегрировать проверку подлинности Firebase с новейшими функциями Next.js, такими как маршрутизатор приложений, промежуточное ПО и серверные компоненты. Он завершается инструкциями по развертыванию приложения в Vercel, демонстрирующими простоту использования библиотеки и перспективный дизайн для разработчиков, желающих улучшить свои приложения Next.js с помощью Firebase Authentication.
featured image - Использование аутентификации Firebase с новейшими функциями Next.js
Amadeusz Winogrodzki HackerNoon profile picture
0-item
1-item

Введение в next-firebase-auth-edge

Вероятно, вы нашли эту статью, когда искали способы добавить аутентификацию Firebase в существующее или новое приложение Next.js. Вы стремитесь принять разумное, беспристрастное и ориентированное на будущее решение, которое максимизирует шансы на успех вашего приложения. Как создатель next-firebase-auth-edge, я должен признать, что высказывать совершенно беспристрастное мнение — не моя сильная сторона, но, по крайней мере, я попытаюсь обосновать подход, который я использовал при разработке библиотеки. Надеемся, что к концу этого руководства вы найдете этот подход простым и жизнеспособным в долгосрочной перспективе.


Как это началось

Я избавлю вас от длинных представлений. Скажу только, что идея библиотеки возникла из ситуации, возможно похожей на вашу. Это было время, когда Next.js выпустил канареечную версию App Router . Я работал над приложением, которое в значительной степени полагалось на перезапись и внутренние перенаправления. Для этого мы использовали специальное приложение Next.js express серверного рендеринга Node.js.


Мы были очень рады появлению App Router и Server Components , но знали, что они не будут совместимы с нашим пользовательским сервером. Промежуточное программное обеспечение казалось мощной функцией, которую мы могли бы использовать, чтобы устранить необходимость в специальном сервере Express, решив вместо этого полагаться исключительно на встроенные функции Next.js для динамического перенаправления и перезаписи пользователей на разные страницы.


В тот раз мы использовали next-firebase-auth . Нам очень понравилась библиотека, но она распространяла нашу логику аутентификации через файлы next.config.js , pages/_app.tsx , pages/api/login.ts , pages/api/logout.ts , которые считались устаревшими. достаточно скоро. Кроме того, библиотека не была совместима с промежуточным программным обеспечением, что не позволяло нам перезаписывать URL-адреса или перенаправлять пользователей в зависимости от их контекста.


Итак, я начал поиск, но, к своему удивлению, не нашел библиотеки, поддерживающей аутентификацию Firebase в промежуточном программном обеспечении. – Почему это могло быть? Это невозможно! Как инженер-программист с более чем 11-летним коммерческим опытом работы с Node.js и React, я готовился решить эту загадку.


Итак, я начал. И ответ стал очевиден. Промежуточное ПО работает внутри Edge Runtime . В Edge Runtime нет библиотеки Firebase, совместимой с API-интерфейсами Web Crypto . Я был обречен . Я чувствовал себя беспомощным. Впервые мне придется ждать , чтобы начать играть с новыми и модными API? - Неа. Под присмотром горшок никогда не закипает. Я быстро перестал рыдать и начал реконструировать next-firebase-auth , firebase-admin и несколько других библиотек аутентификации JWT, адаптируя их к Edge Runtime. Я воспользовался возможностью решить все проблемы, с которыми я столкнулся при использовании предыдущих библиотек аутентификации, стремясь создать самую легкую, простую в настройке и ориентированную на будущее библиотеку аутентификации.


Примерно две недели спустя родилась версия 0.0.1 next-firebase-auth-edge . Это было убедительное доказательство концепции, но вы не захотите использовать версию 0.0.1 . Поверьте мне.


Как дела

Почти два года спустя я рад сообщить, что после 372 коммитов , 110 решенных проблем и множества бесценных отзывов от замечательных разработчиков со всего мира, библиотека достигла стадии, когда другие мои собеседники выражают мне одобрение.



Мой другой я



В этом руководстве я буду использовать версию 1.4.1 next-firebase-auth-edge для создания аутентифицированного приложения Next.js с нуля. Мы подробно рассмотрим каждый шаг, начиная с создания нового проекта Firebase и приложения Next.js, а затем интеграции с библиотеками next-firebase-auth-edge и firebase/auth . В конце этого руководства мы развернем приложение в Vercel, чтобы убедиться, что все работает как локально, так и в готовой к работе среде.


Настройка Firebase

В этой части предполагается, что вы еще не настроили аутентификацию Firebase. Если нет, смело переходите к следующей части.


Давайте перейдем к консоли Firebase и создадим проект.


После создания проекта давайте включим аутентификацию Firebase. Откройте консоль и выберите «Сборка» > «Аутентификация» > «Метод входа» и включите метод «Электронная почта и пароль» . Этот метод мы собираемся поддерживать в нашем приложении.


Включение метода входа по электронной почте/паролю


После того, как вы включили свой первый метод входа, для вашего проекта должна быть включена проверка подлинности Firebase, и вы сможете получить свой ключ веб-API в настройках проекта.


Получить ключ веб-API


Скопируйте ключ API и сохраните его. Теперь давайте откроем следующую вкладку — Cloud Messaging и запишем идентификатор отправителя . Он нам понадобится позже.


Получить идентификатор отправителя


И последнее, но не менее важное: нам необходимо сгенерировать учетные данные сервисной учетной записи. Это позволит вашему приложению получить полный доступ к вашим сервисам Firebase. Перейдите в «Настройки проекта» > «Учетные записи служб» и нажмите «Создать новый закрытый ключ» . Будет загружен файл .json с учетными данными сервисной учетной записи. Сохраните этот файл в известном месте.



Вот и все! Мы готовы интегрировать приложение Next.js с аутентификацией Firebase.

Создание приложения Next.js с нуля

В этом руководстве предполагается, что у вас установлены Node.js и npm . Команды, используемые в этом руководстве, были проверены на соответствие последней версии LTS Node.js v20 . Вы можете проверить версию узла, запустив node -v в терминале. Вы также можете использовать такие инструменты, как NVM, для быстрого переключения между версиями Node.js.

Настройка приложения Next.js с помощью CLI

Откройте свой любимый терминал, перейдите в папку своих проектов и запустите

 npx create-next-app@latest


Для простоты давайте использовать конфигурацию по умолчанию. Это означает, что мы будем использовать TypeScript и tailwind

 ✔ What is your project named? … my-app ✔ Would you like to use TypeScript? … Yes ✔ Would you like to use ESLint? … Yes ✔ Would you like to use Tailwind CSS? … Yes ✔ Would you like to use `src/` directory? … No ✔ Would you like to use App Router? (recommended) … Yes ✔ Would you like to customize the default import alias (@/*)? … No


Давайте перейдем в корневой каталог проекта и убедимся, что все зависимости установлены.

 cd my-app npm install


Чтобы убедиться, что все работает как положено, давайте запустим сервер разработки Next.js с помощью команды npm run dev . Когда вы откроете http://localhost:3000 , вы должны увидеть страницу приветствия Next.js, похожую на эту:


Приветственная страница Next.js

Подготовка переменных среды

Прежде чем мы начнем интеграцию с Firebase, нам нужен безопасный способ хранения и чтения нашей конфигурации Firebase. К счастью, Next.js поставляется со встроенной поддержкой dotenv .


Откройте свой любимый редактор кода и перейдите в папку проекта.


Давайте создадим файл .env.local в корневом каталоге проекта и заполним его следующими переменными среды:


 FIREBASE_ADMIN_CLIENT_EMAIL=... FIREBASE_ADMIN_PRIVATE_KEY=... AUTH_COOKIE_NAME=AuthToken AUTH_COOKIE_SIGNATURE_KEY_CURRENT=secret1 AUTH_COOKIE_SIGNATURE_KEY_PREVIOUS=secret2 USE_SECURE_COOKIES=false NEXT_PUBLIC_FIREBASE_PROJECT_ID=... NEXT_PUBLIC_FIREBASE_API_KEY=AIza... NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=....firebaseapp.com NEXT_PUBLIC_FIREBASE_DATABASE_URL=....firebaseio.com NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=...


Обратите внимание, что переменные с префиксом NEXT_PUBLIC_ будут доступны в пакете на стороне клиента. Они понадобятся нам для настройки Firebase Auth Client SDK.


NEXT_PUBLIC_FIREBASE_PROJECT_ID , FIREBASE_ADMIN_CLIENT_EMAIL и FIREBASE_ADMIN_PRIVATE_KEY можно получить из файла .json , загруженного после создания учетных данных сервисной учетной записи.


AUTH_COOKIE_NAME — это имя файла cookie, используемого для хранения учетных данных пользователя.

AUTH_COOKIE_SIGNATURE_KEY_CURRENT и AUTH_COOKIE_SIGNATURE_KEY_PREVIOUS — это секреты, с помощью которых мы будем подписывать учетные данные.


NEXT_PUBLIC_FIREBASE_API_KEY — это ключ веб-API , полученный с общей страницы настроек проекта.

NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN — это идентификатор вашего проекта .firebaseapp.com.

NEXT_PUBLIC_FIREBASE_DATABASE_URL — это идентификатор вашего проекта .firebaseio.com.

NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID можно получить на странице «Настройки проекта» > «Облачные сообщения».


USE_SECURE_COOKIES не будет использоваться для локальной разработки, но пригодится, когда мы развернем наше приложение в Vercel.

Интеграция с аутентификацией Firebase

Установка next-firebase-auth-edge и первоначальная конфигурация

Добавьте библиотеку в зависимости проекта, запустив npm install next-firebase-auth-edge@^1.4.1


Давайте создадим файл config.ts для инкапсуляции конфигурации нашего проекта. Это не обязательно, но сделает примеры кода более читабельными.

Не тратьте слишком много времени на размышления об этих ценностях. Мы объясним их более подробно по мере продвижения.


 export const serverConfig = { cookieName: process.env.AUTH_COOKIE_NAME!, cookieSignatureKeys: [process.env.AUTH_COOKIE_SIGNATURE_KEY_CURRENT!, process.env.AUTH_COOKIE_SIGNATURE_KEY_PREVIOUS!], cookieSerializeOptions: { path: "/", httpOnly: true, secure: process.env.USE_SECURE_COOKIES === "true", sameSite: "lax" as const, maxAge: 12 * 60 * 60 * 24, }, serviceAccount: { projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!, clientEmail: process.env.FIREBASE_ADMIN_CLIENT_EMAIL!, privateKey: process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\n/g, "\n")!, } }; export const clientConfig = { projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!, authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL, messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID };


Добавление промежуточного программного обеспечения

Создайте файл middleware.ts в корне проекта и вставьте следующее:

 import { NextRequest } from "next/server"; import { authMiddleware } from "next-firebase-auth-edge"; import { clientConfig, serverConfig } from "./config"; export async function middleware(request: NextRequest) { return authMiddleware(request, { loginPath: "/api/login", logoutPath: "/api/logout", apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, cookieSerializeOptions: serverConfig.cookieSerializeOptions, serviceAccount: serverConfig.serviceAccount, }); } export const config = { matcher: [ "/", "/((?!_next|api|.*\\.).*)", "/api/login", "/api/logout", ], };


Хотите верьте, хотите нет, но мы только что интегрировали сервер нашего приложения с аутентификацией Firebase. Прежде чем мы начнем его использовать, давайте немного объясним конфигурацию:


loginPath даст указание authMiddleware предоставить конечную точку GET /api/login . Когда эта конечная точка вызывается с заголовком Authorization: Bearer ${idToken} *, она отвечает заголовком Set-Cookie только для HTTP(S), содержащим подписанные пользовательские токены и токены обновления.


* idToken извлекается с помощью функции getIdToken доступной в Firebase Client SDK . Подробнее об этом позже.


Аналогично, logoutPath инструктирует промежуточное программное обеспечение предоставить GET /api/logout , но не требует каких-либо дополнительных заголовков. При вызове он удаляет файлы cookie аутентификации из браузера.


apiKey — это ключ веб-API. Промежуточное программное обеспечение использует его для обновления пользовательского токена и сброса файлов cookie аутентификации после истечения срока действия учетных данных.


cookieName — это имя файла cookie, установленного и удаленного конечными точками /api/login и /api/logout


cookieSignatureKeys список секретных ключей, с помощью которых подписываются учетные данные пользователя. Учетные данные всегда подписываются первым ключом в списке, поэтому вам необходимо указать хотя бы одно значение. Вы можете предоставить несколько ключей для выполнения ротации ключей.


cookieSerializeOptions — это параметры, передаваемые в cookie при создании заголовка Set-Cookie . Дополнительную информацию см. в файле README файла cookie.


serviceAccount разрешает библиотеке использовать ваши сервисы Firebase.


Средство сопоставления инструктирует сервер Next.js запускать промежуточное ПО для /api/login , /api/logout / любого другого пути, который не является файлом или вызовом API.

 export const config = { matcher: [ "/", "/((?!_next|api|.*\\.).*)", "/api/login", "/api/logout", ], };


Вам может быть интересно, почему мы не включаем промежуточное ПО для всех вызовов /api/* . Мы могли бы, но рекомендуется обрабатывать неаутентифицированные вызовы внутри самого обработчика маршрутов API. Это немного выходит за рамки данного урока, но если вам интересно, дайте мне знать, и я подготовлю несколько примеров!



Как видите, конфигурация минимальна и имеет четко определенную цель. Теперь давайте начнем вызывать наши конечные точки /api/login и /api/logout .


Создание безопасной домашней страницы

Чтобы упростить задачу, давайте очистим домашнюю страницу Next.js по умолчанию и заменим ее персонализированным содержимым.


Откройте ./app/page.tsx и вставьте это:

 import { getTokens } from "next-firebase-auth-edge"; import { cookies } from "next/headers"; import { notFound } from "next/navigation"; import { clientConfig, serverConfig } from "../config"; export default async function Home() { const tokens = await getTokens(cookies(), { apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, serviceAccount: serverConfig.serviceAccount, }); if (!tokens) { notFound(); } return ( <main className="flex min-h-screen flex-col items-center justify-center p-24"> <h1 className="text-xl mb-4">Super secure home page</h1> <p> Only <strong>{tokens?.decodedToken.email}</strong> holds the magic key to this kingdom! </p> </main> ); }


Давайте разберем это по частям.


Функция getTokens предназначена для проверки и извлечения учетных данных пользователя из файлов cookie.

 const tokens = await getTokens(cookies(), { apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, serviceAccount: serverConfig.serviceAccount, });


Он разрешается с помощью null , если пользователь не аутентифицирован или объект, содержащий два свойства:


token , который представляет собой string idToken, которую можно использовать для авторизации запросов API к внешним серверным службам. Это немного выходит за рамки, но стоит отметить, что библиотека поддерживает архитектуру распределенных сервисов. token совместим и готов к использованию со всеми официальными библиотеками Firebase на всех платформах.


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


После получения tokens мы используем функцию notFound из next/navigation чтобы убедиться, что страница доступна только авторизованным пользователям.

 if (!tokens) { notFound(); }


Наконец, мы отображаем базовый персонализированный пользовательский контент.

 <main className="flex min-h-screen flex-col items-center justify-center p-24"> <h1 className="text-xl mb-4">Super secure home page</h1> <p> Only <strong>{tokens?.decodedToken.email}</strong> holds the magic key to this kingdom!" </p> </main>


Давайте запустим это.

Если вы закрыли свой сервер разработки, просто запустите npm run dev .


Когда вы попытаетесь получить доступ к http://localhost:3000/ , вы должны увидеть 404: эта страница не найдена.


Успех! Мы сохранили наши секреты от посторонних глаз!


Установка firebase и инициализация Firebase Client SDK

Запустите npm install firebase в корневом каталоге проекта.


После установки клиентского SDK создайте файл firebase.ts в корневом каталоге проекта и вставьте следующее:

 import { initializeApp } from 'firebase/app'; import { clientConfig } from './config'; export const app = initializeApp(clientConfig);


Это инициализирует Firebase Client SDK и предоставит объект приложения для клиентских компонентов.

Создание страницы регистрации

Какой смысл иметь суперзащищенную домашнюю страницу, если никто не сможет ее просмотреть? Давайте создадим простую страницу регистрации, чтобы люди могли войти в наше приложение.


Давайте создадим новую интересную страницу в ./app/register/page.tsx


 "use client"; import { FormEvent, useState } from "react"; import Link from "next/link"; import { getAuth, createUserWithEmailAndPassword } from "firebase/auth"; import { app } from "../../firebase"; import { useRouter } from "next/navigation"; export default function Register() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmation, setConfirmation] = useState(""); const [error, setError] = useState(""); const router = useRouter(); async function handleSubmit(event: FormEvent) { event.preventDefault(); setError(""); if (password !== confirmation) { setError("Passwords don't match"); return; } try { await createUserWithEmailAndPassword(getAuth(app), email, password); router.push("/login"); } catch (e) { setError((e as Error).message); } } return ( <main className="flex min-h-screen flex-col items-center justify-center p-8"> <div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700"> <div className="p-6 space-y-4 md:space-y-6 sm:p-8"> <h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white"> Pray tell, who be this gallant soul seeking entry to mine humble abode? </h1> <form onSubmit={handleSubmit} className="space-y-4 md:space-y-6" action="#" > <div> <label htmlFor="email" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Your email </label> <input type="email" name="email" value={email} onChange={(e) => setEmail(e.target.value)} id="email" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="[email protected]" required /> </div> <div> <label htmlFor="password" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Password </label> <input type="password" name="password" value={password} onChange={(e) => setPassword(e.target.value)} id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required /> </div> <div> <label htmlFor="confirm-password" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Confirm password </label> <input type="password" name="confirm-password" value={confirmation} onChange={(e) => setConfirmation(e.target.value)} id="confirm-password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required /> </div> {error && ( <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert" > <span className="block sm:inline">{error}</span> </div> )} <button type="submit" className="w-full text-white bg-gray-600 hover:bg-gray-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-primary-800" > Create an account </button> <p className="text-sm font-light text-gray-500 dark:text-gray-400"> Already have an account?{" "} <Link href="/login" className="font-medium text-gray-600 hover:underline dark:text-gray-500" > Login here </Link> </p> </form> </div> </div> </main> ); }


Я знаю. Текста много, но потерпите.


Мы начинаем с "use client"; чтобы указать, что страница регистрации будет использовать клиентские API


 const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmation, setConfirmation] = useState(""); const [error, setError] = useState("");

Затем мы определяем некоторые переменные и установщики для хранения состояния нашей формы.


 const router = useRouter(); async function handleSubmit(event: FormEvent) { event.preventDefault(); setError(""); if (password !== confirmation) { setError("Passwords don't match"); return; } try { await createUserWithEmailAndPassword(getAuth(app), email, password); router.push("/login"); } catch (e) { setError((e as Error).message); } }

Здесь мы определяем логику отправки формы. Сначала мы проверяем, равны ли password и confirmation , в противном случае мы обновляем состояние ошибки. Если значения действительны, мы создаем учетную запись пользователя с помощью createUserWithEmailAndPassword из firebase/auth . Если этот шаг не удался (например, электронное письмо было принято), мы сообщаем пользователю, обновляя ошибку.


Если все идет хорошо, мы перенаправляем пользователя на страницу /login . Вероятно, вы сейчас в замешательстве, и вы правы. Страница /login еще не существует. Мы просто готовимся к тому, что будет дальше.


Когда вы посещаете http://localhost:3000/register , страница должна выглядеть примерно так:


Страница регистрации


Создание страницы входа

Теперь, когда пользователи могут зарегистрироваться, позвольте им подтвердить свою личность.


Создайте страницу входа в ./app/login/page.tsx


 "use client"; import { FormEvent, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { getAuth, signInWithEmailAndPassword } from "firebase/auth"; import { app } from "../../firebase"; export default function Login() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const router = useRouter(); async function handleSubmit(event: FormEvent) { event.preventDefault(); setError(""); try { const credential = await signInWithEmailAndPassword( getAuth(app), email, password ); const idToken = await credential.user.getIdToken(); await fetch("/api/login", { headers: { Authorization: `Bearer ${idToken}`, }, }); router.push("/"); } catch (e) { setError((e as Error).message); } } return ( <main className="flex min-h-screen flex-col items-center justify-center p-8"> <div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700"> <div className="p-6 space-y-4 md:space-y-6 sm:p-8"> <h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white"> Speak thy secret word! </h1> <form onSubmit={handleSubmit} className="space-y-4 md:space-y-6" action="#" > <div> <label htmlFor="email" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Your email </label> <input type="email" name="email" value={email} onChange={(e) => setEmail(e.target.value)} id="email" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="[email protected]" required /> </div> <div> <label htmlFor="password" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" > Password </label> <input type="password" name="password" value={password} onChange={(e) => setPassword(e.target.value)} id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required /> </div> {error && ( <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert" > <span className="block sm:inline">{error}</span> </div> )} <button type="submit" className="w-full text-white bg-gray-600 hover:bg-gray-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-primary-800" > Enter </button> <p className="text-sm font-light text-gray-500 dark:text-gray-400"> Don&apos;t have an account?{" "} <Link href="/register" className="font-medium text-gray-600 hover:underline dark:text-gray-500" > Register here </Link> </p> </form> </div> </div> </main> ); }


Как видите, она очень похожа на страницу регистрации. Давайте сосредоточимся на самом важном:

 async function handleSubmit(event: FormEvent) { event.preventDefault(); setError(""); try { const credential = await signInWithEmailAndPassword( getAuth(app), email, password ); const idToken = await credential.user.getIdToken(); await fetch("/api/login", { headers: { Authorization: `Bearer ${idToken}`, }, }); router.push("/"); } catch (e) { setError((e as Error).message); } }


Именно здесь происходит вся магия . Мы используем signInEmailAndPassword из firebase/auth для получения idToken пользователя.


Затем мы вызываем конечную точку /api/login предоставляемую промежуточным программным обеспечением. Эта конечная точка обновляет файлы cookie нашего браузера с учетными данными пользователя.


Наконец, мы перенаправляем пользователя на домашнюю страницу, вызывая router.push("/");


Страница входа должна выглядеть примерно так



Давайте проверим это!


Перейдите по адресу http://localhost:3000/register , введите случайный адрес электронной почты и пароль, чтобы создать учетную запись. Используйте эти учетные данные на странице http://localhost:3000/login . После того, как вы нажмете «Ввод» , вы будете перенаправлены на суперзащищенную домашнюю страницу.


Супербезопасная домашняя страница




Наконец-то мы увидели нашу собственную , личную , сверхзащищенную домашнюю страницу! Но ждать! Как нам выбраться?


Нам нужно добавить кнопку выхода из системы, чтобы не блокировать себя от мира навсегда (или на 12 дней).


Прежде чем мы начнем, нам нужно создать клиентский компонент , который сможет выполнить выход из системы с помощью Firebase Client SDK.


Давайте создадим новый файл в ./app/HomePage.tsx

 "use client"; import { useRouter } from "next/navigation"; import { getAuth, signOut } from "firebase/auth"; import { app } from "../firebase"; interface HomePageProps { email?: string; } export default function HomePage({ email }: HomePageProps) { const router = useRouter(); async function handleLogout() { await signOut(getAuth(app)); await fetch("/api/logout"); router.push("/login"); } return ( <main className="flex min-h-screen flex-col items-center justify-center p-24"> <h1 className="text-xl mb-4">Super secure home page</h1> <p className="mb-8"> Only <strong>{email}</strong> holds the magic key to this kingdom! </p> <button onClick={handleLogout} className="text-white bg-gray-600 hover:bg-gray-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-primary-800" > Logout </button> </main> ); }


Как вы могли заметить, это слегка измененная версия нашего ./app/page.tsx . Нам пришлось создать отдельный клиентский компонент, потому что getTokens работает только внутри серверных компонентов и обработчиков маршрутов API , а signOut и useRouter требуют запуска в контексте клиента. Я знаю, что это немного сложно, но на самом деле это довольно мощно. Я объясню позже.


Давайте сосредоточимся на процессе выхода из системы.

 const router = useRouter(); async function handleLogout() { await signOut(getAuth(app)); await fetch("/api/logout"); router.push("/login"); }


Сначала мы выходим из Firebase Client SDK. Затем мы вызываем конечную точку /api/logout предоставляемую промежуточным программным обеспечением. Мы заканчиваем перенаправлением пользователя на страницу /login .


Давайте обновим домашнюю страницу нашего сервера. Перейдите в ./app/page.tsx и вставьте следующее

 import { getTokens } from "next-firebase-auth-edge"; import { cookies } from "next/headers"; import { notFound } from "next/navigation"; import { clientConfig, serverConfig } from "../config"; import HomePage from "./HomePage"; export default async function Home() { const tokens = await getTokens(cookies(), { apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, serviceAccount: serverConfig.serviceAccount, }); if (!tokens) { notFound(); } return <HomePage email={tokens?.decodedToken.email} />; }


Теперь наш компонент Home сервера отвечает только за получение пользовательских токенов и передачу их клиентскому компоненту HomePage . На самом деле это довольно распространенный и полезный шаблон.


Давайте проверим это:


Вуаля! Теперь мы можем входить и выходить из приложения по своему желанию. Отлично!

Или это?


Когда неаутентифицированный пользователь пытается войти на домашнюю страницу, открыв http://localhost:3000/, мы показываем 404: эта страница не найдена.

Кроме того, прошедшие проверку подлинности пользователи по-прежнему могут получить доступ к страницам http://localhost:3000/register и http://localhost:3000/login без необходимости выхода из системы.


Мы можем добиться большего.


Кажется, нам нужно добавить логику перенаправления. Давайте определим некоторые правила:

  • Когда аутентифицированный пользователь пытается получить доступ к страницам /register и /login , мы должны перенаправить его на /
  • Когда неаутентифицированный пользователь пытается получить доступ к странице / , мы должны перенаправить его на /login


Промежуточное программное обеспечение — один из лучших способов обработки перенаправлений в приложениях Next.js. К счастью, authMiddleware поддерживает ряд опций и вспомогательных функций для обработки широкого спектра сценариев перенаправления.


Давайте откроем файл middleware.ts и вставим эту обновленную версию.

 import { NextRequest, NextResponse } from "next/server"; import { authMiddleware, redirectToHome, redirectToLogin } from "next-firebase-auth-edge"; import { clientConfig, serverConfig } from "./config"; const PUBLIC_PATHS = ['/register', '/login']; export async function middleware(request: NextRequest) { return authMiddleware(request, { loginPath: "/api/login", logoutPath: "/api/logout", apiKey: clientConfig.apiKey, cookieName: serverConfig.cookieName, cookieSignatureKeys: serverConfig.cookieSignatureKeys, cookieSerializeOptions: serverConfig.cookieSerializeOptions, serviceAccount: serverConfig.serviceAccount, handleValidToken: async ({token, decodedToken}, headers) => { if (PUBLIC_PATHS.includes(request.nextUrl.pathname)) { return redirectToHome(request); } return NextResponse.next({ request: { headers } }); }, handleInvalidToken: async (reason) => { console.info('Missing or malformed credentials', {reason}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); }, handleError: async (error) => { console.error('Unhandled authentication error', {error}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); } }); } export const config = { matcher: [ "/", "/((?!_next|api|.*\\.).*)", "/api/login", "/api/logout", ], };


Вот и все. Мы реализовали все правила перенаправления. Давайте разберем это.


 const PUBLIC_PATHS = ['/register', '/login'];


 handleValidToken: async ({token, decodedToken}, headers) => { if (PUBLIC_PATHS.includes(request.nextUrl.pathname)) { return redirectToHome(request); } return NextResponse.next({ request: { headers } }); },

handleValidToken вызывается, когда к запросу прилагаются действительные учетные данные пользователя, т.е. пользователь аутентифицирован. Он вызывается с объектом tokens в качестве первого и модифицированными заголовками запроса в качестве второго аргумента. Это должно разрешиться с помощью NextResponse .


redirectToHome из next-firebase-auth-edge — это вспомогательная функция, которая возвращает объект, который можно упростить до NextResponse.redirect(new URL(“/“))


Проверяя PUBLIC_PATHS.includes(request.nextUrl.pathname) , мы проверяем, пытается ли аутентифицированный пользователь получить доступ к странице /login или /register , и перенаправляем на главную, если это так.



 handleInvalidToken: async (reason) => { console.info('Missing or malformed credentials', {reason}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); },


handleInvalidToken вызывается, когда происходит что-то ожидаемое . Одним из таких ожидаемых событий является то, что пользователь впервые видит ваше приложение с другого устройства или после истечения срока действия учетных данных.


Зная, что handleInvalidToken вызывается для неаутентифицированного пользователя, мы можем перейти ко второму правилу: когда неаутентифицированный пользователь пытается получить доступ к странице / , мы должны перенаправить его на /login


Поскольку других условий не требуется, мы просто возвращаем результат redirectToLogin , который можно упростить до NextResponse.redirect(new URL(“/login”)) . Это также гарантирует, что пользователь не попадет в цикл перенаправления.


Наконец,

 handleError: async (error) => { console.error('Unhandled authentication error', {error}); return redirectToLogin(request, { path: '/login', publicPaths: PUBLIC_PATHS }); }


В отличие от handleInvalidToken , handleError вызывается, когда происходит что-то неожиданное* и, возможно, требует расследования. Список возможных ошибок с их описанием вы можете найти в документации.


В случае ошибки мы фиксируем этот факт и безопасно перенаправляем пользователя на страницу входа.


* handleError можно вызвать с ошибкой INVALID_ARGUMENT после обновления открытых ключей Google.

Это своего рода ротация ключей, и она ожидается . См. этот выпуск Github для получения дополнительной информации.


Вот и все. Окончательно.


Давайте выйдем из нашего веб-приложения и откроем http://localhost:3000/ . Мы должны быть перенаправлены на страницу /login .

Давайте снова войдем в систему и попробуем ввести http://localhost:3000/login . Мы должны быть перенаправлены на / страницу.


Мы не только обеспечили удобство взаимодействия с пользователем. next-firebase-auth-edge — это библиотека нулевого размера, которая работает только на сервере приложения и не вводит дополнительный код на стороне клиента. Полученный пакет действительно минимален . Это то, что я называю совершенством.


Наше приложение теперь полностью интегрировано с аутентификацией Firebase как в серверном, так и в клиентском компонентах. Мы готовы раскрыть весь потенциал Next.js!




Исходный код приложения можно найти в папке next-firebase-auth-edge/examples/next-typescript-minimal.


Эпилог

В этом руководстве мы рассмотрели интеграцию нового приложения Next.js с аутентификацией Firebase.


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


Если вас заинтересовала библиотека, вы можете просмотреть полноценную стартовую демонстрационную страницу next-firebase-auth-edge .

Он включает в себя интеграцию с Firestore , действия сервера , поддержку App-Check и многое другое.


Библиотека предоставляет специальную страницу документации с множеством примеров.


Если вам понравилась статья, я был бы признателен за добавление репозитория next-firebase-auth-edge . Ваше здоровье! 🎉



Бонус — развертывание приложения в Vercel.

Это дополнительное руководство научит вас, как развернуть приложение Next.js в Vercel.

Создание git-репозитория

Чтобы иметь возможность выполнить развертывание в Vercel, вам необходимо создать репозиторий для вашего нового приложения.


Перейдите на https://github.com/ и создайте новый репозиторий.


create-next-app уже инициировал для нас локальный репозиторий git, поэтому вам просто нужно перейти в корневую папку вашего проекта и запустить:


 git add --all git commit -m "first commit" git branch -M main git remote add origin [email protected]:path-to-your-new-github-repository.git git push -u origin main


Добавление нового проекта Vercel

Перейдите на https://vercel.com/ и войдите в свою учетную запись Github.


После входа в систему перейдите на страницу обзора Vercel и нажмите «Добавить новый» > «Проект».


Нажмите «Импорт» рядом с только что созданным репозиторием Github. Пока не развертывайте.


Прежде чем приступить к развертыванию, нам необходимо предоставить конфигурацию проекта. Давайте добавим несколько переменных среды:

Развертывание в Верселе


Не забудьте установить USE_SECURE_COOKIES значение true , поскольку Vercel по умолчанию использует HTTPS.


Теперь мы готовы нажать «Развернуть».


Подождите минуту или две, и вы сможете получить доступ к своему приложению по URL-адресу, подобному этому: https://next-typescript-minimal-xi.vercel.app/


Сделанный. Могу поспорить, вы не ожидали, что это будет так легко.




Если вам понравилось руководство, я был бы признателен за участие в репозитории next-firebase-auth-edge .


Вы также можете оставить мне свой отзыв в комментариях. Ваше здоровье! 🎉