paint-brush
Sử dụng Xác thực Firebase với các tính năng Next.js mới nhấttừ tác giả@awinogrodzki
3,762 lượt đọc
3,762 lượt đọc

Sử dụng Xác thực Firebase với các tính năng Next.js mới nhất

từ tác giả Amadeusz Winogrodzki32m2024/04/04
Read on Terminal Reader

dài quá đọc không nổi

Hướng dẫn từng bước, toàn diện về cách tích hợp Xác thực Firebase với Next.js bằng cách sử dụng thư viện `next-firebase-auth-edge` có kích thước gói bằng 0. Nó bao gồm các bước đăng ký người dùng, đăng nhập và đăng xuất, cùng với logic chuyển hướng để mang lại trải nghiệm người dùng liền mạch. Trong hướng dẫn này, bạn sẽ tìm hiểu cách tích hợp Xác thực Firebase với các tính năng Next.js mới nhất, chẳng hạn như Bộ định tuyến ứng dụng, Phần mềm trung gian và Thành phần máy chủ. Nó kết thúc bằng hướng dẫn về cách triển khai ứng dụng lên Vercel, thể hiện tính dễ sử dụng và thiết kế phù hợp với tương lai của thư viện dành cho các nhà phát triển đang tìm cách nâng cao ứng dụng Next.js của họ bằng Xác thực Firebase.
featured image - Sử dụng Xác thực Firebase với các tính năng Next.js mới nhất
Amadeusz Winogrodzki HackerNoon profile picture
0-item
1-item

Giới thiệu về next-firebase-auth-edge

Bạn có thể đã tìm thấy bài viết này khi đang tìm cách thêm Xác thực Firebase vào ứng dụng Next.js hiện có hoặc mới của mình. Bạn đặt mục tiêu đưa ra quyết định thông minh, không thiên vị và hướng đến tương lai để tối đa hóa cơ hội thành công cho ứng dụng của bạn. Với tư cách là người tạo ra next-firebase-auth-edge, tôi phải thừa nhận rằng việc đưa ra ý kiến hoàn toàn khách quan không phải là sở trường của tôi, nhưng ít nhất tôi sẽ cố gắng biện minh cho cách tiếp cận mà tôi đã thực hiện khi thiết kế thư viện. Hy vọng rằng đến cuối hướng dẫn này, bạn có thể thấy cách tiếp cận này vừa đơn giản vừa khả thi về lâu dài.


Làm thế nào nó bắt đầu

Tôi sẽ tiết kiệm cho bạn những lời giới thiệu dài dòng. Hãy để tôi nói rằng ý tưởng về thư viện được lấy cảm hứng từ một tình huống có thể giống với hoàn cảnh của bạn. Đó là thời điểm Next.js phát hành phiên bản hoàng yến của App Router . Tôi đang làm việc trên một ứng dụng chủ yếu dựa vào việc viết lại và chuyển hướng nội bộ. Để làm được điều đó, chúng tôi đã sử dụng máy chủ tốc hành Node.js express hiển thị ứng dụng Next.js.


Chúng tôi thực sự hào hứng với Bộ định tuyến ứng dụngThành phần máy chủ nhưng cũng biết rằng nó sẽ không tương thích với máy chủ tùy chỉnh của chúng tôi. Middleware dường như là một tính năng mạnh mẽ mà chúng tôi có thể tận dụng để loại bỏ nhu cầu về máy chủ Express tùy chỉnh, thay vào đó chọn chỉ dựa vào các tính năng tích hợp của Next.js để chuyển hướng và ghi lại người dùng đến các trang khác nhau một cách linh hoạt.


Lần đó, chúng tôi đang sử dụng next-firebase-auth . Chúng tôi thực sự thích thư viện này, nhưng nó phân bổ logic xác thực của chúng tôi thông qua các tệp next.config.js , pages/_app.tsx , pages/api/login.ts , pages/api/logout.ts , những tệp này sẽ được coi là kế thừa đủ sớm. Ngoài ra, thư viện không tương thích với phần mềm trung gian, khiến chúng tôi không thể viết lại URL hoặc chuyển hướng người dùng dựa trên ngữ cảnh của họ.


Vì vậy, tôi đã bắt đầu tìm kiếm, nhưng thật ngạc nhiên, tôi không tìm thấy thư viện nào hỗ trợ Xác thực Firebase trong phần mềm trung gian. – Tại sao lại có thể như vậy? Điều đó là không thể! Là một kỹ sư phần mềm với hơn 11 năm kinh nghiệm thương mại về Node.js và React, tôi đang chuẩn bị giải quyết câu hỏi hóc búa này.


