Skip to content

NextJS中文文档 - Authentication

理解身份验证对于保护应用程序的数据至关重要。本页将指导你使用 React 和 Next.js 的功能来实现身份验证。

在开始之前,将这个过程分解为三个概念会有所帮助:

  1. 身份验证:验证用户的身份。要求用户使用他们拥有的某些信息(如用户名和密码)来证明其身份。
  2. 会话管理:在请求之间跟踪用户的身份验证状态。
  3. 授权:决定用户可以访问哪些路由和数据。

下图展示了使用 React 和 Next.js 功能的身份验证流程:

本页的示例将介绍基本的用户名和密码身份验证,用于教育目的。虽然你可以实现自定义的身份验证解决方案,但为了提高安全性和简便性,我们建议使用身份验证库。这些库为身份验证、会话管理和授权提供了内置解决方案,并提供额外功能,如社交登录、多因素身份验证和基于角色的访问控制。你可以在 身份验证库 部分找到相关列表。

身份验证

会话管理

一旦用户经过身份验证,你需要在请求之间跟踪他们的身份验证状态。这通常通过创建和管理会话来完成。会话使服务器能够记住特定用户的状态并根据此状态自定义其体验。

有两种主要的方法来管理会话:无状态会话数据库会话

  1. 无状态会话:会话数据(或令牌)存储在浏览器的 cookie 中。每次请求都会发送 cookie,允许在服务器上验证会话。这种方法更简单,但如果实现不正确,可能不太安全。
  2. 数据库会话:会话数据存储在数据库中,用户的浏览器只接收加密的会话 ID。这种方法更安全,但可能更复杂,并使用更多的服务器资源。

提示: 虽然你可以使用任一方法,或者两者兼用,但我们建议使用会话管理库,例如 iron-sessionJose

无状态会话

无状态会话不在服务器上保存任何会话数据。相反,所有会话数据都存储在客户端,通常是在已加密和签名的 cookie 中,以确保数据不被篡改。

创建和管理无状态会话

下面是使用 Cookie API 创建和管理无状态会话的示例:

tsx
import { cookies } from 'next/headers'
import { SignJWT } from 'jose'
import { nanoid } from 'nanoid'
import { getUser } from '@/app/lib/db'

// 环境变量中的密钥,用于签名 JWT
const secretKey = process.env.JWT_SECRET_KEY
const key = new TextEncoder().encode(secretKey)

export async function signIn(
  email: string,
  password: string,
  remember: boolean,
): Promise<{ success: boolean; message?: string }> {
  // 检查用户凭证
  const user = await getUser(email)

  if (!user || !(await bcrypt.compare(password, user.password))) {
    return { success: false, message: '无效的凭证' }
  }

  // 生成会话 token(使用 JWT)
  const token = await new SignJWT({
    id: user.id,
    email: user.email,
    role: user.role,
  })
    .setProtectedHeader({ alg: 'HS256' })
    .setJti(nanoid())
    .setIssuedAt()
    .setExpirationTime(remember ? '30d' : '24h')
    .sign(key)

  // 存储在 cookie 中
  cookies().set({
    name: 'session',
    value: token,
    httpOnly: true,
    path: '/',
    secure: process.env.NODE_ENV === 'production',
    maxAge: remember ? 30 * 24 * 60 * 60 : 24 * 60 * 60,
  })

  return { success: true }
}
jsx
import { cookies } from 'next/headers'
import { SignJWT } from 'jose'
import { nanoid } from 'nanoid'
import { getUser } from '@/app/lib/db'

// 环境变量中的密钥,用于签名 JWT
const secretKey = process.env.JWT_SECRET_KEY
const key = new TextEncoder().encode(secretKey)

export async function signIn(email, password, remember) {
  // 检查用户凭证
  const user = await getUser(email)

  if (!user || !(await bcrypt.compare(password, user.password))) {
    return { success: false, message: '无效的凭证' }
  }

  // 生成会话 token(使用 JWT)
  const token = await new SignJWT({
    id: user.id,
    email: user.email,
    role: user.role,
  })
    .setProtectedHeader({ alg: 'HS256' })
    .setJti(nanoid())
    .setIssuedAt()
    .setExpirationTime(remember ? '30d' : '24h')
    .sign(key)

  // 存储在 cookie 中
  cookies().set({
    name: 'session',
    value: token,
    httpOnly: true,
    path: '/',
    secure: process.env.NODE_ENV === 'production',
    maxAge: remember ? 30 * 24 * 60 * 60 : 24 * 60 * 60,
  })

  return { success: true }
}

