Nuxt 中间件与认证流程

HTMLPAGE 团队
8 分钟阅读

详细讲解 Nuxt 3 中间件系统和认证实现,包括路由中间件、全局中间件、认证流程、权限控制和会话管理。帮助开发者构建安全的认证系统。

#Nuxt #中间件 #认证 #权限控制 #安全

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()
  }
})

总结

认证系统的关键点:

  1. 安全性第一 - HTTPS、密码哈希、令牌过期
  2. 会话管理 - 正确使用 cookie 和 storage
  3. 错误处理 - 清晰的错误消息
  4. 用户体验 - 无缝的登录流程
  5. 可扩展性 - 支持多种认证方式

推荐资源