paint-brush
Cách triển khai xác thực trong Next.js 14 với NextAuth.js, Shadcn/ui, Reac-hook-form và Zodtừ tác giả@ljaviertovar
2,035 lượt đọc
2,035 lượt đọc

Cách triển khai xác thực trong Next.js 14 với NextAuth.js, Shadcn/ui, Reac-hook-form và Zod

từ tác giả L Javier Tovar19m2024/03/01
Read on Terminal Reader

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

Trong hệ sinh thái JavaScript và cụ thể hơn là trong các ứng dụng được phát triển bằng Next.js, một trong những thư viện nổi bật nhất để xử lý xác thực là NextAuth.js. Công cụ này cung cấp giải pháp đơn giản và dễ thực hiện để thêm xác thực vào ứng dụng của chúng tôi. Điều tốt nhất là tính linh hoạt của nó; nó cho phép tích hợp các nhà cung cấp xác thực khác nhau như Google, Facebook và Twitter, ngoài xác thực dựa trên thông tin xác thực, chẳng hạn như email và mật khẩu cổ điển.
featured image - Cách triển khai xác thực trong Next.js 14 với NextAuth.js, Shadcn/ui, Reac-hook-form và Zod
L Javier Tovar HackerNoon profile picture
0-item
1-item

Hướng dẫn toàn diện: Thông tin xác thực (Email và mật khẩu)

Việc đảm bảo tính bảo mật và quyền riêng tư của người dùng trở nên quan trọng hơn bao giờ hết. Xác thực web đóng một vai trò quan trọng trong khía cạnh này, đóng vai trò là tuyến phòng thủ đầu tiên để bảo vệ thông tin và dữ liệu của người dùng.


Ngày nay, chúng tôi có các công cụ như NextAuth.js giúp công việc của chúng tôi dễ dàng hơn nhiều, cho phép chúng tôi triển khai các loại xác thực khác nhau một cách dễ dàng trong các ứng dụng Next.js của mình.


Trong loạt hướng dẫn này, chúng ta sẽ xây dựng một hệ thống xác thực hoàn chỉnh trong Next.js 14, bắt đầu từ những điều cơ bản: xác thực bằng email và mật khẩu.

NextAuth.js (Auth.js) là gì?

Trong hệ sinh thái JavaScript và cụ thể hơn là trong các ứng dụng được phát triển bằng Next.js, một trong những thư viện nổi bật nhất để xử lý xác thực là NextAuth.js.


Công cụ này cung cấp giải pháp đơn giản và dễ thực hiện để thêm xác thực vào ứng dụng của chúng tôi. Điều tốt nhất là tính linh hoạt của nó; nó cho phép tích hợp các nhà cung cấp xác thực khác nhau như Google, Facebook và Twitter, ngoài xác thực dựa trên thông tin xác thực, chẳng hạn như email và mật khẩu cổ điển.

Triển khai xác thực thông tin xác thực

Xác thực thông tin xác thực đặc biệt hữu ích trong các ứng dụng mà bạn cần toàn quyền kiểm soát quá trình xác thực và lưu trữ thông tin xác thực người dùng hoặc khi bạn không muốn dựa vào các nhà cung cấp xác thực bên ngoài.

Đang cài đặt

  1. Tạo dự án Next.js mới bằng lệnh sau và làm theo các bước được chỉ định. Chúng ta sẽ sử dụng TypeScript và thư mục src/ .


 npx create-next-app@latest


2. Cài đặt các phụ thuộc cần thiết trong dự án. Lần này, chúng ta sẽ sử dụng pnpm ; bạn có thể sử dụng trình quản lý gói mà bạn chọn.


 pnpm install next-auth prisma react-hook-form zod, bcrypt


