基础篇八 Nuxt4 中间件进阶:请求拦截与权限校验

文章目录

个人网站

前面我们学过路由守卫,用中间件控制页面访问权限。今天深入聊聊中间件的高级用法,包括请求拦截、权限校验、数据预加载等实战技巧。

一、中间件执行流程

理解执行顺序很重要:

复制代码
1. 全局中间件(按字母排序)
2. 页面定义的中间件
3. 布局中间件

目录结构:

复制代码
middleware/
├── 01-auth.global.ts    # 全局,先执行
├── 02-logging.global.ts # 全局,后执行
├── admin.ts             # 页面级
└── guest.ts             # 页面级

二、登录状态检查

最常见的场景:

ts 复制代码
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const userStore = useUserStore()
  
  // 未登录,跳转登录页
  if (!userStore.isLoggedIn) {
    // 保存原目标路径,登录后跳回
    return navigateTo({
      path: '/login',
      query: { redirect: to.fullPath }
    })
  }
})

登录页面:

vue 复制代码
<script setup lang="ts">
const route = useRoute()
const userStore = useUserStore()

const form = reactive({
  email: '',
  password: ''
})

const login = async () => {
  await userStore.login(form.email, form.password)
  
  // 登录成功,跳回原页面
  const redirect = route.query.redirect as string
  navigateTo(redirect || '/')
}
</script>

三、角色权限校验

不同用户有不同权限:

ts 复制代码
// middleware/role.ts
type Role = 'admin' | 'editor' | 'user'

export default defineNuxtRouteMiddleware((to, from) => {
  const userStore = useUserStore()
  
  // 从路由 meta 获取需要的角色
  const requiredRole = to.meta.role as Role
  
  if (!requiredRole) return
  
  // 未登录或权限不足
  if (!userStore.user || !hasPermission(userStore.user.role, requiredRole)) {
    return navigateTo('/403')
  }
})

// 权限等级
const roleLevel: Record<Role, number> = {
  admin: 3,
  editor: 2,
  user: 1
}

const hasPermission = (userRole: Role, requiredRole: Role) => {
  return roleLevel[userRole] >= roleLevel[requiredRole]
}

页面中使用:

vue 复制代码
<script setup lang="ts">
definePageMeta({
  middleware: 'role',
  meta: {
    role: 'admin'  // 只有管理员能访问
  }
})
</script>

四、页面数据预加载

中间件可以预加载数据,避免页面闪烁:

ts 复制代码
// middleware/fetch-user.global.ts
export default defineNuxtRouteMiddleware(async (to, from) => {
  const userStore = useUserStore()
  const token = useCookie('token')
  
  // 有 token 但没有用户数据,尝试获取
  if (token.value && !userStore.user) {
    try {
      await userStore.fetchUser()
    } catch {
      // token 失效,清除
      token.value = null
    }
  }
})

五、访问日志记录

记录页面访问:

ts 复制代码
// middleware/logging.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
  const userStore = useUserStore()
  
  // 只在客户端记录
  if (import.meta.client) {
    const logData = {
      userId: userStore.user?.id || 'anonymous',
      from: from.path,
      to: to.path,
      timestamp: new Date().toISOString(),
      userAgent: navigator.userAgent
    }
    
    // 发送到日志服务(不阻塞导航)
    $fetch('/api/logs', {
      method: 'POST',
      body: logData
    }).catch(console.error)
  }
})

六、IP 黑名单(服务端)

ts 复制代码
// middleware/ip-block.global.ts
export default defineNuxtRouteMiddleware(async (to, from) => {
  // 只在服务端执行
  if (import.meta.server) {
    const event = useRequestEvent()
    const ip = getRequestIP(event, { xForwardedFor: true })
    
    // 检查 IP 是否被封禁
    const blocked = await checkBlockedIP(ip)
    
    if (blocked) {
      return navigateTo('/blocked')
    }
  }
})

const checkBlockedIP = async (ip: string): Promise<boolean> => {
  // 实际项目中可以从数据库或 Redis 查询
  const blockedIPs = ['192.168.1.100', '10.0.0.50']
  return blockedIPs.includes(ip)
}

七、API 请求拦截

服务端 API 路由也可以用中间件:

ts 复制代码
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  const url = getRequestURL(event)
  
  // 只拦截需要认证的接口
  if (!url.pathname.startsWith('/api/protected/')) {
    return
  }
  
  const token = getHeader(event, 'Authorization')?.replace('Bearer ', '')
  
  if (!token) {
    throw createError({
      statusCode: 401,
      message: 'Unauthorized'
    })
  }
  
  try {
    const user = await verifyToken(token)
    event.context.user = user
  } catch {
    throw createError({
      statusCode: 401,
      message: 'Invalid token'
    })
  }
})

API 接口中使用:

ts 复制代码
// server/api/protected/profile.ts
export default defineEventHandler((event) => {
  const user = event.context.user
  
  return {
    id: user.id,
    name: user.name,
    email: user.email
  }
})

八、请求限流

防止接口被滥用:

ts 复制代码
// server/middleware/rate-limit.ts
const requests = new Map<string, number[]>()

export default defineEventHandler((event) => {
  const ip = getRequestIP(event)
  const now = Date.now()
  const windowMs = 60 * 1000  // 1 分钟
  const maxRequests = 100     // 最多 100 次请求
  
  // 获取该 IP 的请求记录
  const ipRequests = requests.get(ip) || []
  
  // 过滤掉窗口外的请求
  const recentRequests = ipRequests.filter(time => now - time < windowMs)
  
  // 检查是否超限
  if (recentRequests.length >= maxRequests) {
    throw createError({
      statusCode: 429,
      message: 'Too many requests'
    })
  }
  
  // 记录本次请求
  recentRequests.push(now)
  requests.set(ip, recentRequests)
})

九、CORS 配置

ts 复制代码
// server/middleware/cors.ts
export default defineEventHandler((event) => {
  const allowedOrigins = [
    'https://example.com',
    'https://admin.example.com'
  ]
  
  const origin = getHeader(event, 'origin') || ''
  
  if (allowedOrigins.includes(origin)) {
    setResponseHeaders(event, {
      'Access-Control-Allow-Origin': origin,
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
      'Access-Control-Allow-Credentials': 'true'
    })
  }
  
  // 处理预检请求
  if (getMethod(event) === 'OPTIONS') {
    setResponseStatus(event, 204)
    return ''
  }
})

十、全局错误处理

ts 复制代码
// server/middleware/error-handler.ts
export default defineEventHandler((event) => {
  // 设置错误处理
  event.node.res.on('finish', () => {
    const status = getResponseStatus(event)
    
    if (status >= 400) {
      const url = getRequestURL(event)
      console.error(`Error ${status}: ${url}`)
      // 发送到错误监控服务
    }
  })
})

总结

中间件高级用法:

场景 实现方式
登录检查 全局中间件 + token 判断
角色权限 to.meta.role + 权限比较
数据预加载 中间件中调用 store 方法
访问日志 全局中间件 + 异步上报
IP 黑名单 服务端中间件 + IP 检查
API 拦截 server/middleware/
请求限流 Map 记录 + 时间窗口

下一篇聊聊插件系统,学习如何扩展 Nuxt 能力。

相关文章

入门篇三:Nuxt4组件自动导入:写代码少敲一半字

入门篇二:Nuxt 4路由自动生成:告别手动配置路由的日子

延伸阅读

nuxt4完整系列,持续更新中。。,欢迎来逛逛


内容有帮助?点赞、收藏、关注三连!评论区等你 💪

相关推荐
brien921 分钟前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance36 分钟前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M42 分钟前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写44 分钟前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李1 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易1 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马1 小时前
前沿视界
前端
kyriewen1 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易2 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员
IT_陈寒2 小时前
Vue的这个响应式陷阱,差点让我加班到天亮
前端·人工智能·后端