全栈开发个人博客09.Authentication

Auth0 是一个身份认证和授权平台,免费账号最多支持25000个用户登录。支持多个社交平台一键登录,比如 Google, 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)
相关推荐
我这里是好的呀6 小时前
全栈开发个人博客12.嵌套评论设计
前端·全栈
我这里是好的呀6 小时前
全栈开发个人博客13.AI聊天设计
前端·全栈
我这里是好的呀6 小时前
全栈开发个人博客11.文章点赞设计
全栈
susnm2 天前
创建你的第一个 Dioxus app
rust·全栈
coco01244 天前
云上教室选座系统开发实战:基于 Vue3、Express 与 MongoDB 的全栈实践
vue.js·全栈
我这里是好的呀4 天前
全栈开发个人博客08.前端接入Graphql
全栈
我这里是好的呀5 天前
全栈开发个人博客02:国际化
全栈
零道5 天前
我用了一周时间,复刻了一个Bolt new
ai编程·全栈·deepseek
Cyber4K5 天前
《48小时极速开发:Python+MySQL 学生信息管理系统架构实战揭秘》
python·全栈