Đối với giao diện người dùng, chúng tôi sẽ sử dụng Shadcn/ui.


 pnpm dlx shadcn-ui@latest init


  • lăng kính : là bộ công cụ cơ sở dữ liệu nguồn mở. Chúng tôi sẽ sử dụng nó để lưu trữ thông tin xác thực của người dùng.

  • xác thực tiếp theo : Xác thực cho Next.js.

  • dạng phản ứng móc : một thư viện giúp bạn xác thực các biểu mẫu trong React.

  • zod : trình xác nhận dữ liệu.

  • bcrypt : để băm mật khẩu.

  • shadcn/ui : một tập hợp các thành phần có thể tái sử dụng.


3. Tạo cấu trúc sau cho dự án:


 ... ├── prisma/ ... ├── src/ │ ├── actions/ │ │ └── auth-actions.tsx │ ├── app/ │ │ ├── api/auth/[...nextauth] │ │ │ └── route.ts │ │ ├── auth/ │ │ │ ├── signin │ │ │ │ └── page.tsx │ │ │ └── signup │ │ │ └── page.tsx │ │ │ ... │ ├── components/ │ │ ├── auth/ │ │ │ ├── auth-buttons.tsx │ │ │ ├── signin-form.tsx │ │ │ ├── signup-form.tsx │ │ │ └── user-nav.ts │ │ ├── ui/ │ │ │ ... │ │ ├── auth-provider.tsx │ │ ├── icons.tsx │ │ └── theme-provider.tsx │ ├── lib/ │ │ ├── prisma.ts │ │ ├── types.d.ts │ │ └── utils.ts │ ... ...


Thiết lập Prisma

Chúng tôi sẽ sử dụng Prisma để lưu trữ và truy xuất người dùng trong cơ sở dữ liệu. Prisma cho phép tích hợp các loại cơ sở dữ liệu khác nhau để bạn có thể sử dụng bất kỳ cơ sở dữ liệu nào bạn cần, chúng tôi sẽ sử dụng SQLite.


  1. Đang khởi tạo Prisma .


 npx prisma init --datasource-provider sqlite


Điều này tạo ra thư mục lăng kính với các lược đồ của nó.


  1. Tạo các mô hình.

Để tạo các mô hình, chúng ta sẽ sử dụng các mô hình được cung cấp bởi @auth/prisma-adapter và tùy chỉnh chúng một chút như sau:


prisma/schema.prisma :


 generator client { provider = "prisma-client-js" output = "../../node_modules/.prisma/client" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } ... model User { id String @id @default(cuid()) username String password String email String @unique emailVerified DateTime? phone String? image String? } ...


3. Tạo cái đầu tiên di cư .


 npx prisma migrate dev --name first-migration


Với lệnh này, nhiều tệp hơn đã được tạo trong thư mục Prisma và cơ sở dữ liệu đã được đồng bộ hóa với các mô hình.


4. khách hàng Prisma

Cuối cùng, chúng ta tạo Prisma Client.


lib/prisma.ts :


 import { PrismaClient } from "@prisma/client"; const globalForPrisma = global as unknown as { prisma: PrismaClient; }; export const prisma = globalForPrisma.prisma || new PrismaClient(); if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; export default prisma;


Thiết lập NextAuth.js

  1. Tạo biến env


 # Secret key for NextAuth.js, used for encryption and session security. It should be a long, # random string unique to your application. NEXTAUTH_SECRET=XXX3B2CC28F123456C6934531CXXXXX # Base URL for your Next.js app, used by NextAuth.js for redirects and callbacks. NEXTAUTH_URL=http://localhost:3000/


  1. Tạo tuyến đường xác thực.

Đường dẫn này cho phép xử lý tất cả các yêu cầu xác thực (chẳng hạn như đăng nhập, đăng xuất và gọi lại của nhà cung cấp) trên một điểm cuối duy nhất.


