总结
需要先在 github 生成需要配置 nextAuth 的 .env 里的三个配置,然后在 auth.ts 中配置 provider 支持 github,再在 app/api/auth/[...nextauth]/route.ts 中配置拦截路由,还可以通过 Adapters 和数据库同步
NextAuth

Nextjs 中使用 github 登陆参考 官网 OAuth 验证(感觉不咋好,不如看 demo)
app router 参考 next-auth-example
prisma 和 mysql 数据库
- 前提:
prisma连接数据库 - 创建一个
user表,包括id、name、email、password - 创建一个
post表,包括id、title、content、authorId
初始化 prisma
//安装
pnpm i prisma @prisma/client
// 初始化
npx prisma init
// 设置.env
DATABASE_URL = 'mysql://root:admin@127.0.0.1:3306/blog_system'//安装
pnpm i prisma @prisma/client
// 初始化
npx prisma init
// 设置.env
DATABASE_URL = 'mysql://root:admin@127.0.0.1:3306/blog_system'prisma 设置表
model User {
id Int @id @default(autoincrement())
username String
email String @unique
password String
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId Int
user User @relation(fields: [userId], references: [id])
}model User {
id Int @id @default(autoincrement())
username String
email String @unique
password String
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId Int
user User @relation(fields: [userId], references: [id])
}同步数据库
npx prisma db push
//或者
npx prisma migrate dev --name initnpx prisma db push
//或者
npx prisma migrate dev --name init四个页面
首页 /
有一个按钮,点击后跳转到 login,要登陆了你才能进到 dashboard

登陆页 /login
用 shadcn 的登陆 Block,但只需要用到 github 登陆

注册页 /signUp

落地页 /dashboard
如果用户登陆了,就跳转到 dashboard,并且展示获取到的用户信息

Github 授权
这里获取的数据后面配置到'.env',为了安全,记得.gitignore忽略提交.env


需要注意这个 callback url 为http://localhost:3000/api/auth/callback/github

最终拿到 AUTH_GITHUB_ID 和 AUTH_GITHUB_SECRET,最后一个AUTH_SECRET 需要随机生成

生成随机字符串
这里是给.env 的 AUTH_SECRET 字段用的
openssl rand -base64 32openssl rand -base64 32配置.env
从上面 github 配置的参数拿到,大约是这样
AUTH_GITHUB_ID = 645659b798f96xxxxxxx
AUTH_GITHUB_SECRET = bae9254b7c41532551b4aexxxxxxxxxxxxxxxxxx
AUTH_SECRET = 1CrfSEZ5KFSglM3IopvODGxxxxxxxxxxxxxxxxxxx=AUTH_GITHUB_ID = 645659b798f96xxxxxxx
AUTH_GITHUB_SECRET = bae9254b7c41532551b4aexxxxxxxxxxxxxxxxxx
AUTH_SECRET = 1CrfSEZ5KFSglM3IopvODGxxxxxxxxxxxxxxxxxxx=配置 auth.js
先 tsconfig.json 配置个别名
"paths": {
"@/*": ["./src/*"],
"auth": ["./auth"]
}"paths": {
"@/*": ["./src/*"],
"auth": ["./auth"]
}再配置auth.js
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [GitHub]
})import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [GitHub]
})配置 app/api/auth/[...nextauth]/route.ts
import { handlers } from 'auth'
export const { GET, POST } = handlersimport { handlers } from 'auth'
export const { GET, POST } = handlers这个 auth 具体看怎么配置路径别名,比如
"paths": {
"@/*": ["./src/*"],
"auth": ["./auth"]
}"paths": {
"@/*": ["./src/*"],
"auth": ["./auth"]
}实现 Github 登陆

首先到根页面,因为访问需要权限才能去 dashboard,所以有个入口先去 /login 的入口,/login 可以 Github 登陆也可以sign up去注册账号,Github 登陆的话就跳回 dashboard,sign up 注册了账号的话还需要跳到 login 登陆

在这里加代码
import { signIn } from 'auth'
function LoginWithGitHub() {
return (
<form
action={async () => {
'use server'
await signIn()
}}
>
<Button variant="outline" className="w-full">
Login with Github
</Button>
</form>
)
}import { signIn } from 'auth'
function LoginWithGitHub() {
return (
<form
action={async () => {
'use server'
await signIn()
}}
>
<Button variant="outline" className="w-full">
Login with Github
</Button>
</form>
)
}然后就能跳到这里

点击就是 Github 登陆了,登陆后会看到多了 3 个 cookie