Vì vậy, tôi bắt đầu. Và câu trả lời đã trở nên rõ ràng. Middleware đang chạy bên trong Edge Runtime . Không có thư viện firebase tương thích với API Web Crypto có sẵn trong Edge Runtime . Tôi đã phải chịu số phận . Tôi cảm thấy bất lực. Đây có phải là lần đầu tiên tôi thực sự phải đợi để sử dụng các API mới và lạ mắt không? - Không. Nồi canh không bao giờ sôi. Tôi nhanh chóng ngừng nức nở và bắt đầu thiết kế ngược next-firebase-auth , firebase-admin và một số thư viện xác thực JWT khác, điều chỉnh chúng cho phù hợp với Edge Runtime. Tôi đã nắm bắt cơ hội để giải quyết tất cả các vấn đề tôi gặp phải với các thư viện xác thực trước đó, nhằm tạo ra thư viện xác thực nhẹ nhất, dễ cấu hình nhất và hướng tới tương lai.


Khoảng hai tuần sau, phiên bản 0.0.1 của next-firebase-auth-edge ra đời. Đó là một bằng chứng chắc chắn về khái niệm, nhưng bạn sẽ không muốn sử dụng phiên bản 0.0.1 . Hãy tin tôi.


Mọi thứ sao rồi

Gần hai năm sau , tôi vui mừng thông báo rằng sau 372 lần cam kết , 110 vấn đề được giải quyếtvô số phản hồi vô giá từ các nhà phát triển tuyệt vời trên toàn thế giới, thư viện đã đạt đến giai đoạn mà cái tôi khác của tôi chấp thuận.



Cái tôi khác của tôi



Trong hướng dẫn này, tôi sẽ sử dụng phiên bản 1.4.1 của next-firebase-auth-edge để tạo ứng dụng Next.js được xác thực từ đầu. Chúng ta sẽ thực hiện chi tiết từng bước, bắt đầu bằng việc tạo dự án Firebase và ứng dụng Next.js mới, sau đó là tích hợp với các thư viện next-firebase-auth-edgefirebase/auth . Ở cuối hướng dẫn này, chúng tôi sẽ triển khai ứng dụng lên Vercel để xác nhận rằng mọi thứ đều hoạt động cả cục bộ và trong môi trường sẵn sàng sản xuất.


Thiết lập căn cứ hỏa lực

Phần này giả sử bạn chưa thiết lập Xác thực Firebase. Hãy bỏ qua phần tiếp theo nếu không.


Hãy đến Bảng điều khiển Firebasetạo một dự án


Sau khi dự án được tạo, hãy bật Xác thực Firebase. Mở bảng điều khiển và làm theo phương pháp Xây dựng > Xác thực > Đăng nhập và bật phương thức Email và mật khẩu . Đó là phương pháp chúng tôi sẽ hỗ trợ trong ứng dụng của mình


Kích hoạt phương thức đăng nhập Email/Mật khẩu


Sau khi bạn bật phương thức đăng nhập đầu tiên, Xác thực Firebase sẽ được bật cho dự án của bạn và bạn có thể truy xuất Khóa API Web của mình trong Cài đặt dự án


Truy xuất khóa API Web


Sao chép khóa API và giữ nó an toàn. Bây giờ, hãy mở tab tiếp theo – Nhắn tin qua đám mây và ghi lại ID người gửi . Chúng ta sẽ cần nó sau này.


Truy xuất ID người gửi


Cuối cùng nhưng không kém phần quan trọng, chúng ta cần tạo thông tin đăng nhập tài khoản dịch vụ. Những điều đó sẽ cho phép ứng dụng của bạn có toàn quyền truy cập vào các dịch vụ Firebase của bạn. Chuyển đến Cài đặt dự án > Tài khoản dịch vụ và nhấp vào Tạo khóa riêng mới . Thao tác này sẽ tải xuống tệp .json có thông tin xác thực tài khoản dịch vụ. Lưu tập tin này ở một vị trí đã biết.



Đó là nó! Chúng tôi sẵn sàng tích hợp ứng dụng Next.js với Xác thực Firebase

Tạo ứng dụng Next.js từ đầu