src/app/api/auth/[...nextauth]


  1. Tạo các nhà cung cấp.


 ... // Imports the Prisma User type for typing. import { User } from '@prisma/client' // Configuration of authentication options for NextAuth. export const authOptions: AuthOptions = { ... // Defines authentication providers, in this case, only credentials. providers: [ CredentialsProvider({ name: 'Credentials', // Defines the required fields for authentication. credentials: { email: { label: 'Email', type: 'text' }, password: { label: 'Password', type: 'password' }, }, // Function to authenticate the user with the provided credentials. async authorize(credentials) { // Searches for the user in the database by email. const user = await prisma.user.findUnique({ where: { email: credentials?.email, }, }) // Checks if the user exists and if the password is correct. if (!user) throw new Error('User name or password is not correct') if (!credentials?.password) throw new Error('Please Provide Your Password') const isPasswordCorrect = await bcrypt.compare(credentials.password, user.password) if (!isPasswordCorrect) throw new Error('User name or password is not correct') // Returns the user without including the password. const { password, ...userWithoutPass } = user return userWithoutPass }, }), ], } // Exports the configured NextAuth handler to handle GET and POST requests. const handler = NextAuth(authOptions) export { handler as GET, handler as POST }


4. Tạo Nhà cung cấp xác thực.


src/components/auth-provider.tsx :


 'use client' import { SessionProvider } from 'next-auth/react' export default function AuthProvider({ children }: { children: React.ReactNode }) { return <SessionProvider>{children}</SessionProvider> }


Thành phần này hoạt động như một nhà cung cấp phiên cho các ứng dụng Next.js sử dụng NextAuth để xác thực.


Việc gói các thành phần hoặc trang trong nhà cung cấp này cấp cho chúng quyền truy cập vào ngữ cảnh phiên, cho phép các thành phần con sử dụng chức năng và hook NextAuth, chẳng hạn như useSession , để truy cập hoặc sửa đổi trạng thái phiên hiện tại của người dùng.


src/app/layout.tsx :


 /* All imports */ export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang='en' suppressHydrationWarning > <body className={`${inter.className} relative`}> <AuthProvider> <main>{children}</main> </AuthProvider> </body> </html> ) }


Thiết lập giao diện người dùng với Shadcn/ui

  1. Cài đặt shadcn/ui theo hướng dẫn tài liệu (bước 2) .


 Would you like to use TypeScript (recommended)? yes Which style would you like to use? › Default Which color would you like to use as base color? › Slate Where is your global CSS file? › src/app/globals.css Do you want to use CSS variables for colors? › yes Are you using a custom tailwind prefix eg. tw-? Leave blank Where is your tailwind.config.js located? › tailwind.config.js Configure the import alias for components: › @/components Configure the import alias for utils: › @/lib/utils Are you using React Server Components? › yes


Bạn có thể sử dụng chủ đề bạn chọn.


2. Thực hiện Chế độ tối


src/app/layout.tsx :


 /* All imports */ export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang='en' suppressHydrationWarning > <body className={`${inter.className} relative`}> <AuthProvider> <ThemeProvider attribute='class' defaultTheme='dark' enableSystem disableTransitionOnChange > <main>{children}</main> <Toaster /> </ThemeProvider> </AuthProvider> </body> </html> ) }


2. Cài đặt các thành phần shadcn/ui sau:


 pnpm dlx shadcn-ui@latest add avatar button dropdown-menu form input label tabs toast


Tạo thành phần xác thực

src/components/auth-buttons.tsx :


 'use client' import Link from 'next/link' import { signIn, useSession } from 'next-auth/react' import { Button } from '../ui/button' import { UserNav } from './user-nav' export default function AuthButtons() { // Use the useSession hook to access session data const { data: session } = useSession() return ( <div className='flex justify-end gap-4'> {session && session.user ? ( <UserNav user={session.user} /> ) : ( <> <Button size={'sm'} variant={'secondary'} onClick={() => signIn()} > Sign In </Button> <Button size={'sm'} asChild className='text-foreground' > <Link href='/auth/signup'>Sign Up</Link> </Button> </> )} </div> ) }


Thành phần này hiển thị động các tùy chọn xác thực dựa trên trạng thái phiên của người dùng. Nếu người dùng đã đăng nhập, nó sẽ hiển thị điều hướng dành riêng cho người dùng. Mặt khác, nó cung cấp các nút để đăng nhập hoặc đăng ký, tận dụng khả năng định tuyến của Next.js và khả năng xác thực của NextAuth để mang lại trải nghiệm mượt mà cho người dùng.


Sửa đổi trang chủ và thêm các nút xác thực. Cái này nó thì trông như thế nào:


Trang chủ

src/components/auth/signup-form.tsx :


 'use client' /* all imports */ // Function to register a new user import { registerUser } from '@/actions/auth-actions' // Define the validation schema for the signup form using Zod const formSchema = z .object({ username: z .string({ required_error: 'Username is required', }) .min(2, 'User name must have at least 2 characters') .max(12, 'Username must be up to 12 characters') .regex(new RegExp('^[a-zA-Z0-9]+$'), 'No special characters allowed!'), email: z.string({ required_error: 'Email is required' }).email('Please enter a valid email address'), password: z .string({ required_error: 'Password is required' }) .min(6, 'Password must have at least 6 characters') .max(20, 'Password must be up to 20 characters'), confirmPassword: z .string({ required_error: 'Confirm your password is required' }) .min(6, 'Password must have at least 6 characters') .max(20, 'Password must be up to 20 characters'), }) .refine(values => values.password === values.confirmPassword, { message: "Password and Confirm Password doesn't match!", path: ['confirmPassword'], }) // Type inference for form inputs based on the Zod schema type InputType = z.infer<typeof formSchema> export function SignUpForm() { const [isLoading, setIsLoading] = useState(false) const { toast } = useToast() // Hook to show toast notifications // Initialize form handling with React Hook Form and Zod for validation const form = useForm<InputType>({ resolver: zodResolver(formSchema), }) // Handles form submission async function onSubmit(values: InputType) { try { setIsLoading(true) const { confirmPassword, ...user } = values // Exclude confirmPassword from data to be sent const response = await registerUser(user) // Register the user if ('error' in response) { toast({ title: 'Something went wrong!', description: response.error, variant: 'success', }) } else { toast({ title: 'Account Created!', description: 'Your account has been created successfully! You can now login.', }) } } catch (error) { console.error(error) toast({ title: 'Something went wrong!', description: "We couldn't create your account. Please try again later!", variant: 'destructive', }) } finally { setIsLoading(false) } } return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <div className='grid gap-2'> // Each FormField validates and displays an input <FormField control={form.control} name='username' render={({ field }) => ( <FormItem> <FormControl> <div className='flex items-center gap-2'> <Icons.user className={`${form.formState.errors.username ? 'text-destructive' : 'text-muted-foreground'} `} /> <Input placeholder='Your Username' className={`${form.formState.errors.username && 'border-destructive bg-destructive/30'}`} {...field} /> </div> </FormControl> <FormMessage /> </FormItem> )} /> // Repeated structure for email, password, and confirmPassword with respective validations and icons <Button className='text-foreground mt-4' disabled={isLoading} // Disable button during form submission > {isLoading && <Icons.spinner className='mr-2 h-4 w-4 animate-spin' />} // Show loading icon if isLoading is true Sign Up </Button> </div> </form> </Form> ) }


Thành phần này đóng gói biểu mẫu đăng ký người dùng, sử dụng biểu mẫu Reac-hook để quản lý trạng thái biểu mẫu và Zod để xác thực lược đồ.


Mặc dù đây không phải là hướng dẫn về các công nghệ này nhưng chúng tôi chỉ sử dụng những kiến thức cơ bản về chúng, nếu có thêm câu hỏi, bạn có thể truy cập tài liệu của chúng.


Tôi đã thêm một số kiểu khác vào trang và nó trông như thế này:


Trang đăng ký



src/actions/auth-action/ts :


 'use server' /* all imports */ export async function registerUser(user: Omit<User, 'id' | 'phone' | 'emailVerified' | 'image'>) { try { // Attempt to create a new user record in the database const result = await prisma.user.create({ data: { ...user, // Hash the password before storing it password: await bcrypt.hash(user.password, 10), }, }) return result } catch (error) { console.log(error) // Handle known request errors from Prisma if (error instanceof Prisma.PrismaClientKnownRequestError) { // Check for unique constraint failure (eg, email already exists) if (error.code === 'P2002') { return { error: 'Email already exists.' } } } // Return a generic error message for any other errors return { error: 'An unexpected error occurred.' } } }


Hàm registerUser được thiết kế để đăng ký người dùng mới một cách an toàn bằng cách tạo một bản ghi trong cơ sở dữ liệu với thông tin người dùng được cung cấp, ngoại trừ các trường như id , phone , emailVerifiedimage .


Nó sử dụng bcrypt để băm mật khẩu của người dùng để lưu trữ an toàn.


Để kiểm tra quá trình đăng ký của chúng tôi và xác thực rằng người dùng đang đăng ký chính xác, chúng tôi cần thêm một số lệnh gọi lại; đây là những chức năng cho phép bạn tùy chỉnh hành vi xác thực và quản lý phiên.


src/app/api/auth/[...nextauth] :


 ... export const authOptions: AuthOptions = { // Define custom pages for authentication flow pages: { signIn: '/auth/signin', // Custom sign-in page }, // Configure session management to use JSON Web Tokens (JWT) session: { strategy: 'jwt', }, // JWT configuration, including secret for token signing jwt: { secret: process.env.NEXTAUTH_SECRET, // Secret used to sign the JWT, stored in environment variables }, ... // Callbacks for customizing JWT and session behaviors callbacks: { // Callback to modify the JWT content. Adds user information if available. async jwt({ token, user }) { if (user) token.user = user as User // Cast user object to User type and assign to token return token }, // Callback to modify session content. Adds user information to the session. async session({ token, session }) { session.user = token.user // Assign user information from token to session return session }, }, } ...


Gọi lại jwt : lệnh gọi lại này được thực thi bất cứ khi nào Mã thông báo web JSON (JWT) được tạo hoặc cập nhật trong vòng đời xác thực. Nó cho phép bạn sửa đổi nội dung của mã thông báo trước khi nó được ký và gửi đến máy khách hoặc được lưu trữ trên máy chủ.


Điều này hữu ích khi thêm thông tin bổ sung vào mã thông báo, thông tin này có thể liên quan đến logic ứng dụng của bạn.


session gọi lại: lệnh gọi lại này được gọi mỗi khi dữ liệu phiên được đọc, chẳng hạn như trong quá trình hiển thị phía máy chủ hoặc trong các yêu cầu API được bảo vệ. Nó cho phép sửa đổi dữ liệu phiên trước khi gửi đến máy khách.


Điều này đặc biệt hữu ích khi thêm hoặc sửa đổi dữ liệu phiên dựa trên thông tin được lưu trữ trong JWT hoặc các tiêu chí khác.


Cuối cùng, chúng ta cần mở rộng định nghĩa loại Session NextAuth và loại JWT để bao gồm thông tin người dùng bổ sung.


src/lib/types.d.ts :


 import { User } from '@prisma/client' declare module 'next-auth' { interface Session { user: User } } declare module 'next-auth/jwt' { interface JWT { user: User } }


Bây giờ, nếu chúng ta điền vào biểu mẫu và gửi nó, chúng ta sẽ có thể nâng cốc chúc mừng thành công. Để xác minh rằng người dùng đã được lưu trong cơ sở dữ liệu, chúng ta có thể xem đồ họa các bảng được tạo bởi Prisma bằng lệnh sau:


 nxp prisma studio


Chúng tôi sẽ có sẵn tuyến đường sau http://localhost:5555


Studio Prisma


src/components/auth/user-nav.tsx :


 /* all imports */ interface Props { user: User // Expect a user object of type User from Prisma client } export function UserNav({ user }: Props) { return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant='ghost' className='relative h-8 w-8 rounded-full' > <Avatar className='h-9 w-9'> <AvatarImage src='/img/avatars/01.png' alt='' /> <AvatarFallback>UU</AvatarFallback> </Avatar> </Button> </DropdownMenuTrigger> <DropdownMenuContent className='w-56' align='end' forceMount > <DropdownMenuLabel className='font-normal'> <div className='flex flex-col space-y-1'> <p className='text-sm font-medium leading-none'>{user.username}</p> <p className='text-xs leading-none text-muted-foreground'>{user.email}</p> </div> </DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem> <Link href={'/api/auth/signout'} // Link to the signout API route className='w-full' > Sign Out </Link> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ) } 


Trang chủ



src/components/auth/signin-form.tsx :


 /* all imports */ // Schema definition for form validation using Zod const formSchema = z.object({ email: z.string({ required_error: 'Please enter your email' }).email('Please enter a valid email address'), password: z.string({ required_error: 'Please enter your password', }), }) // Type inference for form inputs based on the Zod schema type InputType = z.infer<typeof formSchema> // Props definition, optionally including a callback URL interface Props { callbackUrl?: string } export function SignInForm({ callbackUrl }: Props) { const [isLoading, setIsLoading] = useState(false) const { toast } = useToast() const router = useRouter() // Hook to control routing const form = useForm<InputType>({ resolver: zodResolver(formSchema), // Set up Zod as the form validation resolver }) // Function to handle form submission async function onSubmit(values: InputType) { try { setIsLoading(true) // Attempt to sign in using the 'credentials' provider const response = await signIn('credentials', { redirect: false, // Prevent automatic redirection email: values.email, password: values.password, }) // Handle unsuccessful sign in attempts if (!response?.ok) { toast({ title: 'Something went wrong!', description: response?.error, variant: 'destructive', }) return } toast({ title: 'Welcome back! ', description: 'Redirecting you to your dashboard!', }) router.push(callbackUrl ? callbackUrl : '/') // Redirect to the callback URL or home page } catch (error) { toast({ title: 'Something went wrong!', description: "We couldn't create your account. Please try again later!", variant: 'destructive', }) } finally { setIsLoading(false) } } return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <div className='grid gap-2'> <div className='grid gap-1'> <FormField control={form.control} name='email' render={({ field }) => ( <FormItem> <FormControl> <div className='flex items-center gap-2'> <Icons.email className={`${form.formState.errors.email ? 'text-destructive' : 'text-muted-foreground'} `}/> <Input type='email' placeholder='Your Email' className={`${form.formState.errors.email && 'border-destructive bg-destructive/30'}`} {...field} /> </div> </FormControl> <FormMessage /> </FormItem> )} /> {/* Password field */} {/* Similar structure to email field, customized for password input */} </div> <Button className='text-foreground mt-4' disabled={isLoading} // Disable button while loading > {isLoading && <Icons.spinner className='mr-2 h-4 w-4 animate-spin' />} // Show loading spinner when processing Sign In </Button> </div> </form> </Form> ) } 


Trang đăng nhập


🎉 Thế là xong!


Chúng tôi đã hoàn tất việc triển khai xác thực cơ bản với NextAuth.js. Vẫn còn nhiều việc phải làm để có một hệ thống xác thực hoàn chỉnh và chúng tôi sẽ đề cập đến chúng trong các hướng dẫn tiếp theo.


🔗 Repo ở đây


Phần kết luận

Tóm lại, chúng tôi đã khám phá cách triển khai và tùy chỉnh hệ thống xác thực trong Next.js bằng NextAuth, cách mở rộng phiên và JWT để nâng cao khả năng quản lý người dùng cũng như cách xử lý các biểu mẫu với xác thực hiệu quả bằng cách sử dụng Reac-hook-form và Zod.


Bạn muốn kết nối với Tác giả?


Thích kết nối với bạn bè khắp nơi trên thế giới trên 𝕏 .


Cũng được xuất bản ở đây