세션과 인증

인증은 웹 앱에서 매우 흔한 요구 사항입니다. 이 레시피에서는 Nuxt 앱에서 기본적인 사용자 등록과 인증을 구현하는 방법을 보여줍니다.

소개

이 레시피에서는 클라이언트와 서버 양쪽 세션 데이터를 관리하기 위한 편리한 유틸리티를 제공하는 Nuxt Auth Utils를 사용하여 풀스택 Nuxt 앱에 인증을 설정해 보겠습니다.

이 모듈은 보안이 적용되고 봉인된(sealed) 쿠키를 사용해 세션 데이터를 저장하므로, 세션 데이터를 저장하기 위한 데이터베이스를 따로 설정할 필요가 없습니다.

nuxt-auth-utils 설치

nuxt CLI를 사용하여 nuxt-auth-utils 모듈을 설치합니다.

Terminal
npx nuxt module add auth-utils
이 명령은 nuxt-auth-utils를 의존성으로 설치하고, nuxt.config.tsmodules 섹션에 추가합니다.

nuxt-auth-utils는 세션 데이터를 저장하기 위해 봉인된 쿠키를 사용하므로, 세션 쿠키는 NUXT_SESSION_PASSWORD 환경 변수의 비밀 키를 사용해 암호화됩니다.

설정되어 있지 않다면, 개발 모드에서 실행할 때 이 환경 변수는 자동으로 .env에 추가됩니다.
.env
NUXT_SESSION_PASSWORD=a-random-password-with-at-least-32-characters
배포 전에 프로덕션 환경에도 이 환경 변수를 추가해야 합니다.

로그인 API 라우트

이 레시피에서는 정적인 데이터를 기반으로 사용자를 로그인시키는 간단한 API 라우트를 만들겠습니다.

요청 본문에 이메일과 비밀번호를 담은 POST 요청을 받는 /api/login API 라우트를 만들어 봅시다.

server/api/login.post.ts
import { z } from 'zod'

const bodySchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

export default defineEventHandler(async (event) => {
  const { email, password } = await readValidatedBody(event, bodySchema.parse)

  if (email === 'admin@admin.com' && password === 'iamtheadmin') {
    // 쿠키에 사용자 세션을 설정합니다
    // 이 서버 유틸은 auth-utils 모듈에 의해 자동으로 임포트됩니다
    await setUserSession(event, {
      user: {
        name: 'John Doe',
      },
    })
    return {}
  }
  throw createError({
    statusCode: 401,
    message: 'Bad credentials',
  })
})
프로젝트에 zod 의존성을 설치했는지 확인하세요 (npm i zod).
nuxt-auth-utils에서 제공하는 setUserSession 서버 헬퍼에 대해 더 읽어보세요.

로그인 페이지

이 모듈은 애플리케이션에서 사용자가 인증되었는지 알 수 있는 Vue 컴포저블을 제공합니다:

<script setup>
const { loggedIn, session, user, clear, fetch } = useUserSession()
</script>

/api/login 라우트로 로그인 데이터를 제출하는 폼이 있는 로그인 페이지를 만들어 봅시다.

app/pages/login.vue
<script setup lang="ts">
const { loggedIn, user, fetch: refreshSession } = useUserSession()
const credentials = reactive({
  email: '',
  password: '',
})
async function login () {
  try {
    await $fetch('/api/login', {
      method: 'POST',
      body: credentials,
    })

    // 클라이언트 측에서 세션을 새로고침하고 홈 페이지로 리디렉션합니다
    await refreshSession()
    await navigateTo('/')
  } catch {
    alert('Bad credentials')
  }
}
</script>

<template>
  <form @submit.prevent="login">
    <input
      v-model="credentials.email"
      type="email"
      placeholder="Email"
    >
    <input
      v-model="credentials.password"
      type="password"
      placeholder="Password"
    >
    <button type="submit">
      Login
    </button>
  </form>
</template>

API 라우트 보호하기

서버 라우트를 보호하는 것은 데이터를 안전하게 지키는 데 핵심입니다. 클라이언트 측 미들웨어는 사용자에게는 도움이 되지만, 서버 측 보호가 없다면 데이터는 여전히 접근될 수 있습니다. 민감한 데이터가 있는 모든 라우트는 반드시 보호해야 하며, 해당 라우트에서 사용자가 로그인되어 있지 않다면 401 에러를 반환해야 합니다.

auth-utils 모듈은 사용자가 로그인되어 있고 활성 세션을 가지고 있는지 확인하는 데 도움이 되는 requireUserSession 유틸리티 함수를 제공합니다.

인증된 사용자만 접근할 수 있는 /api/user/stats 라우트 예제를 만들어 봅시다.

server/api/user/stats.get.ts
export default defineEventHandler(async (event) => {
  // 사용자가 로그인되어 있는지 확인합니다
  // 유효한 사용자 세션에서 오지 않은 요청이라면 401 에러를 발생시킵니다
  const { user } = await requireUserSession(event)

  // TODO: 사용자 기반으로 통계를 가져옵니다

  return {}
})

앱 라우트 보호하기

서버 측 라우트로 데이터는 안전해졌지만, 다른 작업을 하지 않으면 인증되지 않은 사용자가 /users 페이지에 접근하려 할 때 이상한 데이터를 보게 될 수 있습니다. 이 라우트를 클라이언트 측에서 보호하고 사용자를 로그인 페이지로 리디렉션하기 위해 클라이언트 측 미들웨어를 만들어야 합니다.

nuxt-auth-utils는 사용자가 로그인되어 있는지 확인하고, 로그인되어 있지 않다면 리디렉션하는 데 사용할 수 있는 편리한 useUserSession 컴포저블을 제공합니다.

/middleware 디렉터리에 미들웨어를 생성하겠습니다. 서버와 달리 클라이언트 측 미들웨어는 모든 엔드포인트에 자동으로 적용되지 않으므로, 어디에 적용할지 명시해야 합니다.

app/middleware/authenticated.ts
export default defineNuxtRouteMiddleware(() => {
  const { loggedIn } = useUserSession()

  // 사용자가 인증되지 않았다면 로그인 화면으로 리디렉션합니다
  if (!loggedIn.value) {
    return navigateTo('/login')
  }
})

홈 페이지

이제 라우트를 보호하는 앱 미들웨어가 생겼으니, 인증된 사용자 정보를 표시하는 홈 페이지에 이를 사용할 수 있습니다. 사용자가 인증되지 않았다면 로그인 페이지로 리디렉션됩니다.

보호하려는 라우트에 미들웨어를 적용하기 위해 definePageMeta를 사용하겠습니다.

app/pages/index.vue
<script setup lang="ts">
definePageMeta({
  middleware: ['authenticated'],
})

const { user, clear: clearSession } = useUserSession()

async function logout () {
  await clearSession()
    await navigateTo('/login')
}
</script>

<template>
  <div>
    <h1>Welcome {{ user.name }}</h1>
    <button @click="logout">
      Logout
    </button>
  </div>
</template>

또한 세션을 지우고 사용자를 로그인 페이지로 리디렉션하는 로그아웃 버튼도 추가했습니다.

결론

Nuxt 앱에서 매우 기본적인 사용자 인증과 세션 관리를 성공적으로 설정했습니다. 또한 서버와 클라이언트 측에서 민감한 라우트를 보호하여 인증된 사용자만 접근할 수 있도록 했습니다.

다음 단계로는 다음과 같은 작업을 할 수 있습니다:

OAuth 인증, 데이터베이스 및 CRUD 작업이 포함된 Nuxt 앱의 전체 예시는 오픈 소스 atidone 저장소를 참고하세요.