验证会话

要验证会话,可以解密会话 cookie 并检查它是否有效:

tsx
import { cookies } from 'next/headers'
import { jwtVerify } from 'jose'

// 环境变量中的密钥,用于签名 JWT
const secretKey = process.env.JWT_SECRET_KEY
const key = new TextEncoder().encode(secretKey)

export async function getSession() {
  const cookieStore = cookies()
  const token = cookieStore.get('session')?.value

  if (!token) return null

  try {
    const { payload } = await jwtVerify(token, key, {
      algorithms: ['HS256'],
    })

    return payload
  } catch (error) {
    return null
  }
}
jsx
import { cookies } from 'next/headers'
import { jwtVerify } from 'jose'

// 环境变量中的密钥,用于签名 JWT
const secretKey = process.env.JWT_SECRET_KEY
const key = new TextEncoder().encode(secretKey)

export async function getSession() {
  const cookieStore = cookies()
  const token = cookieStore.get('session')?.value

  if (!token) return null

  try {
    const { payload } = await jwtVerify(token, key, {
      algorithms: ['HS256'],
    })

    return payload
  } catch (error) {
    return null
  }
}

注销用户

要注销用户,只需删除会话 cookie:

tsx
import { cookies } from 'next/headers'

export async function signOut() {
  // 删除会话 cookie
  cookies().delete('session')
}
jsx
import { cookies } from 'next/headers'

export async function signOut() {
  // 删除会话 cookie
  cookies().delete('session')
}

无状态会话的主要优点是它们不需要数据库查询,这使得它们更快速且更易于扩展。然而,它们的缺点是无法轻易地验证或撤销,因为令牌在创建后是完全有效的,直到它们过期。

数据库会话

在这种方法中,会话数据存储在服务器端的数据库中,而用户的浏览器只存储一个包含会话 ID 的 cookie。这个 ID 用于在数据库中查找关联的会话数据。

创建和管理数据库会话

下面是如何创建和管理数据库会话的示例:

tsx
import { cookies } from 'next/headers'
import { nanoid } from 'nanoid'
import { db } from '@/app/lib/db'

export async function signIn(
  email: string,
  password: string,
): Promise<{ success: boolean; message?: string }> {
  // 1. 检查用户凭证
  const user = await db.getUser(email)

  if (!user || !(await bcrypt.compare(password, user.password))) {
    return { success: false, message: '无效的凭证' }
  }

  // 2. 生成唯一的会话 ID
  const sessionToken = nanoid(32)
  const sessionExpiry = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7) // 7 天

  // 3. 在数据库中存储会话
  await db.sessions.create({
    data: {
      sessionToken,
      userId: user.id,
      expires: sessionExpiry,
    },
  })

  // 4. 将会话令牌存储在 cookie 中
  cookies().set({
    name: 'session',
    value: sessionToken,
    httpOnly: true,
    path: '/',
    secure: process.env.NODE_ENV === 'production',
    expires: sessionExpiry,
  })

  return { success: true }
}
jsx
import { cookies } from 'next/headers'
import { nanoid } from 'nanoid'
import { db } from '@/app/lib/db'

export async function signIn(email, password) {
  // 1. 检查用户凭证
  const user = await db.getUser(email)

  if (!user || !(await bcrypt.compare(password, user.password))) {
    return { success: false, message: '无效的凭证' }
  }

  // 2. 生成唯一的会话 ID
  const sessionToken = nanoid(32)
  const sessionExpiry = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7) // 7 天

  // 3. 在数据库中存储会话
  await db.sessions.create({
    data: {
      sessionToken,
      userId: user.id,
      expires: sessionExpiry,
    },
  })

  // 4. 将会话令牌存储在 cookie 中
  cookies().set({
    name: 'session',
    value: sessionToken,
    httpOnly: true,
    path: '/',
    secure: process.env.NODE_ENV === 'production',
    expires: sessionExpiry,
  })

  return { success: true }
}

