Authentication

인증을 이해하는 것은 애플리케이션의 데이터를 보호하는 데 매우 중요합니다. 이 페이지에서는 인증을 구현하기 위해 React 및 Next.js 기능을 사용하는 방법을 안내합니다.

시작하기 전에 프로세스를 세 가지 개념으로 나누면 도움이 됩니다:

  1. Authentication: 사용자가 자신이 주장하는 사람인지 확인합니다. 사용자에게 사용자 이름과 비밀번호와 같은 것을 요구하여 신원을 증명하게 합니다.
  2. Session Management: 요청 간에 사용자의 인증 상태를 추적합니다.
  3. Authorization: 사용자가 접근할 수 있는 라우트와 데이터를 결정합니다.

다음 다이어그램은 React 및 Next.js 기능을 사용한 인증 흐름을 보여줍니다:

Diagram showing the authentication flow with React and Next.js features

이 페이지의 예제는 교육 목적으로 기본적인 사용자 이름과 비밀번호 인증을 설명합니다. 사용자 정의 인증 솔루션을 구현할 수도 있지만, 보안성과 단순성을 높이기 위해 인증 라이브러리를 사용하는 것이 좋습니다. 이러한 라이브러리는 인증, 세션 관리 및 권한 부여에 대한 내장 솔루션을 제공하며 소셜 로그인, 다중 인증 및 역할 기반 액세스 제어와 같은 추가 기능도 제공합니다. Auth Libraries 섹션에서 라이브러리 목록을 찾을 수 있습니다.

Authentication

Here are the steps to implement a sign-up and/or login form:

  1. The user submits their credentials through a form.
  2. The form sends a request that is handled by an API route.
  3. Upon successful verification, the process is completed, indicating the user's successful authentication.
  4. If verification is unsuccessful, an error message is shown.

Consider a login form where users can input their credentials:

회원가입 또는 로그인 양식을 구현하는 단계는 다음과 같습니다:

  1. 사용자가 양식을 통해 자격 증명을 제출합니다.
  2. 양식이 API 경로에서 처리되는 요청을 보냅니다.
  3. 검증이 성공하면 사용자가 성공적으로 인증되었음을 나타냅니다.
  4. 검증이 실패하면 오류 메시지가 표시됩니다.

사용자가 자격 증명을 입력할 수 있는 로그인 양식을 고려해보세요:

pages/login.tsx
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 {
      // Handle errors
    }
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <button type="submit">Login</button>
    </form>
  )
}
pages/login.jsx
import { FormEvent } from 'react'
import { useRouter } from 'next/router'
 
export default function LoginPage() {
  const router = useRouter()
 
  async function handleSubmit(event) {
    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 {
      // Handle errors
    }
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <button type="submit">Login</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: 'Invalid credentials.' })
    } else {
      res.status(500).json({ error: 'Something went wrong.' })
    }
  }
}
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: 'Invalid credentials.' })
    } else {
      res.status(500).json({ error: 'Something went wrong.' })
    }
  }
}

Session Management

세션 관리는 사용자의 인증 상태를 요청 간에 유지하도록 합니다. 이는 세션 또는 토큰을 생성, 저장, 갱신 및 삭제하는 작업을 포함합니다.

세션에는 두 가지 유형이 있습니다:

  1. 무상태(Stateless): 세션 데이터(또는 토큰)가 브라우저의 쿠키에 저장됩니다. 쿠키는 각 요청과 함께 전송되어 서버에서 세션을 검증할 수 있습니다. 이 방법은 더 간단하지만, 제대로 구현되지 않으면 보안이 떨어질 수 있습니다.
  2. 데이터베이스 기반(Database): 세션 데이터가 데이터베이스에 저장되며, 사용자의 브라우저에는 암호화된 세션 ID만 전달됩니다. 이 방법은 더 안전하지만, 구현이 복잡하고 더 많은 서버 자원을 사용할 수 있습니다.

참고: 어느 방법이든 사용할 수 있지만, iron-session (opens in a new tab) 또는 Jose (opens in a new tab)와 같은 세션 관리 라이브러리를 사용하는 것을 권장합니다.

Stateless Sessions

Setting and deleting cookies

API Routes를 사용하여 서버에서 세션을 쿠키로 설정할 수 있습니다:

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: 'Successfully set 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: 'Successfully set cookie!' })
}

Database Sessions

데이터베이스 세션을 생성하고 관리하려면 다음 단계를 따라야 합니다:

  1. 세션과 데이터를 저장할 테이블을 데이터베이스에 생성하거나 인증 라이브러리가 이 작업을 처리하는지 확인합니다.
  2. 세션을 삽입, 업데이트 및 삭제하는 기능을 구현합니다.
  3. 세션 ID를 사용자의 브라우저에 저장하기 전에 암호화하고, 데이터베이스와 쿠키가 동기화되도록 합니다(이 단계는 선택 사항이지만, 미들웨어에서 낙관적 인증 검사를 위해 권장됩니다).

서버에서 세션 생성:

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: 'Internal Server 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: 'Internal Server Error' })
  }
}

Authorization

사용자가 인증되고 세션이 생성된 후, 애플리케이션 내에서 사용자가 접근하고 수행할 수 있는 작업을 제어하기 위해 권한 부여를 구현할 수 있습니다.

