Auth0 是一个身份认证和授权平台,免费账号最多支持25000个用户登录。支持多个社交平台一键登录,比如
Github
等。也可以自定义编写登录注册成功后的 hook,与宿主平台做通信。
1. 创建应用
注册完成后,创建一个web应用,选择类型 Regular Web Application
。得到以下凭证信息,粘贴到.env
文件
ini
//.env
AUTH0_DOMAIN='dev-xxxx.auth0.com'
AUTH0_CLIENT_ID='xxxx'
AUTH0_CLIENT_SECRET='xxxx_xxxx'
2. 配置应用
-
Application Login URI:
配置成项目登录页的路由,
https://thisiscz.vercel.app/en/login
,加上returnTo
参数,可以在登录成功后重定向到指定地址
tsx
//login.tsx
import { Locale } from 'next-intl'
import { redirect } from 'next/navigation'
type Props = {
params: Promise<{ locale: Locale }>
searchParams: Promise<{ returnTo?: string }>
}
export default async function LoginPage({ params, searchParams }: Props) {
const { returnTo } = await searchParams
redirect(
returnTo
? `/auth/login?returnTo=${encodeURIComponent(returnTo)}`
: '/auth/login',
)
}
- Allowed Callback URLs:
登录成功后Auth0自动回调以下路由
bash
http://localhost:3000/auth/callback, https://thisiscz.vercel.app/auth/callback
- Allowed Logout URLs:
退出登录,默认回到首页,支持逗号分隔,可以填多个
bash
http://localhost:3000, https://thisiscz.vercel.app
3. 配置Auth0 SDK Client
pnpm add @auth0/nextjs-auth0
ts
// lib/auth0.js
import { Auth0Client } from '@auth0/nextjs-auth0/server'
// Initialize the Auth0 client
export const auth0 = new Auth0Client({
// Options are loaded from environment variables by default
// Ensure necessary environment variables are properly set
domain: process.env.AUTH0_DOMAIN,
clientId: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
appBaseUrl: process.env.APP_BASE_URL,
secret: process.env.AUTH0_SECRET,
authorizationParameters: {
// In v4, the AUTH0_SCOPE and AUTH0_AUDIENCE environment variables for API authorized applications are no longer automatically picked up by the SDK.
// Instead, we need to provide the values explicitly.
scope: process.env.AUTH0_SCOPE,
audience: process.env.AUTH0_AUDIENCE,
},
})
4. 使用 Auth0 Middleware
/auth
路由开头的交给 auth0 去处理,其他的交个国际化的中间件
ts
import createMiddleware from 'next-intl/middleware'
import { routing } from '@/i18n/routing'
import type { NextRequest } from 'next/server'
import { auth0 } from '@/lib/auth0'
export default async function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/auth')) {
return await auth0.middleware(request)
} else {
const intlMiddleware = createMiddleware(routing)
return await intlMiddleware(request)
}
}
export const config = {
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)',
}
5. Auth0登录成功同步到数据库
auth0 开启 Google 一键登录,可以拿到email
,nickname
等基础信息。此时需要将用户数据种到数据库User表中
auth0 支持在登录成功后编写自定义脚本,将用户数据回传给项目接口,从而创建User数据,并且只有首次登录才会同步数据库。代码如下:
ts
const fetch = require('node-fetch')
exports.onExecutePostLogin = async (event, api) => {
const SECRET = event.secrets.AUTH0_HOOK_SECRET
if (event.user.app_metadata.localUserCreated) {
return
}
const email = event.user.email
const nickname = event.user.nickname || event.user.name
const request = await fetch('https://thisiscz.vercel.app/api/auth/hook', {
method: 'post',
body: JSON.stringify({ email, nickname, secret: SECRET }),
headers: { 'Content-Type': 'application/json' },
})
const response = await request.json()
api.user.setAppMetadata('localUserCreated', true)
}
项目暴露服务接口/api/auth/hook
,加上秘钥校验
ts
import prisma from '@/lib/prisma'
export async function POST(request: Request) {
const { email, nickname, secret } = await request.json()
if (secret !== process.env.AUTH0_HOOK_SECRET) {
return Response.json(
{ message: `You must provide the secret 🤫` },
{ status: 403 },
)
}
if (email) {
await prisma.user.create({
data: { email, nickname },
})
return Response.json(
{ message: `User with email: ${email} has been created successfully!` },
{ status: 200 },
)
}
}
6. 获取用户信息
- 从auth0拿到用户邮箱,再根据邮箱查到本地User表的相关数据,可以封装以下函数,支持在服务端组件中使用
ts
// /lib/getServerUser.ts
import { auth0 } from './auth0'
import prisma from './prisma'
export async function getServerUser() {
const session = (await auth0.getSession()) ?? {}
const email = (session as any)?.user?.email
let user = null
if (email) {
user = await prisma.user.findUnique({
where: {
email,
},
})
}
return user
}
- 客户端组件获取用户数据,可以借助
Zustand
,将服务端拿到的用户数据传递给客户端组件,再设置全局状态useUserStore.setState({ user })
。
Zustand
在服务端设置初始数据,客户端并不能同步到,否则会更简单,具体原因参考:Zustand outside of components
关于 Zustand 具体使用方法不做赘述了
tsx
export default async function LocaleLayout({ children, params }: Props) {
//...
const user = await getServerUser()
return (
<html lang={locale} suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<NextIntlClientProvider>
<ApolloWrapper>
<AppInit user={user} />
</ApolloWrapper>
<Toaster />
</NextIntlClientProvider>
</ThemeProvider>
</body>
</html>
)
}
tsx
//AppInit.tsx
'use client'
import { useUserStore } from '@/store/userStore'
import { useEffect } from 'react'
export default function AppInit({ user }: { user: any }) {
useEffect(() => {
if (user) {
useUserStore.setState({ user })
}
}, [user])
return null
}
7. Next.js路由鉴权
/admin
路由只允许 ADMIN 用户访问。
tsx
// admin/layout.tssx
export default async function AdminLayout({ children, params }: Props) {
const { locale } = await params
// Enable static rendering
setRequestLocale(locale)
const user = await getServerUser()
if (!user || user.role !== 'ADMIN') {
return redirect('/')
}
return <div className="page-wrapper py-6">{children}</div>
}
8. Graphql接口鉴权
当用户权限是 ADMIN ,允许发布博客.
ts
// graphql/context.ts
import { getServerUser } from '@/lib/getServerUser'
import prisma from '@/lib/prisma'
import type { NextApiRequest, NextApiResponse } from 'next'
export async function createContext({
req,
res,
}: {
req: NextApiRequest
res: NextApiResponse
}) {
const user = await getServerUser()
return {
prisma,
user,
}
}
在api/graphql
接口增加context
ts
// api/graphql
import { createYoga } from 'graphql-yoga'
import { schema } from '../../../../graphql/schema'
import { createContext } from '../../../../graphql/context'
import { NextRequest } from 'next/server'
const { handleRequest } = createYoga({
schema,
context: createContext,
})
export async function GET(request: NextRequest) {
return handleRequest(request, {} as any)
}
export async function POST(request: NextRequest) {
return handleRequest(request, {} as any)
}
发布文章接口增加权限校验
ts
// graphql/types/Post.ts
// 定义添加链接的类型
builder.mutationType({
fields: (t: any) => ({
addPost: t.field({
type: 'Post' as any,
args: {
title: t.arg.string(),
summary: t.arg.string(),
content: t.arg.string(),
},
resolve: async (_root: any, args: any, ctx: any) => {
if (ctx.user?.role !== 'ADMIN') {
throw new Error('you are not admin')
}
const { title, summary, content } = args
const post = await ctx.prisma.post.create({
data: { title, summary, content, createdById: ctx.user.id },
})
return post
},
}),
}),
} as any)