验证会话

要验证会话,从 cookie 中获取会话 ID,然后在数据库中查找相应的会话:

tsx
import { cookies } from 'next/headers'
import { db } from '@/app/lib/db'

export async function getSession() {
  const cookieStore = cookies()
  const sessionToken = cookieStore.get('session')?.value

  if (!sessionToken) return null

  // 在数据库中查找会话
  const session = await db.sessions.findUnique({
    where: { sessionToken },
    include: { user: true },
  })

  // 检查会话是否存在且未过期
  if (!session || session.expires < new Date()) {
    return null
  }

  return session.user
}
jsx
import { cookies } from 'next/headers'
import { db } from '@/app/lib/db'

export async function getSession() {
  const cookieStore = cookies()
  const sessionToken = cookieStore.get('session')?.value

  if (!sessionToken) return null

  // 在数据库中查找会话
  const session = await db.sessions.findUnique({
    where: { sessionToken },
    include: { user: true },
  })

  // 检查会话是否存在且未过期
  if (!session || session.expires < new Date()) {
    return null
  }

  return session.user
}

注销用户

要注销用户,从数据库中删除会话并清除 cookie:

tsx
import { cookies } from 'next/headers'
import { db } from '@/app/lib/db'

export async function signOut() {
  const cookieStore = cookies()
  const sessionToken = cookieStore.get('session')?.value

  if (sessionToken) {
    // 从数据库中删除会话
    await db.sessions.delete({
      where: { sessionToken },
    })
  }

  // 删除会话 cookie
  cookies().delete('session')
}
jsx
import { cookies } from 'next/headers'
import { db } from '@/app/lib/db'

export async function signOut() {
  const cookieStore = cookies()
  const sessionToken = cookieStore.get('session')?.value

  if (sessionToken) {
    // 从数据库中删除会话
    await db.sessions.delete({
      where: { sessionToken },
    })
  }

  // 删除会话 cookie
  cookies().delete('session')
}

数据库会话的主要优点是可以随时验证和撤销会话,并可以为单个用户跟踪多个会话。缺点是需要数据库查询来验证每个请求,并且需要定期清理过期的会话。

授权

授权是关于决定一个经过身份验证的用户可以访问哪些资源或执行哪些操作。它通常发生在身份验证之后。在 Next.js 中,你可以在不同级别实现授权:

基于 UI 的授权

你可以基于用户角色等信息在 UI 层面显示或隐藏某些元素:

tsx
import { getSession } from '@/app/lib/auth'

export default async function DashboardPage() {
  const user = await getSession()

  return (
    <div>
      <h1>仪表盘</h1>

      {/* 基本用户权限 */}
      <div>
        <h2>报告</h2>
        <p>所有用户都可见的内容</p>
      </div>

      {/* 管理员权限 */}
      {user?.role === 'ADMIN' && (
        <div>
          <h2>管理设置</h2>
          <p>只有管理员可以看到的内容</p>
        </div>
      )}
    </div>
  )
}
jsx
import { getSession } from '@/app/lib/auth'

export default async function DashboardPage() {
  const user = await getSession()

  return (
    <div>
      <h1>仪表盘</h1>

      {/* 基本用户权限 */}
      <div>
        <h2>报告</h2>
        <p>所有用户都可见的内容</p>
      </div>

      {/* 管理员权限 */}
      {user?.role === 'ADMIN' && (
        <div>
          <h2>管理设置</h2>
          <p>只有管理员可以看到的内容</p>
        </div>
      )}
    </div>
  )
}

路由级授权

对于更强大的保护,你可以在路由级别实现授权。你可以使用中间件来限制基于特定条件的某些路由的访问:

tsx
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'