권한 부여 검사는 두 가지 주요 유형이 있습니다:

  1. Optimistic: 쿠키에 저장된 세션 데이터를 사용하여 사용자가 경로에 접근하거나 작업을 수행할 권한이 있는지 확인합니다. 이러한 검사는 UI 요소를 표시/숨기거나 권한 또는 역할에 따라 사용자를 리디렉션하는 것과 같은 빠른 작업에 유용합니다.
  2. Secure: 데이터베이스에 저장된 세션 데이터를 사용하여 사용자가 경로에 접근하거나 작업을 수행할 권한이 있는지 확인합니다. 이러한 검사는 더 안전하며 민감한 데이터에 접근하거나 작업을 수행해야 하는 경우에 사용됩니다.

두 경우 모두 다음을 권장합니다:

낙관적 검사와 미들웨어 (선택 사항)

일부 경우에는 미들웨어를 사용하여 권한에 따라 사용자를 리디렉션하는 것이 유용할 수 있습니다:

  • 낙관적 검사를 수행하기 위해. 미들웨어는 모든 경로에서 실행되므로 리디렉션 로직을 중앙 집중화하고 권한이 없는 사용자를 미리 필터링하는 좋은 방법입니다.
  • 사용자 간에 데이터를 공유하는 정적 경로를 보호하기 위해 (예: 유료 콘텐츠).

그러나 미들웨어는 모든 경로, 특히 미리 로드된 경로에서도 실행되므로 쿠키에서 세션을 읽는 것(낙관적 검사)에만 집중하고 데이터베이스 검사를 피하여 성능 문제를 방지해야 합니다.

예를 들어:

middleware.ts
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. 쿠키에서 세션 해독
  const cookie = cookies().get('session')?.value
  const session = await decrypt(cookie)
 
  // 5. 사용자가 인증되지 않은 경우 /login으로 리디렉션
  if (isProtectedRoute && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }
 
  // 6. 사용자가 인증된 경우 /dashboard로 리디렉션
  if (
    isPublicRoute &&
    session?.userId &&
    !req.nextUrl.pathname.startsWith('/dashboard')
  ) {
    return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
  }
 
  return NextResponse.next()
}
 
// 미들웨어가 실행되지 않아야 하는 경로
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. 쿠키에서 세션 해독
  const cookie = cookies().get('session').value
  const session = await decrypt(cookie)
 
  // 5. 사용자가 인증되지 않은 경우 /login으로 리디렉션
  if (isProtectedRoute && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }
 
  // 6. 사용자가 인증된 경우 /dashboard로 리디렉션
  if (
    isPublicRoute &&
    session?.userId &&
    !req.nextUrl.pathname.startsWith('/dashboard')
  ) {
    return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
  }
 
  return NextResponse.next()
}
 
// 미들웨어가 실행되지 않아야 하는 경로
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

미들웨어는 초기 검사에 유용할 수 있지만, 데이터 보호의 유일한 방어선이 되어서는 안 됩니다. 대부분의 보안 검사는 데이터 소스에 최대한 가깝게 수행되어야 합니다. 자세한 내용은 데이터 접근 레이어(DAL)을 참조하세요.

:

  • 미들웨어에서 req.cookies.get('session).value를 사용하여 쿠키를 읽을 수도 있습니다.
  • 미들웨어는 Edge Runtime을 사용하므로 인증 라이브러리와 세션 관리 라이브러리가 호환되는지 확인하세요.
  • 미들웨어에서 실행될 경로를 지정하려면 matcher 속성을 사용할 수 있습니다. 그러나 인증을 위해서는 미들웨어가 모든 경로에서 실행되도록 권장됩니다.

Creating a Data Access Layer (DAL)

Protecting API Routes

Next.js에서 API Routes는 서버 측 로직과 데이터 관리를 처리하는 데 필수적입니다. 이러한 라우트를 보안하는 것은 특정 기능에 대한 접근 권한이 있는 사용자만이 이를 사용할 수 있도록 보장하는 데 중요합니다. 일반적으로 사용자의 인증 상태와 역할 기반 권한을 확인하는 작업이 포함됩니다.

다음은 API Route를 보호하는 예제입니다:

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: 'User is not authenticated',
    })
    return
  }
 
  // 사용자가 'admin' 역할을 가지고 있는지 확인
  if (session.user.role !== 'admin') {
    res.status(401).json({
      error: 'Unauthorized access: User does not have admin privileges.',
    })
    return
  }
 
  // 권한이 있는 사용자에 대해 라우트 계속 진행
  // ... API Route 구현
}
pages/api/route.js
export default async function handler(req, res) {
  const session = await getSession(req)
 
  // 사용자가 인증되었는지 확인
  if (!session) {
    res.status(401).json({
      error: 'User is not authenticated',
    })
    return
  }
 
  // 사용자가 'admin' 역할을 가지고 있는지 확인
  if (session.user.role !== 'admin') {
    res.status(401).json({
      error: 'Unauthorized access: User does not have admin privileges.',
    })
    return
  }
 
  // 권한이 있는 사용자에 대해 라우트 계속 진행
  // ... API Route 구현
}

이 예제는 인증 및 권한 부여에 대한 두 단계의 보안 검사를 포함한 API Route를 보여줍니다. 먼저 활성 세션이 있는지 확인한 다음, 로그인한 사용자가 'admin'인지 확인합니다. 이 접근 방식은 인증되고 권한이 부여된 사용자에게만 접근을 제한하여 요청 처리에 대한 견고한 보안을 유지합니다.

Resources

Next.js에서 인증에 대해 배운 후, 보안 인증 및 세션 관리를 구현하는 데 도움이 되는 Next.js 호환 라이브러리와 리소스를 소개합니다:

Auth Libraries

Session Management Libraries

Further Reading

인증 및 보안에 대해 계속해서 배우기 위해 다음 리소스를 참고하세요: