npm install next-auth@beta
// env.local auth_secret=generatetd_random_value
// src/auth.ts
import nextauth from "next-auth"
export const config = {
providers: [],
}
export const { handlers, signin, signout, auth } = nextauth(config)
它应该放在src文件夹内
providers 在 auth.js 中表示是可用于登录用户的服务。用户可以通过四种方式登录。
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>
<img src={session.user.image} alt="user avatar" />
</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>
<img src={session.user.image} alt="user avatar" />
</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
// 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);
适配器:
秘密:
页面:
会话:
回调:
jwt 回调:
会话回调:
从 gcp console 创建新的 oauth 客户端 id > api 和服务 > 凭据

创建后,保存您的客户端 id 和客户端密钥以供以后使用。
当我们在本地工作时,设置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 进行用户身份验证的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号