会话与鉴权
鉴权是 Web 应用中极其常见的需求。本配方将向你展示如何在你的 Nuxt 应用中实现基本的用户注册与鉴权。
简介
在这个配方中,我们将使用 Nuxt Auth Utils 在一个全栈 Nuxt 应用中设置鉴权,它提供了便捷的工具函数来管理客户端和服务端的会话数据。
该模块使用加密且密封(sealed)的 cookie 来存储会话数据,因此你无需搭建数据库来存储会话数据。
安装 nuxt-auth-utils
使用 nuxt CLI 安装 nuxt-auth-utils 模块。
npx nuxt module add auth-utils
nuxt-auth-utils 作为依赖安装,并把它推入我们 nuxt.config.ts 的 modules 部分。Cookie 加密密钥
由于 nuxt-auth-utils 使用密封 cookie 来存储会话数据,会话 cookie 会使用来自 NUXT_SESSION_PASSWORD 环境变量的密钥进行加密。
.env 中。NUXT_SESSION_PASSWORD=a-random-password-with-at-least-32-characters
在部署之前,你需要将此环境变量添加到你的生产环境中。
登录 API 路由
对于这个配方,我们将创建一个简单的 API 路由,基于静态数据来登录一个用户。
让我们创建一个 /api/login API 路由,它接受一个 POST 请求,请求体中包含 email 和 password。
import { z } from 'zod'
const bodySchema = z.object({
email: z.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') {
// 在 cookie 中设置用户会话
// 这个服务端工具函数由 auth-utils 模块自动导入
await setUserSession(event, {
user: {
name: 'John Doe',
},
})
return {}
}
throw createError({
status: 401,
message: 'Bad credentials',
})
})
zod 依赖(npm i zod)。登录页
该模块暴露了一个 Vue 组合式函数,用于了解用户是否在我们的应用中已鉴权:
<script setup>
const { loggedIn, session, user, clear, fetch } = useUserSession()
</script>
让我们创建一个登录页,带有一个表单,将登录数据提交到我们的 /api/login 路由。
<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 路由的示例,只有已鉴权的用户才能访问。
export default defineEventHandler(async (event) => {
// 确保用户已登录
// 如果请求不是来自有效的用户会话,这会抛出 401 错误
const { user } = await requireUserSession(event)
// TODO: 基于用户获取一些统计数据
return {}
})
保护应用路由
有了服务端路由的保护,我们的数据是安全的,但如果不做其它处理,未鉴权的用户在尝试访问 /users 页面时可能会得到一些奇怪的数据。我们应该创建一个客户端中间件 来在客户端保护该路由,并将用户重定向到登录页。
nuxt-auth-utils 提供了一个便捷的 useUserSession 组合式函数,我们将用它来检查用户是否已登录,如果未登录则重定向。
我们将在 /middleware 目录中创建一个中间件。与服务端不同,客户端中间件不会自动应用到所有端点,我们需要指定要应用它的位置。
export default defineNuxtRouteMiddleware(() => {
const { loggedIn } = useUserSession()
// 如果用户未鉴权,将其重定向到登录页
if (!loggedIn.value) {
return navigateTo('/login')
}
})
首页
既然我们已经有了应用中间件来保护路由,我们就可以在首页上使用它来展示已鉴权用户的信息。如果用户未鉴权,他们将被重定向到登录页。
我们将使用 definePageMeta 将中间件应用到我们想要保护的路由上。
<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 应用中设置了一个非常基础的用户鉴权与会话管理。我们还在服务端和客户端保护了敏感路由,确保只有已鉴权的用户才能访问它们。
作为后续步骤,你可以:
- 使用支持的 20+ 个 OAuth 提供商 添加鉴权
- 添加数据库来存储用户,参见 Nitro SQL Database 或 NuxtHub SQL Database
- 让用户使用 email & password 注册,使用密码哈希
- 添加对 WebAuthn / Passkeys 的支持
查看开源的 atidone 仓库,获取一个带有 OAuth 鉴权、数据库和 CRUD 操作的完整 Nuxt 应用示例。