// 这个函数可以被标记为 `async`,如果使用 `await` 的话
export async function middleware(request: NextRequest) {
  // 获取会话 cookie
  const sessionCookie = request.cookies.get('session')?.value

  // 如果会话不存在,重定向到登录页面
  if (!sessionCookie) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  try {
    // 验证 JWT
    const key = new TextEncoder().encode(process.env.JWT_SECRET_KEY)
    const { payload } = await jwtVerify(sessionCookie, key)

    // 检查路由权限
    const isAdminRoute = request.nextUrl.pathname.startsWith('/admin')
    if (isAdminRoute && payload.role !== 'ADMIN') {
      // 非管理员尝试访问管理员路由
      return NextResponse.redirect(new URL('/dashboard', request.url))
    }

    // 如果授权成功,继续
    return NextResponse.next()
  } catch (error) {
    // 无效的会话 token
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

// 在以下路径上执行此中间件
export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*'],
}
jsx
import { NextResponse } from 'next/server'
import { jwtVerify } from 'jose'

// 这个函数可以被标记为 `async`,如果使用 `await` 的话
export async function middleware(request) {
  // 获取会话 cookie
  const sessionCookie = request.cookies.get('session')?.value

  // 如果会话不存在,重定向到登录页面
  if (!sessionCookie) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  try {
    // 验证 JWT
    const key = new TextEncoder().encode(process.env.JWT_SECRET_KEY)
    const { payload } = await jwtVerify(sessionCookie, key)

    // 检查路由权限
    const isAdminRoute = request.nextUrl.pathname.startsWith('/admin')
    if (isAdminRoute && payload.role !== 'ADMIN') {
      // 非管理员尝试访问管理员路由
      return NextResponse.redirect(new URL('/dashboard', request.url))
    }

    // 如果授权成功,继续
    return NextResponse.next()
  } catch (error) {
    // 无效的会话 token
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

// 在以下路径上执行此中间件
export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*'],
}

API 路由授权

保护 API 路由也很重要,以防止未经授权的用户通过直接访问 API 绕过 UI 限制。

以下是如何在路由处理程序中验证授权的示例:

tsx
import { NextResponse } from 'next/server'
import { getSession } from '@/app/lib/auth'

export async function GET() {
  // 验证当前用户
  const user = await getSession()

  // 检查用户是否已认证
  if (!user) {
    return NextResponse.json({ error: '未认证' }, { status: 401 })
  }

  // 检查用户权限
  if (user.role !== 'ADMIN') {
    return NextResponse.json({ error: '未授权' }, { status: 403 })
  }

  // 继续处理只有管理员才能访问的逻辑
  const adminData = {
    sensitiveInformation: '只有管理员才能看到这个',
    // ...其他数据
  }

  return NextResponse.json(adminData)
}
jsx
import { NextResponse } from 'next/server'
import { getSession } from '@/app/lib/auth'

export async function GET() {
  // 验证当前用户
  const user = await getSession()

  // 检查用户是否已认证
  if (!user) {
    return NextResponse.json({ error: '未认证' }, { status: 401 })
  }

  // 检查用户权限
  if (user.role !== 'ADMIN') {
    return NextResponse.json({ error: '未授权' }, { status: 403 })
  }

  // 继续处理只有管理员才能访问的逻辑
  const adminData = {
    sensitiveInformation: '只有管理员才能看到这个',
    // ...其他数据
  }

  return NextResponse.json(adminData)
}

资源

以下是兼容 Next.js 的身份验证和会话管理库列表:

身份验证库

  • Auth.js:Auth.js 是一个灵活、开源的身份验证库,支持多种流行的身份验证服务,如谷歌、Facebook、GitHub 等。
  • Clerk:完整的身份验证和用户管理解决方案,强调安全性和自定义性。
  • Kinde:面向 React、Next.js 和其他框架的现代身份验证平台。
  • NextAuth.js:专门为 Next.js 设计的身份验证库(现在是 Auth.js 项目的一部分)。
  • Lucia:自托管、注重类型的轻量级库。
  • SuperTokens:开源身份验证解决方案,专注于拥有和控制用户数据的能力。
  • Permify:由 OPAL 授权引擎支持的授权框架。
  • Magic:无密码/使用"魔法链接"的身份验证解决方案。
  • Userfront:全方位的身份验证和用户管理平台,包括访问控制。
  • Firebase Authentication:作为 Firebase 平台的一部分,提供身份验证功能。

安全性和最佳实践

理解身份验证和授权的安全面是至关重要的。以下资源提供了更多信息:

进一步阅读

要继续学习有关身份验证和安全性的内容,请查看以下资源:

🎉有任何问题,欢迎联系我

WeChat QR Code
WeChat
QQ QR Code
QQ

赣ICP备2023003243号