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, и вы сможете получить свой ключ веб-API в настройках проекта.
Скопируйте ключ API и сохраните его. Теперь давайте откроем следующую вкладку — Cloud Messaging и запишем идентификатор отправителя . Он нам понадобится позже.
И последнее, но не менее важное: нам необходимо сгенерировать учетные данные сервисной учетной записи. Это позволит вашему приложению получить полный доступ к вашим сервисам Firebase. Перейдите в «Настройки проекта» > «Учетные записи служб» и нажмите «Создать новый закрытый ключ» . Будет загружен файл .json
с учетными данными сервисной учетной записи. Сохраните этот файл в известном месте.
Вот и все! Мы готовы интегрировать приложение Next.js с аутентификацией Firebase.
В этом руководстве предполагается, что у вас установлены Node.js и npm . Команды, используемые в этом руководстве, были проверены на соответствие последней версии LTS Node.js v20 . Вы можете проверить версию узла, запустив node -v
в терминале. Вы также можете использовать такие инструменты, как NVM, для быстрого переключения между версиями Node.js.
Откройте свой любимый терминал, перейдите в папку своих проектов и запустите
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, похожую на эту:
Прежде чем мы начнем интеграцию с 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.
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'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 . Ваше здоровье! 🎉
Это дополнительное руководство научит вас, как развернуть приложение Next.js в Vercel.
Чтобы иметь возможность выполнить развертывание в 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
Перейдите на https://vercel.com/ и войдите в свою учетную запись Github.
После входа в систему перейдите на страницу обзора Vercel и нажмите «Добавить новый» > «Проект».
Нажмите «Импорт» рядом с только что созданным репозиторием Github. Пока не развертывайте.
Прежде чем приступить к развертыванию, нам необходимо предоставить конфигурацию проекта. Давайте добавим несколько переменных среды:
Не забудьте установить USE_SECURE_COOKIES
значение true
, поскольку Vercel по умолчанию использует HTTPS.
Теперь мы готовы нажать «Развернуть».
Подождите минуту или две, и вы сможете получить доступ к своему приложению по URL-адресу, подобному этому: https://next-typescript-minimal-xi.vercel.app/
Сделанный. Могу поспорить, вы не ожидали, что это будет так легко.
Если вам понравилось руководство, я был бы признателен за участие в репозитории next-firebase-auth-edge .
Вы также можете оставить мне свой отзыв в комментариях. Ваше здоровье! 🎉