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.
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ụng và Thà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.
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ết và vô 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.
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-edge
và firebase/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.
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 Firebase và tạ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
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
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.
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
Hướng dẫn này giả sử bạn đã cài đặt Node.js và npm . 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.
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 TypeScript
và 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
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:
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_EMAIL
và FIREBASE_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_CURRENT
và AUTH_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_KEY
là Khóa API Web được lấy từ trang chung Cài đặt dự án
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
là id dự án của bạn .firebaseapp.com
NEXT_PUBLIC_FIREBASE_DATABASE_URL
là id 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
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 };
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
và /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
và /api/logout
của chúng ta.
Để 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:
token
là string
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ò!
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
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 password
và confirmation
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:
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'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
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ủ và trình xử lý tuyến API , trong khi signOut
và useRouter
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/register và http://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:
/register
và /login
, chúng ta nên chuyển hướng họ đến /
/
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
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! 🎉
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
Để 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
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:
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! 🎉