在 Nextjs App Router 中使用 Authjs 进行用户身份验证
目录
初始设置
- 安装
-
配置
- nextauthconfig 设置
- 路由处理程序设置
- 中间件
- 在服务器端组件中获取会话
- 在客户端组件中获取会话
- 文件夹结构
实施身份验证:凭据和 google oauth
- 设置 prisma
- 凭证
-
添加 google oauth 提供商
- 设置 google oauth 应用程序
- 设置重定向 uri
- 设置环境变量
- 设置提供商
- 创建登录和注册页面
- 文件夹结构
初始设置
安装
npm install next-auth@beta
// env.local auth_secret=generatetd_random_value
配置
nextauthconfig 设置
// src/auth.ts import nextauth from "next-auth" export const config = { providers: [], } export const { handlers, signin, signout, auth } = nextauth(config)
它应该放在src文件夹内
providers 在 auth.js 中表示是可用于登录用户的服务。用户可以通过四种方式登录。
- 使用内置的 oauth 提供程序(例如 github、google 等...)
- 使用自定义 oauth 提供程序
- 使用电子邮件
- 使用凭证
https://authjs.dev/reference/nextjs#providers
路由处理程序设置
// src/app/api/auth/[...nextauth]/route.ts import { handlers } from "@/auth" // referring to the auth.ts we just created export const { get, post } = handlers
此文件用于使用 next.js app router 设置路由处理程序。
中间件
// src/middleware.ts import { auth } from "@/auth" export default auth((req) => { // add your logic here } export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], // it's default setting }
在src文件夹内写入。
如果写在 src 文件夹之外,中间件将无法工作。
中间件是一个允许您在请求完成之前运行代码的函数。它对于保护路由和处理整个应用程序的身份验证特别有用。
matcher 是 一个配置选项,用于指定哪些路由中间件应应用于。它有助于仅在必要的路由上运行中间件来优化性能。
示例匹配器: ['/dashboard/:path*'] 仅将中间件应用于仪表板路由。
https://authjs.dev/getting-started/session-management/protecting?framework=express#nextjs-middleware
在服务器端组件中获取会话
// src/app/page.tsx import { auth } from "@/auth" import { redirect } from "next/navigation" export default async function page() { const session = await auth() if (!session) { redirect('/login') } return ( <div> <h1>hello world!</h1> @@##@@ </div> ) }
在客户端组件中获取会话
// src/app/page.tsx "use client" import { usesession } from "next-auth/react" import { userouter } from "next/navigation" export default async function page() { const { data: session } = usesession() const router = userouter() if (!session.user) { router.push('/login') } return ( <div> <h1>hello world!</h1> @@##@@ </div> ) } // src/app/layout.tsx import type { metadata } from "next"; import "./globals.css"; import { sessionprovider } from "next-auth/react" export const metadata: metadata = { title: "create next app", description: "generated by create next app", }; export default function rootlayout({ children, }: readonly<{ children: react.reactnode; }>) { return ( <html lang="en"> <body> <sessionprovider> {children} </sessionprovider> </body> </html> ); }
文件夹结构
/src /app /api /auth [...nextauth] /route.ts // route handler layout.tsx page.tsx auth.ts // provider, callback, logic etc middleware.ts // a function before request
实施身份验证:凭据和 google oauth
设置棱镜
// prisma/schema.prisma model user { id string @id @default(cuid()) name string? email string? @unique emailverified datetime? image string? password string? accounts account[] sessions session[] } model account { // ... (standard auth.js account model) } model session { // ... (standard auth.js session model) } // ... (other necessary models)
// src/lib/prisma.ts import { prismaclient } from "@prisma/client" const globalforprisma = globalthis as unknown as { prisma: prismaclient } export const prisma = globalforprisma.prisma || new prismaclient() if (process.env.node_env !== "production") globalforprisma.prisma = prisma
证书
凭证,在身份验证的上下文中,指的是使用用户提供的信息验证用户身份的方法,通常是用户名(或电子邮件)和密码。
我们可以在 src/auth.ts 中添加凭据。
// src/auth.ts import nextauth from "next-auth"; import type { nextauthconfig } from "next-auth"; import credentials from "next-auth/providers/credentials" import { prismaadapter } from "@auth/prisma-adapter" import { prisma } from "@/lib/prisma" import bcrypt from 'bcryptjs'; export const config = { adapter: prismaadapter(prisma), providers: [ credentials({ credentials: { email: { label: "email", type: "text" }, password: { label: "password", type: "password" } }, authorize: async (credentials): promise<any> => { if (!credentials?.email || !credentials?.password) { return null; } try { const user = await prisma.user.findunique({ where: { email: credentials.email as string } }) if (!user || !user.hashedpassword) { return null } const ispasswordvalid = await bcrypt.compare( credentials.password as string, user.hashedpassword ) if (!ispasswordvalid) { return null } return { id: user.id as string, email: user.email as string, name: user.name as string, } } catch (error) { console.error('error during authentication:', error) return null } } }) ], secret: process.env.auth_secret, pages: { signin: '/login', }, session: { strategy: "jwt", }, callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id token.email = user.email token.name = user.name } return token }, async session({ session, token }) { if (session.user) { session.user.id = token.id as string session.user.email = token.email as string session.user.name = token.name as string } return session }, }, } satisfies nextauthconfig; export const { handlers, auth, signin, signout } = nextauth(config);
适配器:
- 将身份验证系统连接到数据库或数据存储解决方案的模块。
秘密:
- 这是一个随机字符串,用于哈希令牌、签名/加密 cookie 以及生成加密密钥。
- 这对于安全至关重要,应该保密。
- 在本例中,它是使用环境变量 auth_secret 设置的。
页面:
- 此对象允许您自定义身份验证页面的 url。
- 在您的示例中,signin: '/login' 表示登录页面将位于 '/login' 路由,而不是默认的 '/api/auth/signin'。
会话:
回调:
- 这些是在身份验证流程中的各个点调用的函数,允许您自定义流程。
jwt 回调:
- 它在创建或更新 jwt 时运行。
- 在您的代码中,它将用户信息(id、电子邮件、姓名)添加到令牌中。
会话回调:
- 每当检查会话时都会运行。
- 您的代码正在将用户信息从令牌添加到会话对象。
添加 google oauth 提供商
设置 google oauth 应用程序
从 gcp console 创建新的 oauth 客户端 id > api 和服务 > 凭据
创建后,保存您的客户端 id 和客户端密钥以供以后使用。
设置重定向 uri
当我们在本地工作时,设置http://localhost:3000/api/auth/callback/google
生产环境中,只需将 http://localhost:3000 替换为 https://-----即可。
设置环境变量
// .env.local google_client_id={client_id} google_client_secret={client_secret}
设置提供商
// src/auth.ts import googleprovider from "next-auth/providers/google" // add this import. export const { handlers, auth } = nextauth({ adapter: prismaadapter(prisma), providers: [ credentialsprovider({ // ... (previous credentials configuration) }), googleprovider({ clientid: process.env.google_client_id, clientsecret: process.env.google_client_secret, }), ], // ... other configurations })
https://authjs.dev/getting-started/authentication/oauth
创建登录和注册页面
//// ui pages // src/app/login/loginpage.tsx import link from 'next/link' import { loginform } from '@/components/auth/loginform' import { separator } from '@/components/auth/separator' import { authlayout } from '@/components/auth/authlayout' import { googleauthbutton } from '@/components/auth/googleauthbutton' export default function loginpage() { return ( <authlayout title="welcome back!"> <loginform /> <separator /> <googleauthbutton text="sign in with google" /> <div classname="mt-6 text-center"> <p classname="text-sm text-gray-400"> do not have an account?{' '} <link href="/signup" classname="pl-1.5 font-medium text-[#3ba55c] hover:text-[#2d7d46]"> sign up </link> </p> </div> </authlayout> ) } // src/app/signup/signuppage.tsx import link from 'next/link' import { signupform } from '@/components/auth/signupform' import { separator } from '@/components/auth/separator' import { authlayout } from '@/components/auth/authlayout' import { googleauthbutton } from '@/components/auth/googleauthbutton' export default function signuppage() { return ( <authlayout title="welcome!"> <signupform /> <separator /> <googleauthbutton text="sign up with google" /> <div classname="mt-6 text-center"> <p classname="text-sm text-gray-400"> already have an account?{' '} <link href="/login" classname="pl-1.5 font-medium text-[#3ba55c] hover:text-[#2d7d46]"> sign in </link> </p> </div> </authlayout> ) }
//// components // src/components/auth/authlayout.tsx import react from 'react' interface authlayoutprops { children: react.reactnode title: string } export const authlayout: react.fc<authlayoutprops> = ({ children, title }) => { return ( <div classname="min-h-screen bg-[#36393f] flex flex-col justify-center py-12 sm:px-6 lg:px-8"> <div classname="sm:mx-auto sm:w-full sm:max-w-md"> <h2 classname="mt-6 text-center text-3xl font-extrabold text-white"> {title} </h2> </div> <div classname="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> <div classname="bg-[#2f3136] py-8 px-4 shadow sm:rounded-lg sm:px-10"> {children} </div> </div> </div> ) } // src/components/auth/googleauthbutton.tsx import { signin } from "@/auth" import { button } from "@/components/ui/button" interface googleauthbuttonprops { text: string } export const googleauthbutton: react.fc<googleauthbuttonprops> = ({ text }) => { return ( <form action={async () => { "use server" await signin("google", { redirectto: '/' }) }} > <button classname="my-1 w-full bg-white text-gray-700 hover:bg-slate-100" > <svg classname="h-5 w-5 mr-2" viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="m22.56 12.25c0-.78-.07-1.53-.2-2.25h12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285f4"/> <path d="m12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53h2.18v2.84c3.99 20.53 7.7 23 12 23z" fill="#34a853"/> <path d="m5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09v7.07h2.18c1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#fbbc05"/> <path d="m12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15c17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#ea4335"/> <path d="m1 1h22v22h1z" fill="none"/> </svg> {text} </button> </form> ) } // src/components/auth/loginform.tsx 'use client' import { usetransition } from "react" import { useform } from "react-hook-form" import { form, formcontrol, formfield, formitem, formlabel, formmessage, } from "@/components/ui/form" import { input } from "@/components/ui/input" import { button } from "@/components/ui/button" import { loginresolver, loginschema } from "@/schema/login" import { usestate } from "react" import { userouter } from "next/navigation" import { formerror } from "@/components/auth/formerror" import { formsuccess } from "@/components/auth/formsuccess" import { login } from "@/app/actions/auth/login" import { loader2 } from "lucide-react" export const loginform = () => { const [error, seterror] = usestate<string | undefined>('') const [success, setsuccess] = usestate<string | undefined>('') const [ispending, starttransition] = usetransition() const router = userouter(); const form = useform<loginschema>({ defaultvalues: { email: '', password: ''}, resolver: loginresolver, }) const onsubmit = (formdata: loginschema) => { starttransition(() => { seterror('') setsuccess('') login(formdata) .then((data) => { if (data.success) { setsuccess(data.success) router.push('/setup') } else if (data.error) { seterror(data.error) } }) .catch((data) => { seterror(data.error) }) }) } return ( <form {...form}> <form onsubmit={form.handlesubmit(onsubmit)}> <div classname="space-y-3"> <formfield control={form.control} name="email" render={({ field }) => ( <formitem> <formlabel classname="text-white">email address</formlabel> <formcontrol> <input placeholder="enter your email address" {...field} disabled={ispending} classname="bg-[#40444b] text-white border-gray-600 focus:border-2 focus:border-[#2d7d46]" /> </formcontrol> <formmessage /> </formitem> )} /> <formfield control={form.control} name="password" render={({ field }) => ( <formitem> <formlabel classname="text-white">password</formlabel> <formcontrol> <input type="password" placeholder="enter your password" {...field} disabled={ispending} classname="bg-[#40444b] text-white border-gray-600 focus:border-2 focus:border-[#2d7d46]" /> </formcontrol> <formmessage /> </formitem> )} /> <formerror message={error} /> <formsuccess message={success} /> </div> <button type="submit" disabled={ispending} classname="mt-8 w-full bg-[#3ba55c] hover:bg-[#2d7d46] text-white" > {ispending ? ( <> <loader2 classname="mr-2 h-4 w-4 animate-spin" /> loading... </> ) : ( 'login' )} </button> </form> </form> ) } // src/components/auth/signupform.tsx 'use client' import { usetransition } from "react" import { useform } from "react-hook-form" import { form, formcontrol, formfield, formitem, formlabel, formmessage, } from "@/components/ui/form" import { input } from "@/components/ui/input" import { button } from "@/components/ui/button" import { signupresolver, signupschema } from "@/schema/signup" import { usestate } from "react" import { userouter } from "next/navigation" import { formerror } from "@/components/auth/formerror" import { formsuccess } from "@/components/auth/formsuccess" import { signup } from "@/app/actions/auth/signup" import { loader2 } from "lucide-react" export const signupform = () => { const [error, seterror] = usestate<string | undefined>('') const [success, setsuccess] = usestate<string | undefined>('') const [ispending, starttransition] = usetransition() const router = userouter(); const form = useform<signupschema>({ defaultvalues: { name: '', email: '', password: ''}, resolver: signupresolver, }) const onsubmit = async (formdata: signupschema) => { starttransition(() => { seterror('') setsuccess('') signup(formdata) .then((data) => { if (data.success) { setsuccess(data.success) router.push('/login') } else if (data.error) { seterror(data.error) } }) .catch((data) => { seterror(data.error) }) }) } return ( <form {...form}> <form onsubmit={form.handlesubmit(onsubmit)}> <div classname="space-y-3"> <formfield control={form.control} name="name" render={({ field }) => ( <formitem> <formlabel classname="text-white">username</formlabel> <formcontrol> <input placeholder="enter your name" {...field} disabled={ispending} classname="bg-[#40444b] text-white border-gray-600 focus:border-2 focus:border-[#2d7d46]" /> </formcontrol> <formmessage /> </formitem> )} /> <formfield control={form.control} name="email" render={({ field }) => ( <formitem> <formlabel classname="text-white">email address</formlabel> <formcontrol> <input placeholder="enter your email address" {...field} disabled={ispending} classname="bg-[#40444b] text-white border-gray-600 focus:border-2 focus:border-[#2d7d46]" /> </formcontrol> <formmessage /> </formitem> )} /> <formfield control={form.control} name="password" render={({ field }) => ( <formitem> <formlabel classname="text-white">password</formlabel> <formcontrol> <input type="password" placeholder="enter your password" {...field} disabled={ispending} classname="bg-[#40444b] text-white border-gray-600 focus:border-2 focus:border-[#2d7d46]" /> </formcontrol> <formmessage /> </formitem> )} /> <formerror message={error} /> <formsuccess message={success} /> </div> <button type="submit" disabled={ispending} classname="mt-8 w-full bg-[#3ba55c] hover:bg-[#2d7d46] text-white" > {ispending ? ( <> <loader2 classname="mr-2 h-4 w-4 animate-spin" /> loading... </> ) : ( 'sign up' )} </button> </form> </form> ) } // src/components/auth/formsuccess.tsx import { checkcircledicon } from "@radix-ui/react-icons"; interface formsuccessprops { message?: string; } export const formsuccess = ({ message }: formsuccessprops) => { if (!message) return null; return ( <div classname="bg-emerald-500/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-emerald-500"> <checkcircledicon classname="h-4 w-4" /> <p>{message}</p> </div> ); }; // src/components/auth/formerror.tsx import { exclamationtriangleicon } from "@radix-ui/react-icons"; interface formerrorprops { message?: string; } export const formerror = ({ message }: formerrorprops) => { if (!message) return null; return ( <div classname="bg-destructive/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-destructive"> <exclamationtriangleicon classname="h-4 w-4" /> <p>{message}</p> </div> ); }; // src/components/auth/separator.tsx export const separator = () => { return ( <div classname="my-4 relative"> <div classname="absolute inset-0 flex items-center"> <div classname="w-full border-t border-gray-600" /> </div> <div classname="relative flex justify-center text-sm"> <span classname="px-2 bg-[#2f3136] text-gray-400">or continue with</span> </div> </div> ) }
//// actions // src/app/actions/auth/login.ts 'use server' import { loginschema, loginschema } from '@/schema/login' import { signin } from '@/auth' export const login = async (formdata: loginschema) => { const email = formdata['email'] as string const password = formdata['password'] as string const validatedfields = loginschema.safeparse({ email: formdata.email as string, password: formdata.password as string, }) if (!validatedfields.success) { return { errors: validatedfields.error.flatten().fielderrors, message: 'login failed. please check your input.' } } try { const result = await signin('credentials', { redirect: false, callbackurl: '/setup', email, password }) if (result?.error) { return { error : 'invalid email or password'} } else { return { success : 'login successfully'} } } catch { return { error : 'login failed'} } } // src/app/actions/auth/signup.ts 'use server' import bcrypt from 'bcryptjs' import { signupschema, signupschema } from "@/schema/signup" import { prisma } from '@/lib/prisma'; export const signup = async (formdata: signupschema) => { const validatedfields = signupschema.safeparse({ name: formdata.name as string, email: formdata.email as string, password: formdata.password as string, }) if (!validatedfields.success) { return { errors: validatedfields.error.flatten().fielderrors, message: 'sign up failed. please check your input.' } } try { const hashedpassword = await bcrypt.hash(validatedfields.data.password, 10); const existinguser = await prisma.user.findunique({ where: { email: validatedfields.data.email } }) if (existinguser) { return { error: 'user already exists!' } } await prisma.user.create({ data: { name: validatedfields.data.name, email: validatedfields.data.email, hashedpassword: hashedpassword, }, }); return { success: 'user created successfully!' } } catch (error) { return { error : `sign up failed`} } }
//// validations // src/schema/login.ts import * as z from 'zod'; import { zodresolver } from '@hookform/resolvers/zod'; export const loginschema = z.object({ email: z.string().email('this is not valid email address'), password: z .string() .min(8, { message: 'password must contain at least 8 characters' }), }); export type loginschema = z.infer<typeof loginschema>; export const loginresolver = zodresolver(loginschema); // src/schema/signup.ts import * as z from 'zod'; import { zodresolver } from '@hookform/resolvers/zod'; export const signupschema = z.object({ name: z.string().min(1, { message: 'name is required' }), email: z.string().email('this is not valid email address'), password: z .string() .min(8, { message: 'password must contain at least 8 characters' }), }); export type signupschema = z.infer<typeof signupschema>; export const signupresolver = zodresolver(signupschema);
// src/middleware.ts import { nextresponse } from 'next/server' import { auth } from "@/auth" export default auth((req) => { const { nexturl, auth: session } = req const isloggedin = !!session const isloginpage = nexturl.pathname === "/login" const issignuppage = nexturl.pathname === "/signup" const issetuppage = nexturl.pathname === "/setup" // if trying to access /setup while not logged in if (!isloggedin && issetuppage) { const loginurl = new url("/login", nexturl.origin) return nextresponse.redirect(loginurl) } // if trying to access /login or /signup while already logged in if (isloggedin && (isloginpage || issignuppage)) { const dashboardurl = new url("/", nexturl.origin) return nextresponse.redirect(dashboardurl) } // for all other cases, allow the request to pass through return nextresponse.next() }) export const config = { matcher: ["/login","/signup", "/setup", "/"], };
文件夹结构
/src /app /actions /login.ts // Login Action /signup.ts // Signup Action /api /auth [...nextauth] /route.ts /login page.tsx // Login Page /signup page.tsx // Sign Up Page layout.tsx page.tsx /components /auth AuthLayout.tsx GoogleAuthButton.tsx LoginForm.tsx SignupForm.tsx FormSuccess.tsx FormError.tsx Separator.tsx /schema login.ts signup.ts auth.ts // in src folder middleware.ts // in src folder
以上就是在 Nextjs App Router 中使用 Authjs 进行用户身份验证的详细内容,更多请关注其它相关文章!