后台管理系统八股

项⽬地址:https://github.com/Xiaodie-888/Frontend.git 前端

                  https://github.com/Xiaodie-888/backend.git 后端
技术栈:Vue3+Vite+Typrscript+Pinia+Element-plus+Vue-Router+Express.js+MySQL

核⼼⼯作与技术:

  1. 基础组件封装:基于 Element Plus 封装了 SearchForm 和 DataTable 两个核⼼业务组件,使⽤ TypeScript 定义类型系统,并将分⻚等通⽤逻辑提取到 useTable Hook 中,使组件在三个核⼼业务模块中复⽤,提升开发效率。
  2. 权限控制实现:实现了基于 JWT 的登录认证流程,开发了动态路由加载和按钮级权限控制,封装统⼀的 axios 请求拦截器处理 token,有效解决了系统权限管理混乱的问题。
  3. 性能优化:通过路由懒加载和 keep-alive 缓存优化⾸屏加载,针对产品列表实现分⻚加载和表格组件更新优化,显著提升了系统响应速度。

基于JWT的完整权限认证系统实现

循序渐进的系统构建过程

第一步:搭建JWT认证基础

先在后端建立JWT认证机制,使用jsonwebtoken库生成Token。用户登录时,系统验证账号密码后,将用户信息(排除密码等敏感数据)加密生成7小时有效期的JWT Token,以Bearer格式返回给前端。同时返回用户的department(部门)和position(职位)信息,为后续权限控制奠定基础。

第二步:封装axios请求拦截器

再在前端封装统一的HTTP请求处理机制。在请求拦截器中自动从localStorage获取Token并添加到请求头的Authorization字段;在响应拦截器中统一处理各种状态码,特别是401状态码时自动清除本地存储并跳转登录页,实现Token失效的自动处理。这样开发者无需手动管理Token状态。

第三步:开发动态路由加载系统

然后基于用户角色实现路由的动态生成。后端根据用户部门返回对应的路由配置(超级管理员拥有全部8个模块,人事部仅有用户管理,产品部拥有产品相关3个模块)。前端通过import.meta.glob动态导入组件,使用router.addRoute将权限路由添加到Vue Router中,配合Pinia的数据持久化,确保页面刷新后路由状态保持。

第四步:实现按钮级权限控制

接着建立细粒度的权限控制体系。采用部门级 + 职位级双重权限模型:通过v-if="userAdmin||superAdmin"控制菜单显示;基于职位进行功能权限控制,如审核功能用v-if="!admin()"限制仅经理可操作;在关键操作函数中进行权限拦截,如if(position=='员工') return阻止员工编辑产品。形成了从路由→菜单→按钮→操作的四级权限防护。

第五步:状态管理与持久化

最后使用Pinia管理权限状态,配合pinia-plugin-persistedstate插件实现数据持久化。将用户信息、权限数据、动态路由等关键状态进行统一管理,确保系统重启或页面刷新后用户无需重新登录。

说说你对keep-alive的理解是什么?

一、keep-alive 的核心功能

keep-alive是 Vue 的内置组件,主要作用是缓存组件实例避免组件切换时重复渲染 DOM,从而保留组件状态。当它包裹动态组件时,未激活的组件会被缓存到内存中,而非销毁,再次切换时直接从缓存中恢复状态。

通过keep-alive,Vue 实现了高效的组件状态管理,尤其适用于标签页切换、多视图缓存等场景,能显著提升用户体验和应用性能。

二、关键属性与用法
  1. 核心属性

    • include:指定需要缓存的组件名称(字符串、正则或数组)。
    • exclude:指定不需要缓存的组件名称(字符串、正则或数组)。
    • max:限制缓存的组件数量,超出时按 LRU(最近最少使用)规则移除最早的缓存。
  2. 基本用法

    
    
      
    
    
    
    
      
    
    
     

    组件匹配规则:优先检查name选项,若不存在则匹配局部注册名称,匿名组件无法被缓存。

三、生命周期钩子

