目录
初始设置
- 安装
-
配置
- 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 (
hello world!
@@##@@
)
}
在客户端组件中获取会话
// 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 (
hello world!
@@##@@
)
}
// 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 (
{children}
);
}
文件夹结构
/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 => {
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”表示 json web token 将用于会话管理而不是数据库会话。
回调:
- 这些是在身份验证流程中的各个点调用的函数,允许您自定义流程。
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 (
do not have an account?{' '}
sign up
)
}
// 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 (
already have an account?{' '}
sign in
)
}
//// components
// src/components/auth/authlayout.tsx
import react from 'react'
interface authlayoutprops {
children: react.reactnode
title: string
}
export const authlayout: react.fc = ({ children, title }) => {
return (
{title}
{children}
)
}
// src/components/auth/googleauthbutton.tsx
import { signin } from "@/auth"
import { button } from "@/components/ui/button"
interface googleauthbuttonprops {
text: string
}
export const googleauthbutton: react.fc = ({ text }) => {
return (
)
}
// 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('')
const [success, setsuccess] = usestate('')
const [ispending, starttransition] = usetransition()
const router = userouter();
const form = useform({
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 (
)
}
// 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('')
const [success, setsuccess] = usestate('')
const [ispending, starttransition] = usetransition()
const router = userouter();
const form = useform({
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 (
)
}
// 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 (
{message}
);
};
// 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 (
{message}
);
};
// src/components/auth/separator.tsx
export const separator = () => {
return (
or continue with
)
}
//// 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;
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;
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











