Nuxt 中间件与认证流程
中间件基础
什么是中间件
中间件是在组件渲染之前执行的函数,可以用于路由保护、重定向、验证等。
请求流程:
导航到路由
↓
执行全局中间件
↓
执行路由级中间件
↓
执行页面组件
↓
渲染页面
中间件的使用场景
| 场景 | 中间件用途 |
|---|---|
| 认证 | 检查用户是否登录 |
| 授权 | 检查用户是否有权访问 |
| 日志 | 记录用户行为 |
| 数据预加载 | 页面加载前获取数据 |
| 重定向 | 根据条件重定向 |
创建中间件
1. 路由级中间件
// middleware/auth.ts
export default defineRouteMiddleware((to, from) => {
// 检查用户是否已登录
const authStore = useAuthStore()
if (!authStore.isLoggedIn) {
// 未登录时重定向到登录页
return navigateTo('/login')
}
})
// middleware/admin.ts
export default defineRouteMiddleware((to, from) => {
const authStore = useAuthStore()
if (!authStore.isAdmin) {
// 无管理员权限时中止导航
return abortNavigation('无权访问此页面')
}
})
// middleware/analytics.ts
export default defineRouteMiddleware((to, from) => {
// 中间件可以访问 Nuxt 上下文
console.log(`用户导航到:${to.path}`)
})
2. 在页面中使用中间件
<!-- pages/dashboard.vue -->
<template>
<div>
<h1>用户仪表板</h1>
<p>只有登录用户才能看到此页面</p>
</div>
</template>
<script setup lang="ts">
// 应用 auth 中间件
definePageMeta({
middleware: 'auth'
})
</script>
<!-- pages/admin/index.vue -->
<template>
<div>
<h1>管理面板</h1>
</div>
</template>
<script setup lang="ts">
// 应用多个中间件,按顺序执行
definePageMeta({
middleware: ['auth', 'admin']
})
</script>
3. 全局中间件
// middleware/tracking.global.ts
export default defineRouteMiddleware((to, from) => {
// 这个中间件会在每次路由变化时执行
const analytics = useAnalytics()
analytics.trackPageView({
path: to.path,
name: to.name,
timestamp: Date.now()
})
})
// middleware/logger.global.ts
export default defineRouteMiddleware((to, from) => {
console.log(`从 ${from.path} 导航到 ${to.path}`)
})
认证系统实现
1. 认证 Store
// stores/auth.ts
import { defineStore } from 'pinia'
import type { User } from '~/types'
export const useAuthStore = defineStore('auth', () => {
// 状态
const user = ref<User | null>(null)
const token = ref<string | null>(null)
const isLoggedIn = computed(() => !!user.value)
const isAdmin = computed(() => user.value?.role === 'admin')
// 从 localStorage 恢复
const restoreAuth = () => {
if (process.client) {
const savedToken = localStorage.getItem('auth_token')
const savedUser = localStorage.getItem('auth_user')
if (savedToken && savedUser) {
token.value = savedToken
user.value = JSON.parse(savedUser)
}
}
}
// 登录
const login = async (email: string, password: string) => {
try {
const response = await $fetch('/api/auth/login', {
method: 'POST',
body: { email, password }
})
token.value = response.token
user.value = response.user
// 保存到 localStorage
if (process.client) {
localStorage.setItem('auth_token', response.token)
localStorage.setItem('auth_user', JSON.stringify(response.user))
}
return true
} catch (error) {
console.error('登录失败:', error)
return false
}
}
// 登出
const logout = () => {
token.value = null
user.value = null
if (process.client) {
localStorage.removeItem('auth_token')
localStorage.removeItem('auth_user')
}
}
// 检查会话(SSR 时调用)
const checkSession = async () => {
if (!token.value) return
try {
const response = await $fetch('/api/auth/verify', {
headers: {
Authorization: `Bearer ${token.value}`
}
})
user.value = response.user
return true
} catch (error) {
// 令牌无效,清除
logout()
return false
}
}
return {
user: readonly(user),
token: readonly(token),
isLoggedIn,
isAdmin,
login,
logout,
restoreAuth,
checkSession
}
})
2. 认证中间件
// middleware/auth.ts
export default defineRouteMiddleware(async (to, from) => {
const authStore = useAuthStore()
// 如果已登录,直接返回
if (authStore.isLoggedIn) {
return
}
// 检查是否有 token(SSR 时从 cookie 读取)
const authCookie = useCookie('auth_token')
if (authCookie.value && !authStore.token) {
// 从 cookie 恢复
authStore.restoreAuth()
// 验证 token 有效性
const isValid = await authStore.checkSession()
if (!isValid) {
return navigateTo('/login')
}
} else if (!authStore.isLoggedIn) {
// 未登录,重定向到登录页
return navigateTo({
path: '/login',
query: { redirect: to.fullPath } // 登录后重定向回此页
})
}
})
// middleware/admin.ts
export default defineRouteMiddleware((to, from) => {
const authStore = useAuthStore()
if (!authStore.isAdmin) {
throw createError({
statusCode: 403,
statusMessage: '禁止访问'
})
}
})
3. 登录页面
<!-- pages/login.vue -->
<template>
<div class="login-container">
<form @submit.prevent="handleLogin">
<h1>登录</h1>
<div class="form-group">
<label>邮箱</label>
<input
v-model="email"
type="email"
required
>
</div>
<div class="form-group">
<label>密码</label>
<input
v-model="password"
type="password"
required
>
</div>
<button type="submit" :disabled="loading">
{{ loading ? '登录中...' : '登录' }}
</button>
<p v-if="error" class="error">{{ error }}</p>
</form>
</div>
</template>
<script setup lang="ts">
definePageMeta({
// 登录页面不需要认证
middleware: []
})
const authStore = useAuthStore()
const router = useRouter()
const route = useRoute()
const email = ref('')
const password = ref('')
const loading = ref(false)
const error = ref('')
const handleLogin = async () => {
loading.value = true
error.value = ''
const success = await authStore.login(email.value, password.value)
if (success) {
// 登录成功,重定向
const redirect = route.query.redirect as string || '/dashboard'
await navigateTo(redirect)
} else {
error.value = '邮箱或密码错误'
loading.value = false
}
}
</script>
高级认证功能
1. 刷新令牌
// server/api/auth/refresh.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const refreshToken = body.refreshToken
// 验证 refresh token
try {
const decoded = verifyRefreshToken(refreshToken)
const user = await db.user.findById(decoded.userId)
// 生成新的访问令牌
const newAccessToken = generateAccessToken(user)
return {
accessToken: newAccessToken,
user
}
} catch (error) {
throw createError({
statusCode: 401,
statusMessage: '令牌已过期'
})
}
})
// 在 HTTP 拦截器中使用
export default defineNuxtPlugin(() => {
const authStore = useAuthStore()
$fetch.create({
async onResponse({ response }) {
// 令牌过期时尝试刷新
if (response.status === 401) {
const refreshToken = useCookie('refresh_token').value
if (refreshToken) {
const newTokens = await $fetch('/api/auth/refresh', {
method: 'POST',
body: { refreshToken }
})
// 更新令牌
authStore.token = newTokens.accessToken
// 重试原始请求
// ... 实现重试逻辑
}
}
}
})
})
2. 权限控制
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
// 保护 /api/protected 下的所有路由
if (event.node.req.url?.startsWith('/api/protected')) {
const authHeader = getHeader(event, 'authorization')
if (!authHeader) {
throw createError({
statusCode: 401,
statusMessage: '未提供认证令牌'
})
}
try {
const token = authHeader.replace('Bearer ', '')
const decoded = verifyToken(token)
// 将用户信息添加到事件上下文
event.context.user = decoded
} catch (error) {
throw createError({
statusCode: 401,
statusMessage: '无效的令牌'
})
}
}
})
// 在 API 路由中使用
// server/api/protected/profile.get.ts
export default defineEventHandler(async (event) => {
const user = event.context.user
if (user.role !== 'admin') {
throw createError({
statusCode: 403,
statusMessage: '无权访问'
})
}
return {
user,
profile: await db.user.findById(user.id)
}
})
3. OAuth 集成
// server/api/auth/github.get.ts
export default defineOAuthEventHandler({
async onSuccess(event, { user }) {
// 处理成功的 OAuth 登录
// 在数据库中创建或更新用户
let dbUser = await db.user.findByEmail(user.email)
if (!dbUser) {
dbUser = await db.user.create({
email: user.email,
name: user.name,
avatar: user.avatar,
provider: 'github'
})
}
// 生成认证令牌
const token = generateAuthToken(dbUser)
// 设置 cookie
setCookie(event, 'auth_token', token, {
httpOnly: true,
maxAge: 60 * 60 * 24 * 30 // 30 天
})
// 重定向到首页
return sendRedirect(event, '/')
},
async onError(event, error) {
console.error('OAuth 错误:', error)
return sendRedirect(event, '/login?error=oauth_failed')
}
})
安全最佳实践
// ✅ 安全检查清单
// 1. HTTPS only
export const useAuthConfig = {
secure: !process.dev, // 生产环境必须 HTTPS
httpOnly: true, // JavaScript 无法访问 cookie
sameSite: 'strict' // CSRF 保护
}
// 2. Token 过期
const TOKEN_EXPIRY = 15 * 60 * 1000 // 15 分钟
const REFRESH_TOKEN_EXPIRY = 30 * 24 * 60 * 60 * 1000 // 30 天
// 3. 密码哈希
import bcrypt from 'bcrypt'
export const hashPassword = (password: string) => {
return bcrypt.hash(password, 10)
}
export const verifyPassword = (password: string, hash: string) => {
return bcrypt.compare(password, hash)
}
// 4. CSRF 保护
// 在表单中添加 CSRF token
<input type="hidden" :value="csrfToken" name="_csrf">
// 5. 速率限制
import rateLimit from 'express-rate-limit'
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 5 // 最多 5 次尝试
})
// 6. 会话超时
export const SESSION_TIMEOUT = 30 * 60 * 1000 // 30 分钟
会话管理
// 使用 Nuxt App 插件初始化认证
// plugins/auth.client.ts
export default defineNuxtPlugin(async () => {
const authStore = useAuthStore()
// 从 localStorage 恢复会话
authStore.restoreAuth()
})
// plugins/auth.server.ts
export default defineNuxtPlugin(async () => {
const authStore = useAuthStore()
// 从 cookie 恢复会话
const authCookie = useCookie('auth_token')
if (authCookie.value) {
// 验证令牌
await authStore.checkSession()
}
})
总结
认证系统的关键点:
- 安全性第一 - HTTPS、密码哈希、令牌过期
- 会话管理 - 正确使用 cookie 和 storage
- 错误处理 - 清晰的错误消息
- 用户体验 - 无缝的登录流程
- 可扩展性 - 支持多种认证方式


