会话和身份验证

身份验证是 Web 应用中极为常见的需求。本指南将向你展示如何在 Nuxt 应用中实现基本的用户注册和身份验证。

简介

¥Introduction

在本教程中,我们将使用 Nuxt 身份验证实用程序 在全栈 Nuxt 应用中设置身份验证,Nuxt 身份验证实用程序 提供了便捷的实用程序来管理客户端和服务器端会话数据。

¥In this recipe we'll be setting up authentication in a full-stack Nuxt app using Nuxt Auth Utils which provides convenient utilities for managing client-side and server-side session data.

该模块使用安全密封的 Cookie 来存储会话数据,因此你无需设置数据库来存储会话数据。

¥The module uses secured & sealed cookies to store session data, so you don't need to setup a database to store session data.

安装 nuxt-auth-utils

¥Install nuxt-auth-utils

使用 nuxi CLI 安装 nuxt-auth-utils 模块。

¥Install the nuxt-auth-utils module using the nuxi CLI.

Terminal
npx nuxi@latest module add auth-utils
此命令将安装 nuxt-auth-utils 作为依赖,并将其推送到 nuxt.config.tsmodules 部分。¥This command will install nuxt-auth-utils as dependency and push it in the modules section of our nuxt.config.ts

¥Cookie Encryption Key

由于 nuxt-auth-utils 使用密封的 Cookie 来存储会话数据,因此会话 Cookie 使用来自 NUXT_SESSION_PASSWORD 环境变量的密钥进行加密。

¥As nuxt-auth-utils uses sealed cookies to store session data, session cookies are encrypted using a secret key from the NUXT_SESSION_PASSWORD environment variable.

如果未设置,此环境变量将在开发模式下运行时自动添加到你的 .env 中。
.env
NUXT_SESSION_PASSWORD=a-random-password-with-at-least-32-characters
你需要在部署之前将此环境变量添加到你的生产环境中。

登录 API 路由

¥Login API Route

为了使其按预期工作,建议不要将 作为页面组件的根元素。

¥For this recipe, we'll create a simple API route to sign-in a user based on static data.

让我们创建一个 /api/login API 路由,它将接受请求正文中包含邮箱和密码的 POST 请求。

¥Let's create a /api/login API route that will accept a POST request with the email and password in the request body.

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') {
    // set the user session in the cookie
    // this server util is auto-imported by the auth-utils module
    await setUserSession(event, {
      user: {
        name: 'John Doe'
      }
    })
    return {}
  }
  throw createError({
    statusCode: 401,
    message: 'Bad credentials'
  })
})
确保在项目(npm i zod)中安装 zod 依赖。¥Make sure to install the zod dependency in your project (npm i zod).
了解更多关于 nuxt-auth-utils 公开的 setUserSession 服务器助手的信息。¥Read more about the setUserSession server helper exposed by nuxt-auth-utils.

登录页面

¥Login Page

模块公开一个 Vue 可组合函数,用于识别用户是否在我们的应用中进行了身份验证:

¥The module exposes a Vue composable to know if a user is authenticated in our application:

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

让我们创建一个带有表单的登录页面,以便将登录数据提交到我们的 /api/login 路由。

¥Let's create a login page with a form to submit the login data to our /api/login route.

pages/login.vue
<script setup lang="ts">
const { loggedIn, user, fetch: refreshSession } = useUserSession()
const credentials = reactive({
  email: '',
  password: '',
})
async function login() {
  $fetch('/api/login', {
    method: 'POST',
    body: credentials
  })
  .then(async () => {
    // Refresh the session on client-side and redirect to the home page
    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 路由

¥Protect API Routes

保护服务器路由是确保数据安全的关键。客户端中间件对用户很有帮助,但如果没有服务器端保护,你的数据仍然可以被访问。保护任何包含敏感数据的路由至关重要,如果用户未登录这些路由,我们应该返回 401 错误。

¥Protecting server routes is key to making sure your data is safe. Client-side middleware is helpful for the user, but without server-side protection your data can still be accessed. It is critical to protect any routes with sensitive data, we should return a 401 error if the user is not logged in on those.

auth-utils 模块提供 requireUserSession 实用函数,帮助确保用户已登录并拥有活动会话。

¥The auth-utils module provides the requireUserSession utility function to help make sure that users are logged in and have an active session.

让我们创建一个只有经过身份验证的用户才能访问的 /api/user/stats 路由示例。

¥Let's create an example of a /api/user/stats route that only authenticated users can access.

server/api/user/stats.get.ts
export default defineEventHandler(async (event) => {
  // make sure the user is logged in
  // This will throw a 401 error if the request doesn't come from a valid user session
  const { user } = await requireUserSession(event)

  // TODO: Fetch some stats based on the user

  return {}
});

保护应用路由

¥Protect App Routes

通过服务器端路由,我们的数据是安全的,但如果不采取任何其他措施,未经身份验证的用户在尝试访问 /users 页面时可能会收到一些奇怪的数据。我们应该创建一个 客户端中间件 来保护客户端的路由,并将用户重定向到登录页面。

¥Our data is safe with the server-side route in place, but without doing anything else, unauthenticated users would probably get some odd data when trying to access the /users page. We should create a client-side middleware to protect the route on the client side and redirect users to the login page.

nuxt-auth-utils 提供了一个便捷的 useUserSession 可组合函数,我们将使用它来检查用户是否已登录,并在未登录时重定向用户。

¥nuxt-auth-utils provides a convenient useUserSession composable which we'll use to check if the user is logged in, and redirect them if they are not.

我们将在 /middleware 目录中创建一个中间件。与服务器不同,客户端中间件不会自动应用于所有端点,我们需要指定其应用位置。

¥We'll create a middleware in the /middleware directory. Unlike on the server, client-side middleware is not automatically applied to all endpoints, and we'll need to specify where we want it applied.

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

  // redirect the user to the login screen if they're not authenticated
  if (!loggedIn.value) {
    return navigateTo('/login')
  }
})

主页

¥Home Page

现在我们有了应用中间件来保护路由,我们可以在主页上使用它来显示经过身份验证的用户信息。如果用户未通过身份验证,他们将被重定向到登录页面。

¥Now that we have our app middleware to protect our routes, we can use it on our home page that display our authenticated user information. If the user is not authenticated, they will be redirected to the login page.

我们将使用 definePageMeta 将中间件应用于我们想要保护的路由。

¥We'll use definePageMeta to apply the middleware to the route that we want to protect.

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>

我们还添加了一个注销按钮,用于清除会话并将用户重定向到登录页面。

¥We also added a logout button to clear the session and redirect the user to the login page.

结论

¥Conclusion

我们已经在 Nuxt 应用中成功设置了非常基本的用户身份验证和会话管理。我们还在服务器端和客户端保护了敏感路由,以确保只有经过身份验证的用户才能访问它们。

¥We've successfully set up a very basic user authentication and session management in our Nuxt app. We've also protected sensitive routes on the server and client side to ensure that only authenticated users can access them.

接下来,你可以:

¥As next steps, you can:

查看开源 atidone 代码库,获取包含 OAuth 身份验证、数据库和 CRUD 操作的 Nuxt 应用的完整示例。

¥Checkout the open source atidone repository for a full example of a Nuxt app with OAuth authentication, database and CRUD operations.