一、权限管理

1.1 权限分类

核心概念

权限管理本质上可以分为三类:

类型说明实现方式
功能权限决定用户可以访问哪些页面或功能模块动态路由
按钮权限决定用户在某个页面上可以使用哪些具体操作指令/组件控制
接口权限决定用户可以调用哪些接口服务端校验 + 前端配合

1.2 实现步骤

流程图:

登录 → 获取权限树 → 生成动态路由 → 添加路由 → 渲染菜单
                    ↓
              按钮权限控制
                    ↓
              接口权限校验

代码实现:

// 1. 登录后获取权限树
const userInfo = await login(credentials)
const permissionTree = await getPermissionTree(userInfo.id)
 
// 2. 生成动态路由
const routes = generateRoutes(permissionTree)
 
// 3. 添加路由
routes.forEach((route) => {
  router.addRoute(route)
})
 
// 4. 按钮权限指令
app.directive("permission", {
  mounted(el, binding) {
    const { value } = binding
    const hasPermission = store.state.permissions.includes(value)
    if (!hasPermission) {
      el.parentNode && el.parentNode.removeChild(el)
    }
  },
})

使用示例:

<template>
  <!-- 按钮权限 -->
  <button v-permission="'user:add'">添加用户</button>
 
  <!-- 条件渲染 -->
  <el-button v-if="hasPermission('user:delete')">删除</el-button>
</template>

二、Token 处理

2.1 Token 完整流程

登录 → 获取 Token → 存储 Token → 请求携带 → 验证 Token → 刷新/过期

2.2 请求拦截器配置

// request.js
import axios from "axios"
import { getToken, removeToken } from "@/utils/auth"
 
const request = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 15000,
})
 
// 请求拦截器
request.interceptors.request.use(
  (config) => {
    const token = getToken()
    if (token) {
      config.headers["Authorization"] = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  },
)
 
// 响应拦截器
request.interceptors.response.use(
  (response) => {
    const { code, data } = response.data
 
    // 业务错误处理
    if (code !== 200) {
      Message.error(data.message || "Error")
      return Promise.reject(new Error(data.message || "Error"))
    }
 
    return data
  },
  (error) => {
    // 401 Token 过期
    if (error.response?.status === 401) {
      removeToken()
      location.reload()
    }
    return Promise.reject(error)
  },
)
 
export default request

2.3 Token 存储方式对比

存储方式优点缺点安全性
localStorage持久化XSS 攻击⭐⭐
sessionStorage会话级安全刷新丢失⭐⭐⭐
Cookie (HttpOnly)防 XSS需服务端配合⭐⭐⭐⭐
Pinia/Vuex内存安全刷新丢失⭐⭐⭐

最佳实践

推荐组合:Cookie (HttpOnly) + 内存存储,双重保护


三、单点登录(SSO)

3.1 常见方案对比

方案适用场景实现难度
主域名共享 Cookie同主域名子系统
CAS跨域多系统⭐⭐⭐
OAuth2.0第三方登录⭐⭐⭐
JWT分布式系统⭐⭐

3.2 同主域名方案

// 主域名:baidu.com
// 子系统:xxx.baidu.com
 
// 设置 Cookie 时指定主域名
document.cookie = `token=${token}; domain=.baidu.com; path=/`

3.3 CAS 流程

1. 用户访问 system-a.com → 跳转 CAS 认证中心
2. 登录成功后生成全局会话 → 返回 system-a.com?ticket=ST-xxx
3. system-a 验证 ticket → 获取用户信息
4. 访问 system-b.com → 发现已有全局会话 → 直接登录

四、全局异常捕获

4.1 Vue 错误处理

// main.js
const app = createApp(App)
 
// Vue 级别错误
app.config.errorHandler = (err, vm, info) => {
  console.error("Vue Error:", err, info)
  // 发送错误日志到服务器
  reportError({
    message: err.message,
    stack: err.stack,
    info,
  })
}
 
// 全局未捕获错误
window.onerror = function (message, source, lineno, colno, error) {
  console.error("Global Error:", message, source, lineno, colno, error)
  reportError({ message, source, lineno, colno, error })
  return false
}
 
// Promise 错误
window.addEventListener("unhandledrejection", (event) => {
  console.error("Unhandled Promise:", event.reason)
  reportError({
    message: event.reason?.message || "Unhandled Promise",
    stack: event.reason?.stack,
  })
})
 
// 资源加载错误
window.addEventListener(
  "error",
  (event) => {
    if (event.target.tagName) {
      // 图片、脚本等资源加载错误
      console.error("Resource Error:", event.target.src || event.target.href)
    }
  },
  true,
)

4.2 错误上报服务

// utils/error-report.js
export function reportError(error) {
  const errorData = {
    url: window.location.href,
    userAgent: navigator.userAgent,
    timestamp: Date.now(),
    ...error,
  }
 
  // 使用 sendBeacon 保证离线也能发送
  if (navigator.sendBeacon) {
    navigator.sendBeacon("/api/error/log", JSON.stringify(errorData))
  } else {
    // 降级方案
    fetch("/api/error/log", {
      method: "POST",
      body: JSON.stringify(errorData),
      keepalive: true,
    })
  }
}

五、常见功能实现

5.1 防抖与节流

// 防抖(debounce)- n 秒后执行,期间触发重新计时
function debounce(fn, delay = 300) {
  let timer = null
  return function (...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}
 
// 节流(throttle)- n 秒内只执行一次
function throttle(fn, delay = 300) {
  let lastTime = 0
  return function (...args) {
    const now = Date.now()
    if (now - lastTime >= delay) {
      fn.apply(this, args)
      lastTime = now
    }
  }
}

5.2 深拷贝

// 简单深拷贝
function deepClone(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== "object") return obj
  if (obj instanceof Date) return new Date(obj)
  if (obj instanceof RegExp) return new RegExp(obj)
 
  // 循环引用检测
  if (map.has(obj)) return map.get(obj)
 
  const clone = Array.isArray(obj) ? [] : {}
  map.set(obj, clone)
 
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], map)
    }
  }
  return clone
}

5.3 数组转树形结构

// 数组转树
function arrayToTree(array, parentId = null) {
  return array
    .filter((item) => item.parentId === parentId)
    .map((item) => ({
      ...item,
      children: arrayToTree(array, item.id),
    }))
}
 
// 树转数组
function treeToArray(tree) {
  return tree.reduce((acc, item) => {
    const { children, ...rest } = item
    acc.push(rest)
    if (children?.length) {
      acc.push(...treeToArray(children))
    }
    return acc
  }, [])
}

相关链接