获取登陆信息
主要是await auth()
import { Button } from '@/components/ui/button'
import { auth, signOut } from 'auth'
interface User {
name: string
email: string
image: string
}
function LoginOut() {
return (
<form
action={async () => {
'use server'
await signOut()
}}
>
<Button>退出登陆</Button>
</form>
)
}
export default async function Home() {
const session = (await auth()) as { user: User }
const { user } = session
return (
<main className="">
<div className="absolute left-[50%] mt-[30vh] translate-x-[-50%] translate-y-[-50%]">
<div>你已经登陆,你现在在dashboard</div>
<br />
<div>
<div>用户名:{user.name}</div>
<br />
<LoginOut></LoginOut>
</div>
</div>
</main>
)
}import { Button } from '@/components/ui/button'
import { auth, signOut } from 'auth'
interface User {
name: string
email: string
image: string
}
function LoginOut() {
return (
<form
action={async () => {
'use server'
await signOut()
}}
>
<Button>退出登陆</Button>
</form>
)
}
export default async function Home() {
const session = (await auth()) as { user: User }
const { user } = session
return (
<main className="">
<div className="absolute left-[50%] mt-[30vh] translate-x-[-50%] translate-y-[-50%]">
<div>你已经登陆,你现在在dashboard</div>
<br />
<div>
<div>用户名:{user.name}</div>
<br />
<LoginOut></LoginOut>
</div>
</div>
</main>
)
}中间件判断是否登陆
具体判断保护路由,看下一个点 Adapters的 auth.ts 配置代码
import NextAuth from 'next-auth'
import { authConfig } from './auth.config'
export default NextAuth(authConfig).auth
export const config = {
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)']
}import NextAuth from 'next-auth'
import { authConfig } from './auth.config'
export default NextAuth(authConfig).auth
export const config = {
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)']
}Database Adapters
用户登录信息持久化到数据库,参考https://authjs.dev/getting-started/adapters和https://authjs.dev/reference/adapter/prisma
需要安装@auth/prisma-adapter,然后配置prisma和auth.ts
配置 schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
posts Post[]
}
model Post {
id String @id @default(cuid())
title String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
user User @relation(fields: [userId], references: [id])
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
posts Post[]
}
model Post {
id String @id @default(cuid())
title String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
user User @relation(fields: [userId], references: [id])
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}配置auth.ts
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
import { PrismaAdapter } from '@auth/prisma-adapter'
import type { NextAuthConfig } from 'next-auth'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const config = {
adapter: PrismaAdapter(prisma),
theme: {
logo: 'https://next-auth.js.org/img/logo/logo-sm.png'
},
providers: [GitHub],
callbacks: {
authorized({ request: { nextUrl }, auth }) {
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
}
}
} satisfies NextAuthConfig
export const { handlers, auth, signIn, signOut } = NextAuth(config)import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
import { PrismaAdapter } from '@auth/prisma-adapter'
import type { NextAuthConfig } from 'next-auth'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const config = {
adapter: PrismaAdapter(prisma),
theme: {
logo: 'https://next-auth.js.org/img/logo/logo-sm.png'
},
providers: [GitHub],
callbacks: {
authorized({ request: { nextUrl }, auth }) {
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
}
}
} satisfies NextAuthConfig
export const { handlers, auth, signIn, signOut } = NextAuth(config)邮箱登陆
参考https://authjs.dev/getting-started/providers/email-tutorial
安装 nodemailer
pnpm i nodemailer
配置邮箱
我用的网易邮箱大师,先回到旧版,然后开启服务



然后就拿到了密钥
配置.env
PASSWORD 不是你的邮箱登录密码,而是刚才开启 SMTP 时的密钥 每个邮箱的 SMTP 端口都不一样,自己 Google 查一下,163 邮箱的端口是 25
EMAIL_SERVER=smtp://userame:PASSWORD@smtp.163.com:25
EMAIL_FROM=userame@163.comEMAIL_SERVER=smtp://userame:PASSWORD@smtp.163.com:25
EMAIL_FROM=userame@163.com配置 auth.ts
在 providers 里面加配置
import Email from 'next-auth/providers/nodemailer'
export const config = {
providers: [
GitHub,
Email({
server: process.env. EMAIL_SERVER,
from: process.env. EMAIL_FROM
})
]
} satisfies NextAuthConfig
export const { handlers, auth, signIn, signOut } = NextAuth(config)import Email from 'next-auth/providers/nodemailer'
export const config = {
providers: [
GitHub,
Email({
server: process.env. EMAIL_SERVER,
from: process.env. EMAIL_FROM
})
]
} satisfies NextAuthConfig
export const { handlers, auth, signIn, signOut } = NextAuth(config)此时就多了一个邮箱选项,但是要项目上线后才能使用邮箱登陆

Zod 表单校验
参考 shadcn 的 demo,react-hook-form 和 Zod 一起使用
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
const formSchema = z.object({
username: z.string().min(2, {
message: 'Username must be at least 2 characters.'
})
})
export function ProfileForm() {
// 1. Define your form.
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: ''
}
})
// 2. Define a submit handler.
function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values.
// ✅ This will be type-safe and validated.
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
const formSchema = z.object({
username: z.string().min(2, {
message: 'Username must be at least 2 characters.'
})
})
export function ProfileForm() {
// 1. Define your form.
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: ''
}
})
// 2. Define a submit handler.
function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values.
// ✅ This will be type-safe and validated.
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}踩坑
使用 shadcn 的时候,它的 input 组件,默认是没有 name 属性的,无法被 form 和 server action 收集

server action 里报错如下,重启 vscode 就行了

使用 adapter 后会有报错,原因是不能直接在 middleware 里使用 prisma/client,也就是不能直接export { auth as middleware } from 'auth',需要重新在中间价配置 nextAuth
