在本章中,我们将涵盖以下主题:
- 什么是身份验证。
- 如何使用 NextAuth.js 向应用程序添加身份验证。
- 如何使用中间件重定向用户并保护路由。
- 如何使用 React 的 useFormStatus 和 useFormState 处理挂起状态和表单错误。
什么是身份验证?
身份验证是现代 Web 应用程序中的关键部分。它是系统检查用户是否是他们所声称的那个人的方法。
一个安全的网站通常使用多种方式来检查用户的身份。例如,在输入用户名和密码后,网站可能会向你的设备发送验证码或使用诸如 Google Authenticator 之类的外部应用程序。这种双因素身份验证(2FA)有助于提高安全性。即使有人知道你的密码,他们也不能在没有你的唯一令牌的情况下访问你的账户。
身份验证与授权
在 Web 开发中,身份验证和授权具有不同的作用:
- 身份验证是确保用户是他们所声称的那个人。你通过拥有的东西(如用户名和密码)证明你的身份。
- 授权是下一步。一旦确认了用户的身份,授权决定他们可以使用应用程序的哪些部分。
所以,身份验证检查你是谁,授权决定你在应用程序中可以做什么或访问什么。
创建登录路由
首先在应用程序中创建一个新的路由 /login
并粘贴以下代码:
// /app/login/page.tsx import AcmeLogo from '@/app/ui/acme-logo'; import LoginForm from '@/app/ui/login-form'; export default function LoginPage() { return ( <main className="flex items-center justify-center md:h-screen"> <div className="relative mx-auto flex w-full max-w-[400px] flex-col space-y-2.5 p-4 md:-mt-32"> <div className="flex h-20 w-full items-end rounded-lg bg-blue-500 p-3 md:h-36"> <div className="w-32 text-white md:w-36"> <AcmeLogo /> </div> </div> <LoginForm /> </div> </main> ); }
你会注意到页面导入了 <LoginForm />
,你将在本章稍后更新它。
设置 NextAuth.js
我们将使用 NextAuth.js 向应用程序添加身份验证。NextAuth.js 抽象了管理会话、登录和登出等方面的复杂性。尽管你可以手动实现这些功能,但过程可能耗时且容易出错。NextAuth.js 简化了这一过程,为 Next.js 应用程序提供了统一的身份验证解决方案。
安装 NextAuth.js:
pnpm i next-auth@beta
生成一个应用程序的密钥,用于加密 cookies,以确保用户会话的安全:
openssl rand -base64 32
在你的 .env
文件中,添加生成的密钥:
.env AUTH_SECRET=your-secret-key
添加页面选项
在项目的根目录下创建一个 auth.config.ts
文件,导出一个 authConfig
对象。该对象将包含 NextAuth.js 的配置选项:
// /auth.config.ts import type { NextAuthConfig } from 'next-auth'; export const authConfig = { pages: { signIn: '/login', }, };
使用中间件保护路由
接下来,添加逻辑以保护你的路由。这将防止用户在未登录时访问仪表板页面。
// /auth.config.ts import type { NextAuthConfig } from 'next-auth'; export const authConfig = { pages: { signIn: '/login', }, callbacks: { authorized({ auth, request: { nextUrl } }) { const isLoggedIn = !!auth?.user; const isOnDashboard = nextUrl.pathname.startsWith('/dashboard'); if (isOnDashboard) { if (isLoggedIn) return true; return false; // Redirect unauthenticated users to login page } else if (isLoggedIn) { return Response.redirect(new URL('/dashboard', nextUrl)); } return true; }, }, providers: [], // Add providers with an empty array for now } satisfies NextAuthConfig;
接下来,在项目的根目录下创建一个 middleware.ts
文件并粘贴以下代码:
// /middleware.ts import NextAuth from 'next-auth'; import { authConfig } from './auth.config'; export default NextAuth(authConfig).auth; export const config = { matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], };
添加凭证提供者
添加 Credentials
提供者允许用户使用用户名和密码登录:
// /auth.ts import NextAuth from 'next-auth'; import { authConfig } from './auth.config'; import Credentials from 'next-auth/providers/credentials'; export const { auth, signIn, signOut } = NextAuth({ ...authConfig, providers: [Credentials({})], });
更新登录表单
连接认证逻辑与登录表单:
// app/ui/login-form.tsx 'use client'; import { lusitana } from '@/app/ui/fonts'; import { AtSymbolIcon, KeyIcon, ExclamationCircleIcon, } from '@heroicons/react/24/outline'; import { ArrowRightIcon } from '@heroicons/react/20/solid'; import { Button } from '@/app/ui/button'; import { useFormState, useFormStatus } from 'react-dom'; import { authenticate } from '@/app/lib/actions'; export default function LoginForm() { const [errorMessage, dispatch] = useFormState(authenticate, undefined); return ( <form action={dispatch} className="space-y-3"> <div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8"> <h1 className={`${lusitana.className} mb-3 text-2xl`}> Please log in to continue. </h1> <div className="w-full"> <div> <label className="mb-3 mt-5 block text-xs font-medium text-gray-900" htmlFor="email" > Email </label> <div className="relative"> <input className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500" id="email" type="email" name="email" placeholder="Enter your email address" required /> <AtSymbolIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" /> </div> </div> <div className="mt-4"> <label className="mb-3 mt-5 block text-xs font-medium text-gray-900" htmlFor="password" > Password </label> <div className="relative"> <input className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500" id="password" type="password" name="password" placeholder="Enter password" required minLength={6} /> <KeyIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" /> </div> </div> </div> <LoginButton /> <div className="flex h-8 items-end space-x-1" aria-live="polite" aria-atomic="true" > {errorMessage && ( <> <ExclamationCircleIcon className="h-5 w-5 text-red-500" /> <p className="text-sm text-red-500">{errorMessage}</p> </> )} </div> </div> </form> ); } function LoginButton() { const { pending } = useFormStatus(); return ( <Button className="mt-4 w-full" aria-disabled={pending}> Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" /> </Button> ); }
添加注销功能
在 <SideNav />
组件中调用 signOut
函数以添加注销功能:
// /ui/dashboard/sidenav.tsx import Link from 'next/link'; import NavLinks from '@/app/ui/dashboard/nav-links'; import AcmeLogo from '@/app/ui/acme-logo'; import { PowerIcon } from '@heroicons/react/24/outline'; import { signOut } from '@/auth'; export default function SideNav() { return ( <div className="flex h-full flex-col px-3 py-4 md:px-2"> // ... <div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2"> <NavLinks /> <div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div> <form action={async () => { 'use server'; await signOut(); }} > <button className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3"> <PowerIcon className="w-6" /> <div className="hidden md:block">Sign Out</div> </button> </form> </div> </div> ); }
试试吧
现在,你应该可以使用以下凭据登录和注销应用程序:
- 电子邮件:[email protected]
- 密码:123456