Hướng dẫn này giả sử bạn đã cài đặt Node.jsnpm . Các lệnh được sử dụng trong hướng dẫn này đã được xác minh dựa trên LTS Node.js v20 mới nhất. Bạn có thể xác minh phiên bản nút bằng cách chạy node -v trong terminal. Bạn cũng có thể sử dụng các công cụ như NVM để chuyển đổi nhanh chóng giữa các phiên bản Node.js.

Thiết lập ứng dụng Next.js bằng CLI

Mở thiết bị đầu cuối yêu thích của bạn, điều hướng đến thư mục dự án của bạn và chạy

 npx create-next-app@latest


Để đơn giản, hãy sử dụng cấu hình mặc định. Điều này có nghĩa là chúng tôi sẽ sử dụng TypeScripttailwind

 ✔ 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


Hãy điều hướng đến thư mục gốc của dự án và đảm bảo tất cả các phụ thuộc đã được cài đặt

 cd my-app npm install


Để xác nhận mọi thứ hoạt động như mong đợi, hãy khởi động máy chủ dev Next.js bằng lệnh npm run dev . Khi bạn mở http://localhost:3000 , bạn sẽ thấy trang chào mừng Next.js, tương tự như sau:


Trang chào mừng Next.js

Chuẩn bị các biến môi trường

Trước khi bắt đầu tích hợp với Firebase, chúng tôi cần một cách an toàn để lưu trữ và đọc cấu hình Firebase của mình. May mắn thay, Next.js có hỗ trợ dotenv tích hợp.


Mở trình soạn thảo mã yêu thích của bạn và điều hướng đến thư mục dự án


Hãy tạo tệp .env.local trong thư mục gốc của dự án và điền vào nó các biến môi trường sau:


 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=...


Xin lưu ý rằng các biến có tiền tố NEXT_PUBLIC_ sẽ có sẵn trong gói phía máy khách. Chúng tôi sẽ cần những thứ đó để thiết lập SDK khách hàng xác thực Firebase


NEXT_PUBLIC_FIREBASE_PROJECT_ID , FIREBASE_ADMIN_CLIENT_EMAILFIREBASE_ADMIN_PRIVATE_KEY có thể được truy xuất từ tệp .json được tải xuống sau khi tạo thông tin xác thực tài khoản dịch vụ


AUTH_COOKIE_NAME sẽ là tên của cookie được sử dụng để lưu trữ thông tin đăng nhập của người dùng

AUTH_COOKIE_SIGNATURE_KEY_CURRENTAUTH_COOKIE_SIGNATURE_KEY_PREVIOUS là những bí mật mà chúng tôi sẽ ký thông tin xác thực


NEXT_PUBLIC_FIREBASE_API_KEYKhóa API Web được lấy từ trang chung Cài đặt dự án

NEXT_PUBLIC_FIREBASE_AUTH_DOMAINid dự án của bạn .firebaseapp.com

NEXT_PUBLIC_FIREBASE_DATABASE_URLid dự án của bạn .firebaseio.com

Có thể lấy NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID từ trang Cài đặt dự án > Tin nhắn qua đám mây


USE_SECURE_COOKIES sẽ không được sử dụng để phát triển cục bộ nhưng sẽ hữu ích khi chúng tôi triển khai ứng dụng của mình lên Vercel

Tích hợp với xác thực Firebase

Cài đặt cấu hình next-firebase-auth-edge

Thêm thư viện vào phần phụ thuộc của dự án bằng cách chạy npm install next-firebase-auth-edge@^1.4.1


Hãy tạo tệp config.ts để đóng gói cấu hình dự án của chúng tôi. Việc này không bắt buộc nhưng sẽ làm cho các ví dụ mã dễ đọc hơn.

Đừng dành quá nhiều thời gian để suy ngẫm về những giá trị đó. Chúng tôi sẽ giải thích chúng chi tiết hơn khi chúng tôi theo dõi.


 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 };


Thêm phần mềm trung gian

Tạo tệp middleware.ts trong thư mục gốc của dự án và dán đoạn sau

 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", ], };


Dù bạn có tin hay không, nhưng chúng tôi vừa tích hợp máy chủ Ứng dụng của mình với Xác thực Firebase. Trước khi chúng ta thực sự sử dụng nó, hãy giải thích cấu hình một chút:


loginPath sẽ hướng dẫn authMiddleware hiển thị điểm cuối GET /api/login . Khi điểm cuối này được gọi với Authorization: Bearer ${idToken} *, nó sẽ phản hồi với tiêu đề HTTP(S)-Only Set-Cookie chứa mã thông báo làm mới và tùy chỉnh đã ký


* idToken được truy xuất bằng chức năng getIdToken có sẵn trong SDK khách Firebase . Thêm về điều này sau.


Tương tự, logoutPath hướng dẫn phần mềm trung gian hiển thị GET /api/logout nhưng nó không yêu cầu bất kỳ tiêu đề bổ sung nào. Khi được gọi, nó sẽ xóa cookie xác thực khỏi trình duyệt.


apiKey là Khóa API Web. Middleware sử dụng nó để làm mới mã thông báo tùy chỉnh và đặt lại cookie xác thực sau khi thông tin đăng nhập hết hạn.


cookieName là tên của bộ cookie và bị xóa bởi các điểm cuối /api/login/api/logout


cookieSignatureKeys danh sách các khóa bí mật mà thông tin đăng nhập của người dùng được ký. Thông tin xác thực luôn được ký bằng khóa đầu tiên trong danh sách, do đó bạn cần cung cấp ít nhất một giá trị. Bạn có thể cung cấp nhiều phím để thực hiện xoay phím


cookieSerializeOptions là các tùy chọn được chuyển tới cookie khi tạo tiêu đề Set-Cookie . Xem cookie README để biết thêm thông tin


serviceAccount ủy quyền cho thư viện sử dụng dịch vụ Firebase của bạn.


Trình so khớp hướng dẫn máy chủ Next.js chạy Middleware dựa trên /api/login , /api/logout / bất kỳ đường dẫn nào khác không phải là tệp hoặc lệnh gọi api.

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


Bạn có thể thắc mắc tại sao chúng tôi không kích hoạt phần mềm trung gian cho tất cả lệnh gọi /api/* . Chúng tôi có thể, nhưng đó là một cách tốt để xử lý các cuộc gọi không được xác thực trong chính trình xử lý tuyến đường API. Phần này hơi nằm ngoài phạm vi của hướng dẫn này, nhưng nếu bạn quan tâm, hãy cho tôi biết và tôi sẽ chuẩn bị một số ví dụ!



Như bạn có thể thấy, cấu hình rất tối thiểu và có mục đích được xác định rõ ràng. Bây giờ, hãy bắt đầu gọi các điểm cuối /api/login/api/logout của chúng ta.


Tạo trang chủ an toàn

Để làm mọi thứ đơn giản nhất có thể, hãy xóa trang chủ Next.js mặc định và thay thế nó bằng một số nội dung được cá nhân hóa


Mở ./app/page.tsx và dán cái này:

 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> ); }


Hãy chia nhỏ điều này từng chút một.


Hàm getTokens được thiết kế để xác thực và trích xuất thông tin xác thực của người dùng từ cookie

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


Nó giải quyết bằng null , nếu người dùng không được xác thực hoặc một đối tượng chứa hai thuộc tính:


tokenstring idToken mà bạn có thể sử dụng để ủy quyền các yêu cầu API tới các dịch vụ phụ trợ bên ngoài. Điều này hơi nằm ngoài phạm vi, nhưng cần đề cập rằng thư viện này hỗ trợ kiến trúc dịch vụ phân tán. Mã token tương thích và sẵn sàng sử dụng với tất cả thư viện Firebase chính thức trên tất cả các nền tảng.


decodedToken như tên gợi ý, là phiên bản được giải mã của token , chứa tất cả thông tin cần thiết để nhận dạng người dùng, bao gồm địa chỉ email, ảnh hồ sơ và xác nhận quyền sở hữu tùy chỉnh , điều này còn cho phép chúng tôi hạn chế quyền truy cập dựa trên vai trò và quyền.


Sau khi nhận được tokens , chúng tôi sử dụng chức năng notFound từ next/navigation để đảm bảo chỉ người dùng được xác thực mới có thể truy cập trang

 if (!tokens) { notFound(); }


Cuối cùng, chúng tôi hiển thị một số nội dung người dùng cơ bản, được cá nhân hóa

 <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>


Hãy chạy nó.

Trong trường hợp bạn đã đóng máy chủ dev của mình, chỉ cần chạy npm run dev .


Khi bạn cố gắng truy cập http://localhost:3000/ , bạn sẽ thấy 404: Không thể tìm thấy trang này.


Thành công! Chúng tôi đã giữ bí mật của mình an toàn trước những con mắt tò mò!


Cài đặt firebase và khởi tạo SDK khách hàng Firebase

Chạy npm install firebase tại thư mục gốc của dự án


Sau khi cài đặt SDK máy khách, hãy tạo tệp firebase.ts trong thư mục gốc của dự án và dán đoạn mã sau

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


Điều này sẽ khởi tạo SDK khách hàng Firebase và hiển thị đối tượng ứng dụng cho các thành phần máy khách

Tạo trang đăng ký

Trang chủ siêu an toàn có ý nghĩa gì nếu không ai có thể xem nó? Hãy xây dựng một trang đăng ký đơn giản để cho phép mọi người truy cập ứng dụng của chúng tôi.


Hãy tạo một trang mới, lạ mắt trong ./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> ); }


Tôi biết. Đó là rất nhiều văn bản, nhưng hãy kiên nhẫn với tôi.


Chúng tôi bắt đầu với "use client"; để cho biết rằng trang đăng ký sẽ sử dụng API phía máy khách


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

Sau đó, chúng tôi xác định một số biến và setters để giữ trạng thái biểu mẫu của chúng tôi


 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); } }

Ở đây, chúng tôi xác định logic gửi biểu mẫu của mình. Đầu tiên, chúng tôi xác thực passwordconfirmation có bằng nhau hay không, nếu không, chúng tôi sẽ cập nhật trạng thái lỗi. Nếu các giá trị hợp lệ, chúng tôi sẽ tạo tài khoản người dùng bằng createUserWithEmailAndPassword từ firebase/auth . Nếu bước này không thành công (ví dụ: email đã bị lấy mất), chúng tôi sẽ thông báo cho người dùng bằng cách cập nhật lỗi.


Nếu mọi việc suôn sẻ, chúng tôi chuyển hướng người dùng đến trang /login . Có lẽ bây giờ bạn đang bối rối, và bạn có quyền như vậy. /login chưa tồn tại. Chúng tôi chỉ đang chuẩn bị cho những gì sắp xảy ra.


Khi bạn truy cập http://localhost:3000/register , trang sẽ trông gần giống như thế này:


Trang đăng ký


Tạo trang đăng nhập

Bây giờ, người dùng đã có thể đăng ký, hãy để họ chứng minh danh tính của mình


Tạo trang đăng nhập trong ./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> ); }


Như bạn thấy, nó khá giống với trang đăng ký. Hãy tập trung vào phần quan trọng:

 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); } }


Đó là nơi tất cả các phép thuật xảy ra. Chúng tôi sử dụng signInEmailAndPassword từ firebase/auth để truy xuất idToken của người dùng.


Sau đó, chúng tôi gọi điểm cuối /api/login do phần mềm trung gian hiển thị. Điểm cuối này cập nhật cookie trình duyệt của chúng tôi bằng thông tin đăng nhập của người dùng.


Cuối cùng, chúng tôi chuyển hướng người dùng đến trang chủ bằng cách gọi router.push("/");


Trang đăng nhập sẽ trông đại khái như thế này



Hãy thử nghiệm nó!


Truy cập http://localhost:3000/register , nhập một số địa chỉ email và mật khẩu ngẫu nhiên để tạo tài khoản. Sử dụng các thông tin đăng nhập đó trong trang http://localhost:3000/login . Sau khi nhấp vào Enter , bạn sẽ được chuyển hướng đến trang chủ siêu an toàn


Trang chủ siêu an toàn




Cuối cùng chúng ta cũng đã thấy được trang chủ cá nhân , cực kỳ an toàn của riêng mình ! Nhưng chờ đã! Chúng ta thoát ra bằng cách nào?


Chúng ta cần thêm nút đăng xuất để không tự khóa mình khỏi thế giới mãi mãi (hoặc 12 ngày).


Trước khi bắt đầu, chúng ta cần tạo một thành phần ứng dụng khách có thể đăng xuất chúng ta bằng SDK ứng dụng khách Firebase.


Hãy tạo một tệp mới trong ./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> ); }


Như bạn có thể nhận thấy, đây là phiên bản được sửa đổi một chút của ./app/page.tsx của chúng tôi. Chúng tôi phải tạo một thành phần máy khách riêng biệt vì getTokens chỉ hoạt động bên trong các thành phần máy chủtrình xử lý tuyến API , trong khi signOutuseRouter yêu cầu phải chạy trong ngữ cảnh máy khách. Tôi biết hơi phức tạp một chút, nhưng nó thực sự khá mạnh mẽ. Tôi sẽ giải thích sau.


Hãy tập trung vào quá trình đăng xuất

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


Đầu tiên, chúng tôi đăng xuất khỏi SDK khách hàng Firebase. Sau đó, chúng tôi gọi điểm cuối /api/logout do phần mềm trung gian hiển thị. Chúng tôi kết thúc bằng cách chuyển hướng người dùng đến trang /login .


Hãy cập nhật trang chủ máy chủ của chúng tôi. Đi tới ./app/page.tsx và dán đoạn sau

 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} />; }


Giờ đây, thành phần máy chủ Home của chúng tôi chỉ chịu trách nhiệm tìm nạp mã thông báo của người dùng và chuyển nó xuống thành phần máy khách HomePage . Đây thực sự là mô hình khá phổ biến và hữu ích.


Hãy kiểm tra điều này:


Thì đấy! Bây giờ chúng ta có thể đăng nhập và đăng xuất khỏi ứng dụng theo ý muốn của mình. Điều đó thật hoàn hảo!

Hoặc là nó?


Khi người dùng chưa được xác thực cố gắng vào trang chủ bằng cách mở http://localhost:3000/, chúng tôi hiển thị 404: Không thể tìm thấy trang này.

Ngoài ra, người dùng được xác thực vẫn có thể truy cập trang http://localhost:3000/registerhttp://localhost:3000/login mà không cần phải đăng xuất.


Chúng ta có thể làm tốt hơn.


Có vẻ như chúng tôi cần thêm một số logic chuyển hướng. Hãy xác định một số quy tắc:

  • Khi người dùng được xác thực cố gắng truy cập các trang /register/login , chúng ta nên chuyển hướng họ đến /
  • Khi người dùng chưa được xác thực cố gắng truy cập / trang, chúng tôi nên chuyển hướng họ đến /login


Middleware là một trong những cách tốt nhất để xử lý chuyển hướng trong ứng dụng Next.js. May mắn thay, authMiddleware hỗ trợ một số tùy chọn và chức năng trợ giúp để xử lý nhiều tình huống chuyển hướng.


Hãy mở tệp middleware.ts và dán phiên bản cập nhật này

 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", ], };


Đó phải là nó. Chúng tôi đã triển khai tất cả các quy tắc chuyển hướng. Hãy phá vỡ điều này.


 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 được gọi khi thông tin xác thực người dùng hợp lệ được đính kèm với yêu cầu, tức là. người dùng được xác thực. Nó được gọi với đối tượng tokens là đối tượng đầu tiên và Tiêu đề yêu cầu được sửa đổi làm đối số thứ hai. Nó sẽ được giải quyết bằng NextResponse .


redirectToHome từ next-firebase-auth-edge là một hàm trợ giúp trả về một đối tượng có thể được đơn giản hóa thành NextResponse.redirect(new URL(“/“))


Bằng cách kiểm tra PUBLIC_PATHS.includes(request.nextUrl.pathname) , chúng tôi xác thực xem người dùng được xác thực có cố gắng truy cập trang /login hoặc /register hay không và chuyển hướng về trang chủ nếu đúng như vậy.



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


handleInvalidToken được gọi khi điều gì đó được mong đợi xảy ra. Một trong những sự kiện được mong đợi này là người dùng nhìn thấy ứng dụng của bạn lần đầu tiên, từ một thiết bị khác hoặc sau khi thông tin xác thực đã hết hạn.


Biết rằng handleInvalidToken được gọi cho người dùng chưa được xác thực, chúng ta có thể tiến hành quy tắc thứ hai: Khi người dùng chưa được xác thực cố gắng truy cập / trang, chúng ta nên chuyển hướng họ đến /login


Vì không có điều kiện nào khác cần đáp ứng nên chúng tôi chỉ trả về kết quả của redirectToLogin có thể được đơn giản hóa thành NextResponse.redirect(new URL(“/login”)) . Nó cũng đảm bảo người dùng không rơi vào vòng lặp chuyển hướng.


Cuối cùng,

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


Ngược lại với handleInvalidToken , handleError được gọi khi có điều gì đó không mong muốn* xảy ra và có thể cần được điều tra. Bạn có thể tìm thấy danh sách các lỗi có thể xảy ra cùng với mô tả của chúng trong tài liệu


Trong trường hợp có lỗi, chúng tôi ghi lại thông tin này và chuyển hướng người dùng đến trang đăng nhập một cách an toàn


* handleError có thể được gọi với lỗi INVALID_ARGUMENT sau khi khóa công khai của Google được cập nhật.

Đây là hình thức luân chuyển key và được mong đợi . Xem vấn đề Github này để biết thêm thông tin


Bây giờ, thế là xong. Cuối cùng.


Hãy đăng xuất khỏi ứng dụng web của chúng tôi và mở http://localhost:3000/ . Chúng ta sẽ được chuyển hướng đến trang /login .

Hãy đăng nhập lại và thử nhập http://localhost:3000/login . Chúng ta nên được chuyển hướng đến / trang.


Chúng tôi không chỉ cung cấp trải nghiệm người dùng liền mạch. next-firebase-auth-edge là thư viện có kích thước gói bằng 0, chỉ hoạt động trong máy chủ của Ứng dụng và không giới thiệu mã phía máy khách bổ sung. Gói kết quả thực sự là tối thiểu . Đó là những gì tôi gọi là hoàn hảo.


Ứng dụng của chúng tôi hiện đã được tích hợp hoàn toàn với Xác thực Firebase cả trong các thành phần Máy chủ và Máy khách. Chúng tôi sẵn sàng phát huy hết tiềm năng của Next.js!




Mã nguồn của ứng dụng có thể được tìm thấy trong next-firebase-auth-edge/examples/next-Typescript-minimal


Lời kết

Trong hướng dẫn này, chúng tôi đã tiến hành tích hợp ứng dụng Next.js mới với Xác thực Firebase.


Mặc dù khá rộng rãi nhưng bài viết đã bỏ qua một số phần quan trọng của quy trình xác thực, chẳng hạn như biểu mẫu đặt lại mật khẩu hoặc các phương thức đăng nhập khác ngoài email và mật khẩu.


Nếu quan tâm đến thư viện, bạn có thể xem trước trang demo đầy đủ của next-firebase-auth-edge khởi động .

Nó có tính năng tích hợp Firestore , Hành động máy chủ , hỗ trợ Kiểm tra ứng dụng và hơn thế nữa


Thư viện cung cấp một trang tài liệu chuyên dụng với rất nhiều ví dụ


Nếu bạn thích bài viết, tôi sẽ đánh giá cao việc gắn dấu sao cho kho lưu trữ next-firebase-auth-edge . Chúc mừng! 🎉



Phần thưởng – Triển khai ứng dụng lên Vercel

Hướng dẫn bổ sung này sẽ hướng dẫn bạn cách triển khai ứng dụng Next.js của bạn lên Vercel

Tạo kho lưu trữ git

Để có thể triển khai lên Vercel, bạn cần tạo kho lưu trữ cho ứng dụng mới của mình.


Đi tới https://github.com/ và tạo một kho lưu trữ mới.


create-next-app đã khởi tạo kho lưu trữ git cục bộ cho chúng tôi, vì vậy bạn chỉ cần truy cập thư mục gốc của dự án và chạy:


 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


Thêm dự án Vercel mới

Truy cập https://vercel.com/ và đăng nhập bằng tài khoản Github của bạn


Sau khi bạn đăng nhập, hãy truy cập trang Tổng quan của Vercel và nhấp vào Thêm mới > Dự án


Nhấp vào Nhập bên cạnh kho lưu trữ Github mà chúng tôi vừa tạo. Chưa triển khai.


Trước khi triển khai, chúng tôi cần cung cấp cấu hình dự án. Hãy thêm một số biến môi trường:

Triển khai tới Vercel


Hãy nhớ đặt USE_SECURE_COOKIES thành true , vì Vercel sử dụng HTTPS theo mặc định


Bây giờ, chúng ta đã sẵn sàng nhấp vào Triển khai


Đợi một hoặc hai phút và bạn sẽ có thể truy cập ứng dụng của mình bằng url tương tự như sau: https://next-typescript-minimal-xi.vercel.app/


Xong. Tôi cá là bạn không ngờ nó lại dễ dàng đến vậy.




Nếu bạn thích hướng dẫn này, tôi sẽ đánh giá cao việc gắn dấu sao cho kho lưu trữ next-firebase-auth-edge .


Bạn cũng có thể cho tôi biết phản hồi của bạn trong phần bình luận. Chúc mừng! 🎉