keep-alive缓存的组件会新增两个钩子

  • activated:组件被激活(从缓存中恢复)时调用。
  • deactivated:组件被停用(缓存到内存)时调用。

生命周期对比

  • 首次进入组件:beforeCreate → created → mounted → activated
  • 切换后再次进入:activated(直接从缓存恢复,跳过mounted
四、在路由中的应用

通过路由元信息meta.keepAlive控制组件缓存:

  1. 路由配置

    const routes = [
      {
        path: '/list',
        name: 'ItemList',
        component: ItemList,
        meta: { keepAlive: true } // 标记需要缓存
      }
    ]
    
  2. 组件使用

    
      
      
    
    
    
    
五、原理分析
  1. 缓存机制

    • keep-alive通过cache对象存储组件实例,键为组件key,值为组件 VNode。
    • keys数组记录缓存顺序,用于实现max限制下的 LRU 淘汰策略。
  2. 渲染逻辑

    • 首次渲染时,组件会被存入cachekeys添加对应key
    • 再次切换时,key存在于cache中,直接从缓存中获取组件实例,避免重新创建。
  3. 源码核心片段

    render() {
      const vnode = getFirstComponentChild(this.$slots.default)
      const componentOptions = vnode?.componentOptions
      const name = getComponentName(componentOptions)
      
      // 检查是否需要缓存(通过include/exclude过滤)
      if (需要缓存) {
        const key = vnode.key || componentOptions.Ctor.cid + '::' + componentOptions.tag
        if (this.cache[key]) {
          // 命中缓存,直接使用实例
          vnode.componentInstance = this.cache[key].componentInstance
          this.keys.push(this.keys.splice(this.keys.indexOf(key), 1)[0]) // 调整缓存顺序
        } else {
          // 未命中,存入缓存
          this.cache[key] = vnode
          this.keys.push(key)
          // 超出max时删除最早的缓存
          if (this.max && this.keys.length > this.max) {
            pruneCacheEntry(this.cache, this.keys[0], this.keys, this._vnode)
          }
        }
        vnode.data.keepAlive = true
      }
      return vnode
    }
    
六、缓存数据更新方案

当组件被缓存时,数据可能不会自动更新,可通过以下方式解决:

  1. activated钩子

    • 组件激活时重新获取数据:
    activated() {
      this.fetchData() // 重新请求数据
    }
    
  2. beforeRouteEnter导航守卫

    • 路由进入前触发,通过next(vm => ...)访问组件实例:
    beforeRouteEnter(to, from, next) {
      next(vm => {
        vm.fetchData() // 在组件实例中调用方法
      })
    }
    
七、注意事项
  • 服务器端渲染(SSR)activateddeactivated在 SSR 期间不生效。
  • 组件销毁:被exclude匹配的组件会被从缓存中移除,并触发$destroy
  • 性能优化:合理设置max,避免缓存过多组件导致内存占用过高。

Javascript本地存储的方式有哪些?区别及应用场景?

一、本地存储方式及特点

1. Cookie
  • 本质:小型文本文件,用于解决 HTTP 无状态问题,由服务器设置并发送到客户端存储。
  • 特点
    • 存储容量:单个 Cookie 不超过 4KB,每个域名最多存储 20 个 Cookie。
    • 生命周期:可通过 Expires 或 Max - Age 设置过期时间,若未设置则随浏览器关闭失效。
    • 数据类型仅支持字符串,需手动解析(如 JSON.stringify())。
    • 网络传输:每次 HTTP 请求时自动携带到服务器,增加请求头开销。
    • 安全性:可通过 Secure 属性限制仅在 HTTPS 下传输,HttpOnly 属性防止 XSS 攻击。
  • 使用示例
    // 设置 Cookie
    document.cookie = "username=John; expires=Thu, 31 Dec 2025 23:59:59 GMT; path=/";
    // 获取 Cookie
    const cookies = document.cookie.split('; ').reduce((obj, cookie) => {
      const [key, value] = cookie.split('=');
      obj[key] = value;
      return obj;
    }, {});
    // 删除 Cookie(设置过期时间为过去)
    document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
    
2. localStorage
  • 本质:HTML5 新增的本地存储 API,基于域名持久化存储数据。
  • 特点
    • 存储容量:通常为 5 - 10MB,不同浏览器略有差异。
    • 生命周期永久存储,除非手动删除或浏览器清除缓存。
    • 数据类型:仅支持字符串,需通过 JSON.stringify() 和 JSON.parse() 处理对象。
    • 共享性同一域名下的所有页面共享数据,支持跨窗口通信(通过 storage 事件)
    • 当本页操作(新增、修改、删除)了localStorage的时候,本页面不会触发storage事件,但是别的页面会触发storage事件。
  • 使用示例
    // 存储对象
    localStorage.setItem('user', JSON.stringify({ name: 'Alice', age: 25 }));
    // 获取数据
    const user = JSON.parse(localStorage.getItem('user'));
    // 删除单个数据
    localStorage.removeItem('user');
    // 清空所有数据
    localStorage.clear();
    

localStorage 也不是完美的,它有两个缺点:

  • 无法像Cookie一样设置过期时间
  • 只能存入字符串,无法直接存对象
3. sessionStorage
  • 本质:与 localStorage 类似,但生命周期更短。
  • 特点
    • 存储容量:同 localStorage,约 5 - 10MB。
    • 生命周期:仅在当前浏览器会话(窗口)中有效,关闭窗口后数据自动删除。
    • 共享性:同一域名、同一窗口下的页面共享数据,不同窗口间不共享。
    • 数据类型:同 localStorage,仅支持字符串。
  • 使用示例
    // 存储临时数据
    sessionStorage.setItem('tempData', '临时内容');
    // 在同窗口其他页面获取数据
    const temp = sessionStorage.getItem('tempData');
    
4. IndexedDB
  • 本质:低级异步数据库 API,用于存储大量结构化数据。
  • 特点
    • 存储容量:理论上无上限(受浏览器和硬件限制),适合存储大量数据。
    • 生命周期:永久存储,除非手动删除。
    • 数据类型:支持存储 JavaScript 对象、文件(Blob)等复杂数据类型。
    • 操作方式:异步操作,不阻塞主线程,支持事务(Transaction)机制。
    • 索引机制:支持创建索引,实现高效查询(如按字段排序、模糊搜索)。
  • 使用示例(简化流程)
    // 打开/创建数据库
    const request = indexedDB.open('MyDatabase', 1);
    request.onsuccess = (event) => {
      const db = event.target.result;
      // 开始事务
      const transaction = db.transaction(['users'], 'readwrite');
      const store = transaction.objectStore('users');
      // 添加数据
      const user = { id: 1, name: 'Bob', email: '[email protected]' };
      const addRequest = store.add(user);
      addRequest.onsuccess = () => console.log('数据添加成功');
    };
    

二、核心区别对比

维度 Cookie localStorage sessionStorage IndexedDB
存储容量 4KB 以内 5 - 10MB 5 - 10MB 理论无上限
生命周期 可设置过期时间,或随浏览器关闭失效 永久存储,手动删除才失效 窗口关闭后失效 永久存储,手动删除才失效
数据类型 仅字符串 仅字符串(需序列化对象) 仅字符串(需序列化对象) 支持对象、Blob 等复杂类型
网络传输

自动携带到服务器

(每次 HTTP 请求时自动携带到服务器,增加请求头开销。)

仅本地存储,不参与传输 仅本地存储,不参与传输 仅本地存储,不参与传输
异步性 同步操作 部分浏览器同步 部分浏览器同步 异步操作
共享性 同域名下共享 同域名下所有窗口共享 同域名、同窗口下共享 同域名下共享
安全性 可通过 Secure/HttpOnly 增强 本地存储,需前端自行控制 本地存储,需前端自行控制 本地存储,需前端自行控制

三、应用场景

1. Cookie
  • 场景
    • 身份验证:存储登录令牌(如 JWT),随请求发送到服务器验证身份。
    • 用户偏好:记录用户主题设置、语言偏好等轻量级数据。
    • 购物车标记:临时存储未登录用户的购物车商品(结合后端同步)。
  • 注意:由于每次请求都会携带 Cookie,避免存储大量数据,且需注意安全风险(如 XSS、CSRF)。
2. localStorage
  • 场景
    • 持久化用户数据:存储用户设置、收藏夹、已读状态等长期需要保留的数据。
    • 离线缓存:缓存静态资源(如图片、JS/CSS 文件),减少重复请求。
    • 跨页面状态共享:如多标签页间共享购物车信息(通过 storage 事件监听)。
  • 示例
    • 电商网站存储用户勾选的「记住登录状态」选项。
    • 新闻类应用缓存已浏览的文章列表,避免重复加载。
3. sessionStorage
  • 场景
    • 临时会话数据:存储表单临时输入内容(如多步骤表单的中间数据),防止刷新丢失。
    • 单次会话隐私数据:如敏感操作的临时验证码、一次性登录凭证。
    • 单窗口状态管理:如视频播放进度(仅在当前窗口有效)。
  • 示例
    • 银行网站的转账页面,存储临时输入的金额和账号信息。
    • 在线考试系统中存储当前题目的答案,防止窗口关闭后数据丢失。
4. IndexedDB
  • 场景
    • 大规模数据存储:如离线地图、电子书库、大量用户日志记录。
    • 复杂数据查询:需要按字段索引查询(如按时间排序的聊天记录)。
    • 离线应用:如 PWA(渐进式 Web 应用)的本地数据存储,支持离线访问。
    • 文件存储:存储用户上传的文件(Blob),如在线文档编辑器的历史版本。
  • 示例
    • 笔记类应用存储数千条笔记及其附件。
    • 音乐播放器缓存用户下载的歌曲列表和元数据。

四、选择建议

  • 轻量级、需服务器交互的数据:选 Cookie(如登录令牌)。
  • 持久化、跨会话的简单数据:选 localStorage(如用户设置)。
  • 临时、单会话的简单数据:选 sessionStorage(如表单临时输入)。
  • 大量、复杂结构化数据或离线应用:选 IndexedDB(如大型文档库)。

hash 路由和 history 路由的区别是什么

一、核心原理差异
  1. hash 路由(锚点路由)

    1. 原理:利用 URL 中的 # 符号(锚点)及其后的内容(如 #/home)作为路由标识。浏览器对 # 后的内容不会向服务器发送请求,仅作为前端页面内部的锚点定位。
    2. 示例https://example.com/#/user/123,其中 #/user/123 是 hash 部分。
    3. 核心机制:通过监听 hashchange 事件(如用户手动修改 hash 或通过 location.hash 操作),前端解析 hash 内容并触发路由切换。
  2. history 路由(HTML5 History API)

    1. 原理:借助 HTML5 提供的 History API(如 pushState()replaceState())修改 URL 的路径部分(不含 #),模拟传统服务器路由的 URL 结构。
    2. 示例https://example.com/user/123,路径 /user/123 由前端控制。
    3. 核心机制:前端通过 API 修改 URL 后,需配合服务器配置(如 Nginx 重定向),确保刷新时服务器返回正确页面,避免 404 错误。

    二、详细对比表
    维度 hash 路由 history 路由
    URL 结构 包含 #,如 example.com/#/path 无 #,如 example.com/path
    服务器请求 hash 变化不触发服务器请求,仅前端处理 路径变化可能触发服务器请求(需后端支持)
    浏览器历史记录 修改 hash 不会新增历史记录,仅更新当前 URL 每次 pushState() 会新增历史记录,支持前进 / 后退
    SEO 支持 搜索引擎难以解析 hash 内容,SEO 效果差 URL 更符合传统格式,SEO 友好性更好
    服务器配置 无需额外配置,兼容性强 需要服务器配置重定向(如将所有路径指向首页)
    参数传递 可通过 hash 传递简单参数(如 #/user?id=1 可通过路径参数(/user/1)或 state 对象传递复杂数据
    兼容性 支持所有主流浏览器(包括 IE8+) 依赖 HTML5 History API,IE9 及以下不支持
    三、典型应用场景

    hash 路由适用场景

    • 简单单页应用:如移动端 H5 页面、无需 SEO 的内部管理系统,无需后端配置即可快速实现路由。
    • 兼容性要求高:需支持低版本浏览器(如 IE8)时,hash 是更稳妥的选择。
    • 纯前端交互:如图片查看器的锚点定位、表单步骤切换,无需服务器参与。

    history 路由适用场景

    • 需要 SEO 的网站:如博客、新闻类网站,清晰的 URL 结构有利于搜索引擎抓取。
    • 复杂应用架构:路径参数可直接映射资源(如 https://api.com/posts/1),便于前后端路由统一。
    • 模拟传统网站体验:用户可直接复制路径、通过书签收藏页面,刷新后仍能正确显示内容(需后端配合)。

    hash 路由:简单、兼容性强,适合纯前端场景,但 URL 不够美观且 SEO 能力弱。

    history 路由:URL 更符合直觉,支持 SEO 和复杂交互,但需要后端配置支持,且兼容性略差。

    四、后端支持与配置示例

    history 路由的服务器配置(以 Nginx 为例)
    当用户刷新 example.com/path 时,服务器需将请求重定向到首页(如 index.html),由前端路由处理路径:

    server {
        listen 80;
        server_name example.com;
        root /path/to/your/app;
        
        location / {
            try_files $uri $uri/ /index.html;  # 所有路径重定向到首页
        }
    }
    

    前端路由库的选择

    • hash 路由:Vue Router 可通过 mode: 'hash' 启用(默认模式),React Router 需使用 HashRouter
    • history 路由:Vue Router 需设置 mode: 'history',React Router 使用 BrowserRouter,同时确保后端已配置重定向。

    当 JWT 过期时,浏览器会发起一个新的请求来刷新 token。如果此时多个请求同时发起,那么每个请求都会触发一次 token 刷新,就会导致多次请求刷新的问题,你该怎么进行解决呢

    解决措施:维护一个 task queue 和 isRefresh 的标识,如果处于正在刷新 token 的阶段,之后发出的多个请求保存在一个队列中,等 token 刷新完后再请求(避免多次刷新 token)

    JWT 的原理是什么

    JWT 的原理可以参考阮一峰的这篇文章:JWT 原理,需要核心掌握的就是 JWT 的结构以及 Base64Url 算法(后面会提到)

    JWT 分为三个部分:

    • Header(头部):以 JSON 对象的形式描述 JWT 的元数据,包括签名的算法以及 token 的类型。最后,将上述的 JSON 通过 Base64Url 算法转成字符串
    • Payload(负载):用来存放实际需要传递的数据,也通过 Base64Url 算法转为字符串
    • Signature(签名):对前两部分的签名,防止数据被篡改

    Base64Url 算法:

    JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+/=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_ 。这就是 Base64URL 算法

    一、JWT 核心原理
    1. 结构与组成

      • Header:声明使用的签名算法(如 HS256、RS256)和类型(固定为 JWT)
        {
          "alg": "HS256",
          "typ": "JWT"
        }
        
      • Payload:存储声明(Claims),包括注册声明(如issexp)和自定义声明(如userIdroles
        {
          "sub": "1234567890",
          "name": "John Doe",
          "iat": 1516239022,
          "exp": 1516242622
        }
        
      • Signature:对 Header 和 Payload 的哈希签名,确保数据未被篡改
        HMACSHA256(
          base64UrlEncode(header) + "." +
          base64UrlEncode(payload),
          secret
        )
        
    2. 工作流程

      • 用户登录:客户端发送凭证(用户名 / 密码)到服务器
      • 生成 Token:服务器验证通过后,用私钥生成 JWT 并返回
      • 存储 Token:客户端将 Token 存储在 localStorage 或 Cookie 中
      • 携带 Token:每次请求时在Authorization头中添加Bearer {token}
      • 验证 Token:服务器用公钥或相同私钥验证签名和有效期
    二、JWT 鉴权的实现步骤
    1. 生成 JWT(服务端)
    const jwt = require('jsonwebtoken');
    
    // 登录接口
    app.post('/api/login', async (req, res) => {
      // 验证用户凭证
      const user = await User.findOne({ username: req.body.username });
      if (!user || !comparePassword(req.body.password, user.password)) {
        return res.status(401).json({ message: '认证失败' });
      }
    
      // 生成Token(包含用户ID和角色)
      const token = jwt.sign(
        { 
          userId: user._id,
          roles: user.roles,
          username: user.username
        },
        process.env.JWT_SECRET, // 私钥
        { 
          expiresIn: '1h', // 有效期
          algorithm: 'HS256' // 签名算法
        }
      );
    
      res.json({ token });
    });
    
    2. 前端存储与请求携带
    // 登录成功后存储Token
    localStorage.setItem('auth_token', token);
    
    // 配置axios拦截器自动携带Token
    axios.interceptors.request.use(config => {
      const token = localStorage.getItem('auth_token');
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    });
    
    3. 验证 JWT(服务端)
    const expressJwt = require('express-jwt');
    
    // 验证中间件
    const authMiddleware = expressJwt({
      secret: process.env.JWT_SECRET,
      algorithms: ['HS256'],
      credentialsRequired: true // 要求必须有Token
    }).unless({
      path: [
        '/api/login', // 白名单:登录接口无需验证
        '/api/register'
      ]
    });
    
    // 应用中间件
    app.use(authMiddleware);
    
    // 受保护的路由
    app.get('/api/userInfo', (req, res) => {
      // 验证通过后,用户信息会被注入到req.user中
      res.json({
        message: '用户信息',
        user: req.user
      });
    });
    
    4. 处理 Token 过期与刷新
    // 错误处理中间件
    app.use((err, req, res, next) => {
      if (err.name === 'UnauthorizedError') {
        // Token过期或无效
        if (err.code === 'token_expired') {
          return res.status(401).json({
            message: 'Token已过期,请刷新或重新登录',
            code: 40101
          });
        }
        return res.status(401).json({ message: '无效Token' });
      }
      next(err);
    });
    
    // Token刷新接口
    app.post('/api/refreshToken', async (req, res) => {
      const oldToken = req.headers.authorization?.split(' ')[1];
      if (!oldToken) {
        return res.status(400).json({ message: '缺少Token' });
      }
    
      try {
        // 验证旧Token签名(忽略过期时间)
        const decoded = jwt.verify(oldToken, process.env.JWT_SECRET, { ignoreExpiration: true });
        
        // 生成新Token
        const newToken = jwt.sign(
          { userId: decoded.userId, roles: decoded.roles },
          process.env.JWT_SECRET,
          { expiresIn: '1h' }
        );
        
        res.json({ token: newToken });
      } catch (err) {
        res.status(401).json({ message: '刷新失败,请重新登录' });
      }
    });
    
    三、安全增强措施
    1. 使用 HTTPS:防止 Token 在传输过程中被截获
    2. 设置合理的过期时间
      • 短期访问 Token(如 15 分钟)+ 长期刷新 Token(如 7 天)
      • 敏感操作(如支付)使用一次性 Token
    3. 黑名单机制
      // 登出时将Token加入黑名单(Redis实现)
      app.post('/api/logout', async (req, res) => {
        const token = req.headers.authorization?.split(' ')[1];
        if (token) {
          const decoded = jwt.decode(token);
          const expireTime = decoded.exp - Math.floor(Date.now() / 1000);
          await redisClient.set(`revoked:${token}`, 'true', 'EX', expireTime);
        }
        res.json({ message: '登出成功' });
      });
      
      // 验证中间件中检查黑名单
      authMiddleware.unless({
        path: ['/api/login', '/api/refreshToken']
      });
      
      app.use(async (req, res, next) => {
        if (req.user) {
          const token = req.headers.authorization?.split(' ')[1];
          const isRevoked = await redisClient.exists(`revoked:${token}`);
          if (isRevoked) {
            return res.status(401).json({ message: 'Token已失效' });
          }
        }
        next();
      });
      
    4. CSRF 防护
      • 不使用 Cookie 存储 Token
      • 敏感操作添加验证码或二次验证
    5. 非对称加密(RS256)
      // 生成Token(服务端)
      const privateKey = fs.readFileSync('private.key');
      const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
      
      // 验证Token(客户端/其他服务)
      const publicKey = fs.readFileSync('public.key');
      jwt.verify(token, publicKey, { algorithm: 'RS256' });
      

    你还知道其他什么登录方式么,能不能说一下这些方式和 JWT 方案的区别

    Session

    Session 是在用户登录成功后,由服务器自动生成的唯一标识

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    浏览器        ⇄        服务器

    ----------           -------------------------

    输入账号密码         验证成功 → 生成 Session

         ↓                        ↓

    发送 POST /login    Set-Cookie: sessionid=xxx

         ↓                        ↓

    保存 Cookie           服务器内存保存 sessionid → userInfo

         

    后续请求自动带 Cookie

         

    服务器根据 sessionid 查找用户信息

    也就是说,当浏览器接收到服务器返回的 sessionID 之后,会先将此信息存入 cookie,同时 cookie 记录此 sessionID 属于哪个域名;在后续请求的时候,cookie 信息会被自动发送给服务端,服务端也会从 cookie 中获取 sessionID,再根据 ID 寻找响应的 session 信息

    适用场景

    适合于同源 Web 应用,且无需跨域

    Session 和 Token 的区别

    对比项 SessionToken                                                    Token

    存储位置 存储在服务端(如内存或 Redis) 存储在客户端(如 Cookie 或 LocalStorage)
    状态管理 有状态(需要服务器存储用户会话) 无状态(不需要服务器存储用户信息)
    跨域支持 不支持跨域(需要特殊配置或共享存储) 支持跨域(特别是 JWT)
    安全性 相对安全,易于控制(服务器存储) 需要注意防止 XSS 攻击,存储需小心

    详细解析你的JWT认证和权限控制系统

    第一步:后端 JWT 认证实现

    1. JWT 配置 (backend_wz/jwt_config/index.js)
    jwtSecretKey:'gbms',
    

            定义了 JWT 的密钥,用于 token 的加密和解密

    1. 登录认证逻辑 (backend_wz/router_handler/login.js)
    // 第四步 生成返回给前端的token
    // 剔除加密后的密码,头像,创建时间,更新时间
    const user = {
        ...results[0],
        password: '',
        imageUrl: '',
        create_time: '',
        update_time: '',
    }
    // 设置token的有效时长 有效期为7个小时
    const tokenStr = jwt.sign(user, jwtconfig.jwtSecretKey, {
        expiresIn: '7h'
    })
    res.send({
        results: results[0],
        status: 0,
        message: '登录成功',
        token: 'Bearer ' + tokenStr,
    })
    

    核心功能:

    • 验证用户账号密码
    • 生成包含用户信息的 JWT token
    • 设置 7 小时的过期时间
    • 返回 "Bearer" + token 格式

    1. 基于角色的路由配置
    const superAdminRouter = [{
            name: 'set',
            path: '/set',
            component: 'set/index'
        },
        {
            name: 'user',
            path: '/user',
            component: 'user/index'
        }, {
            name: 'login_log',
            path: '/login_log',
            component: 'login_log/index'
        }, {
            name: 'operation_log',
            path: '/operation_log',
            component: 'operation_log/index'
        }, {
            name: 'product',
            path: '/product',
            component: 'product/index'
        }, {
            name: 'audit',
            path: '/audit',
            component: 'product/audit'
        }, {
            name: 'outbound',
            path: '/outbound',
            component: 'product/outbound'
        },
        {
            name: 'statistics',
            path: '/statistics',
            component: 'statistics/index'
        }
    ]
    

    第二步:前端 axios 拦截器实现

    1. 请求拦截器添加 Token
    instance.interceptors.request.use(function (config) {
        // 添加请求之前的逻辑 - 从localStorage获取token并添加到请求头
        const token = localStorage.getItem('token')
        if (token) {
            config.headers.Authorization = token // 后端返回的已经包含 "Bearer " 前缀
        }
        return config;
    }, function (error) {
    
    1. 响应拦截器处理 Token 过期
                case 401:
                    ElMessage.error('未授权,请登录')
                    // token过期或无效,清除本地存储并跳转到登录页
                    localStorage.clear()
                    router.push('/')
                    break
    

    第三步:动态路由加载实现

    1. 菜单 Store 管理 (后台前端_wz/src/stores/menu.ts)
            function compilerMenu (arr:any) {
                if(!arr) return
                menuData.value = arr
                arr.forEach((item:any)=>{
                    let rts = {
                        name:item.name,
                        path:item.path,
                        component:item.component
                    }
                    if(item.children && item.children.length){
                        compilerMenu(item.children)
                    }
                    if(!item.children){
                        let path = loadComponent(item.component)
                        // console.log(path)
                        rts.component = path;
                        router.addRoute('menu',rts)
                    }
    
                    function loadComponent(url:string){
                        let Module = import.meta.glob("@/views/**/*.vue")
                        return Module[`/src/views/${url}.vue`]
    

    关键特性:

    • 动态导入组件:使用 import.meta.glob 实现组件的动态加载
    • 路由动态添加:使用 router.addRoute 将权限路由添加到路由表
    • 递归处理:支持嵌套路由的处理
    1. 登录后获取用户权限路由
        userStore.userInfo(account)
        const routerList = await returnMenuList(id)
        // console.log(routerList)
        menuStore.setRouter(routerList)
        router.push('/menu') // 登录成功后跳转至菜单页面
    

    ️ 第四步:路由守卫权限控制


    路由守卫实现 (后台前端_wz/src/router/index.ts)

    // 路由守卫 - 基于token的权限控制
    router.beforeEach((to, from, next) => {
      const token = localStorage.getItem('token')
      
      // 白名单路由(不需要token验证的页面)
      const whiteList = ['/']
      
      if (token) {
        // 已登录状态
        if (to.path === '/') {
          // 如果已登录,访问登录页则重定向到菜单页
          next('/menu')
        } else {
          // 正常访问其他页面
          next()
        }
      } else {
        // 未登录状态
        if (whiteList.indexOf(to.path) !== -1) {
          // 访问白名单页面,放行
          next()
        } else {
          // 访问需要权限的页面,重定向到登录页
          ElMessage.error('请先登录')
          next('/')
        }
      }
    })
    

    系统架构总结
    你的权限控制系统解决了以下问题:

    • 身份认证:基于 JWT 的无状态认证
    • 权限管理:基于角色的权限控制(RBAC)
    • 路由保护:防止未授权访问
    • 动态菜单:根据权限动态生成菜单
    • 按钮级控制:细粒度的功能权限控制
    • 安全性:token 过期处理、错误处理等

    按钮级权限控制

    通过对你原始代码的详细分析,我发现你确实已经实现了多层次的按钮级权限控制:

     1. 菜单级权限控制

    12:27:后台前端_wz/src/views/menu/index.vue

              
                
                用户模块
              
              
                
                登录日志
              
              
                
                操作日志
              
              
                
    

     2. 基于职位的按钮权限控制

    A. 审核功能权限(产品审核)

    18:25:后台前端_wz/src/views/product/components/notes.vue