Menu

Authentication

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

在开始之前,我们可以将这个过程分解为三个概念:

  1. 身份认证:验证用户的身份是否属实。要求用户提供他们所拥有的信息来证明身份,比如用户名和密码。
  2. 会话管理:在请求之间跟踪用户的认证状态。
  3. 授权:决定用户可以访问哪些路由和数据。

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

展示 React 和 Next.js 功能的身份认证流程图

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

身份认证

以下是实现注册和/或登录表单的步骤:

  1. 用户通过表单提交他们的凭据。
  2. 表单发送一个由 API 路由处理的请求。
  3. 验证成功后,该过程完成,表明用户成功通过身份认证。
  4. 如果验证失败,则显示错误消息。

考虑一个用户可以输入其凭据的登录表单:

pages/login.tsx
TypeScript
import { FormEvent } from 'react'
import { useRouter } from 'next/router'
 
export default function LoginPage() {
  const router = useRouter()
 
  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
 
    const formData = new FormData(event.currentTarget)
    const email = formData.get('email')
    const password = formData.get('password')
 
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    })
 
    if (response.ok) {
      router.push('/profile')
    } else {
      // 处理错误
    }
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <input type="email" name="email" placeholder="邮箱" required />
      <input type="password" name="password" placeholder="密码" required />
      <button type="submit">登录</button>
    </form>
  )
}

上面的表单有两个输入字段用于捕获用户的电子邮件和密码。提交时,它会触发一个函数,该函数向 API 路由 (/api/auth/login) 发送 POST 请求。

然后你可以在 API 路由中调用你的身份认证提供商的 API 来处理身份认证:

pages/api/auth/login.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { signIn } from '@/auth'
 
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const { email, password } = req.body
    await signIn('credentials', { email, password })
 
    res.status(200).json({ success: true })
  } catch (error) {
    if (error.type === 'CredentialsSignin') {
      res.status(401).json({ error: '无效的凭据。' })
    } else {
      res.status(500).json({ error: '发生错误。' })
    }
  }
}
pages/api/auth/login.js
import { signIn } from '@/auth'
 
export default async function handler(req, res) {
  try {
    const { email, password } = req.body
    await signIn('credentials', { email, password })
 
    res.status(200).json({ success: true })
  } catch (error) {
    if (error.type === 'CredentialsSignin') {
      res.status(401).json({ error: '无效的凭据。' })
    } else {
      res.status(500).json({ error: '发生错误。' })
    }
  }
}

会话管理

会话管理确保用户的认证状态在请求之间得到保持。它涉及创建、存储、刷新和删除会话或令牌。

有两种类型的会话:

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

值得注意的是: 虽然你可以使用任一方法,或两者都用,但我们建议使用会话管理库,如 iron-sessionJose

无状态会话

设置和删除 cookies

你可以使用 API 路由 在服务器上将会话设置为 cookie:

pages/api/login.ts
import { serialize } from 'cookie'
import type { NextApiRequest, NextApiResponse } from 'next'
import { encrypt } from '@/app/lib/session'
 
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const sessionData = req.body
  const encryptedSessionData = encrypt(sessionData)
 
  const cookie = serialize('session', encryptedSessionData, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 * 24 * 7, // 一周
    path: '/',
  })
  res.setHeader('Set-Cookie', cookie)
  res.status(200).json({ message: '成功设置 cookie!' })
}
pages/api/login.js
import { serialize } from 'cookie'
import { encrypt } from '@/app/lib/session'
 
export default function handler(req, res) {
  const sessionData = req.body
  const encryptedSessionData = encrypt(sessionData)
 
  const cookie = serialize('session', encryptedSessionData, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 * 24 * 7, // 一周
    path: '/',
  })
  res.setHeader('Set-Cookie', cookie)
  res.status(200).json({ message: '成功设置 cookie!' })
}

数据库会话

要创建和管理数据库会话,你需要遵循以下步骤:

  1. 在数据库中创建一个表来存储会话和数据(或检查你的身份认证库是否处理这个)。
  2. 实现插入、更新和删除会话的功能。
  3. 在存储到用户浏览器之前加密会话 ID,并确保数据库和 cookie 保持同步(这是可选的,但建议用于在 Middleware 中进行乐观认证检查)。

在服务器上创建会话

pages/api/create-session.ts
import db from '../../lib/db'
import type { NextApiRequest, NextApiResponse } from 'next'
 
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const user = req.body
    const sessionId = generateSessionId()
    await db.insertSession({
      sessionId,
      userId: user.id,
      createdAt: new Date(),
    })
 
    res.status(200).json({ sessionId })
  } catch (error) {
    res.status(500).json({ error: '服务器内部错误' })
  }
}
pages/api/create-session.js
import db from '../../lib/db'
 
export default async function handler(req, res) {
  try {
    const user = req.body
    const sessionId = generateSessionId()
    await db.insertSession({
      sessionId,
      userId: user.id,
      createdAt: new Date(),
    })
 
    res.status(200).json({ sessionId })
  } catch (error) {
    res.status(500).json({ error: '服务器内部错误' })
  }
}

授权

一旦用户通过身份认证并创建会话,你可以实现授权来控制用户可以在你的应用程序中访问和执行什么操作。

有两种主要类型的授权检查:

  1. 乐观型:使用存储在 cookie 中的会话数据检查用户是否有权访问路由或执行操作。这些检查对于快速操作很有用,比如根据权限或角色显示/隐藏 UI 元素或重定向用户。
  2. 安全型:使用存储在数据库中的会话数据检查用户是否有权访问路由或执行操作。这些检查更安全,用于需要访问敏感数据或操作的操作。

对于这两种情况,我们建议:

通过 Middleware 进行乐观检查(可选)

在某些情况下,你可能想要使用 Middleware 并根据权限重定向用户:

  • 执行乐观检查。由于 Middleware 在每个路由上运行,它是集中重定向逻辑和预过滤未授权用户的好方法。
  • 保护共享数据的静态路由(例如付费墙后面的内容)。

然而,由于 Middleware 在每个路由上运行,包括 预获取 的路由,重要的是只从 cookie 读取会话(乐观检查),并避免数据库检查以防止性能问题。

例如:

middleware.ts
TypeScript
import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'
 
// 1. 指定受保护和公开的路由
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
 
export default async function middleware(req: NextRequest) {
  // 2. 检查当前路由是受保护的还是公开的
  const path = req.nextUrl.pathname
  const isProtectedRoute = protectedRoutes.includes(path)
  const isPublicRoute = publicRoutes.includes(path)
 
  // 3. 从 cookie 解密会话
  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)
 
  // 4. 如果用户未通过身份认证,则重定向到 /login
  if (isProtectedRoute && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }
 
  // 5. 如果用户已通过身份认证,则重定向到 /dashboard
  if (
    isPublicRoute &&
    session?.userId &&
    !req.nextUrl.pathname.startsWith('/dashboard')
  ) {
    return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
  }
 
  return NextResponse.next()
}
 
// Middleware 不应运行的路由
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
middleware.js
import { NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'
 
// 1. 指定受保护和公开的路由
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
 
export default async function middleware(req) {
  // 2. 检查当前路由是受保护的还是公开的
  const path = req.nextUrl.pathname
  const isProtectedRoute = protectedRoutes.includes(path)
  const isPublicRoute = publicRoutes.includes(path)
 
  // 3. 从 cookie 解密会话
  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)
 
  // 4. 如果用户未通过身份认证,则重定向到 /login
  if (isProtectedRoute && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }
 
  // 5. 如果用户已通过身份认证,则重定向到 /dashboard
  if (
    isPublicRoute &&
    session?.userId &&
    !req.nextUrl.pathname.startsWith('/dashboard')
  ) {
    return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
  }
 
  return NextResponse.next()
}
 
// Middleware 不应运行的路由
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

虽然 Middleware 对于初始检查很有用,但它不应该是保护数据的唯一防线。大部分安全检查应该在尽可能接近数据源的地方执行,参见 数据访问层 了解更多信息。

提示:

  • 在 Middleware 中,你也可以使用 req.cookies.get('session').value 来读取 cookies。
  • Middleware 使用 Edge Runtime,检查你的身份认证库和会话管理库是否兼容。
  • 你可以在 Middleware 中使用 matcher 属性来指定 Middleware 应该在哪些路由上运行。不过,对于身份认证,建议 Middleware 在所有路由上运行。

创建数据访问层(DAL)

保护 API 路由

Next.js 中的 API 路由对于处理服务器端逻辑和数据管理至关重要。确保只有经过授权的用户才能访问特定功能是至关重要的。这通常涉及验证用户的身份认证状态和其基于角色的权限。

以下是保护 API 路由的示例:

pages/api/route.ts
import { NextApiRequest, NextApiResponse } from 'next'
 
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const session = await getSession(req)
 
  // 检查用户是否已通过身份认证
  if (!session) {
    res.status(401).json({
      error: '用户未通过身份认证',
    })
    return
  }
 
  // 检查用户是否具有 'admin' 角色
  if (session.user.role !== 'admin') {
    res.status(401).json({
      error: '未授权访问:用户没有管理员权限。',
    })
    return
  }
 
  // 为授权用户继续执行路由
  // ... API 路由的实现
}
pages/api/route.js
export default async function handler(req, res) {
  const session = await getSession(req)
 
  // 检查用户是否已通过身份认证
  if (!session) {
    res.status(401).json({
      error: '用户未通过身份认证',
    })
    return
  }
 
  // 检查用户是否具有 'admin' 角色
  if (session.user.role !== 'admin') {
    res.status(401).json({
      error: '未授权访问:用户没有管理员权限。',
    })
    return
  }
 
  // 为授权用户继续执行路由
  // ... API 路由的实现
}

这个示例演示了一个具有两层安全检查的 API 路由,用于身份认证和授权。它首先检查活动会话,然后验证登录用户是否是 'admin'。这种方法确保了安全访问,仅限于经过身份认证和授权的用户,为请求处理维护了强健的安全性。

资源

现在你已经了解了 Next.js 中的身份认证,以下是兼容 Next.js 的库和资源,可以帮助你实现安全的身份认证和会话管理:

身份认证库

会话管理库

延伸阅读

要继续了解身份认证和安全性,请查看以下资源: