【Vue】权限控制

news2024/11/14 21:15:48

权限管理

分类:

  1. 页面权限
  2. 功能(按钮)权限
  3. 接口权限

vue3-element-admin 的实现方案

一般我们在业务中将 路由可以分为两种,constantRoutesasyncRoutes

  • constantRoutes: 代表那些不需要动态判断权限的路由,如登录页、404(或者不存在的路由)、首页、数据大屏等通用页面。

  • asyncRoutes: 代表那些需求动态判断权限并通过 addRoutes 动态添加的页面。

后台管理系统中的路由都具有不同的访问权限,侧边菜单栏也是同理,需要根据权限,异步生成。

整体步骤都十分类似:

我们在登录后获取 token ,将其存入 localStorage 中,用来“象征用户身份”。

登录表单提交业务实现:

/** 登录表单提交 */
function handleLoginSubmit() {
  loginFormRef.value?.validate((valid: boolean) => {
    if (valid) {
      loading.value = true;
      userStore
        .login(loginData.value)
        .then(() => {
          const { path, queryParams } = parseRedirect();
          router.push({ path: path, query: queryParams });
        })
        .catch(() => {
          getCaptcha();
        })
        .finally(() => {
          loading.value = false;
        });
    }
  });
}

调用登录接口,存储 token 到localStorage 中。

/**
 * 登录
 * @param {LoginData}
 * @returns
 */
function login(loginData: LoginData) {
  return new Promise<void>((resolve, reject) => {
    AuthAPI.login(loginData)
      .then((data) => {
        const { tokenType, accessToken } = data;
        localStorage.setItem(TOKEN_KEY, tokenType + " " + accessToken); // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
        resolve();
      })
      .catch((error) => {
        reject(error);
      });
  });
}

登录接口。

class AuthAPI {
  /** 登录 接口*/
  static login(data: LoginData) {
    const formData = new FormData();
    formData.append("username", data.username);
    formData.append("password", data.password);
    formData.append("captchaKey", data.captchaKey);
    formData.append("captchaCode", data.captchaCode);
    return request<any, LoginResult>({
      url: "/api/v1/auth/login",
      method: "post",
      data: formData,
      headers: {
        "Content-Type": "multipart/form-data",
      },
    });
  }
  // ...
}

获取验证码。

/** 获取验证码 */
function getCaptcha() {
  AuthAPI.getCaptcha().then((data) => {
    loginData.value.captchaKey = data.captchaKey;
    captchaBase64.value = data.captchaBase64;
  });
}

通过上述过程,我们已经成功获取 token 并存储在了 localStorage 中。

之后我们就可以根据 token “用户身份” 来进行权限控制了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

token 可以获取用户角色,而不同角色对应不同权限的路由,然后通过 router.addRoutes 动态挂载路由。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

调用获取路由接口,获取路由表,进行动态路由处理,将其与常量路由进行拼接,得到总路由。

/**
 * 生成动态路由
 */
function generateRoutes() {
  return new Promise<RouteRecordRaw[]>((resolve, reject) => {
    MenuAPI.getRoutes()
      .then((data) => {
        const dynamicRoutes = transformRoutes(data);
        routes.value = constantRoutes.concat(dynamicRoutes);
        resolve(dynamicRoutes);
      })
      .catch((error) => {
        reject(error);
      });
  });
}

这里使用 mock 数据:

export default defineMock([
  {
    url: "menus/routes",
    method: ["GET"],
    body: {
      code: "00000",
      data: [
        {
          path: "/doc",
          component: "Layout",
          redirect: "https://juejin.cn/post/7228990409909108793",
          name: "/doc",
          meta: {
            title: "平台文档",
            icon: "document",
            hidden: false,
            alwaysShow: false,
            params: null,
          },
          children: [
            {
              path: "internal-doc",
              component: "demo/internal-doc",
              name: "InternalDoc",
              meta: {
                title: "平台文档(内嵌)",
                icon: "document",
                hidden: false,
                alwaysShow: false,
                params: null,
              },
            },
            {
              path: "https://juejin.cn/post/7228990409909108793",
              name: "Https://juejin.cn/post/7228990409909108793",
              meta: {
                title: "平台文档(外链)",
                icon: "link",
                hidden: false,
                alwaysShow: false,
                params: null,
              },
            },
          ],
        },
        {
          path: "/multi-level",
          component: "Layout",
          name: "/multiLevel",
          meta: {
            title: "多级菜单",
            icon: "cascader",
            hidden: false,
            alwaysShow: true,
            params: null,
          },
          children: [
            {
              path: "multi-level1",
              component: "demo/multi-level/level1",
              name: "MultiLevel1",
              meta: {
                title: "菜单一级",
                icon: "",
                hidden: false,
                alwaysShow: true,
                params: null,
              },
              children: [
                {
                  path: "multi-level2",
                  component: "demo/multi-level/children/level2",
                  name: "MultiLevel2",
                  meta: {
                    title: "菜单二级",
                    icon: "",
                    hidden: false,
                    alwaysShow: false,
                    params: null,
                  },
                  children: [
                    {
                      path: "multi-level3-1",
                      component: "demo/multi-level/children/children/level3-1",
                      name: "MultiLevel31",
                      meta: {
                        title: "菜单三级-1",
                        icon: "",
                        hidden: false,
                        keepAlive: true,
                        alwaysShow: false,
                        params: null,
                      },
                    },
                    {
                      path: "multi-level3-2",
                      component: "demo/multi-level/children/children/level3-2",
                      name: "MultiLevel32",
                      meta: {
                        title: "菜单三级-2",
                        icon: "",
                        hidden: false,
                        keepAlive: true,
                        alwaysShow: false,
                        params: null,
                      },
                    },
                  ],
                },
              ],
            },
          ],
        },
      ],
      msg: "一切ok",
    },
  },
]);

转换路由数据为组件(根据实际业务进行弹性操作)。

/**
 * 转换路由数据为组件
 */
const transformRoutes = (routes: RouteVO[]) => {
  const asyncRoutes: RouteRecordRaw[] = [];
  routes.forEach((route) => {
    const tmpRoute = { ...route } as RouteRecordRaw;
    // 顶级目录,替换为 Layout 组件
    if (tmpRoute.component?.toString() == "Layout") {
      tmpRoute.component = Layout;
    } else {
      // 其他菜单,根据组件路径动态加载组件
      const component = modules[`../../views/${tmpRoute.component}.vue`];
      if (component) {
        tmpRoute.component = component;
      } else {
        tmpRoute.component = modules[`../../views/error-page/404.vue`];
      }
    }

    if (tmpRoute.children) {
      tmpRoute.children = transformRoutes(route.children);
    }

    asyncRoutes.push(tmpRoute);
  });

  return asyncRoutes;
};

vue-element-admin 的实现方案

当然他们只是实现的写法不同,大致的思路还是相同的。

为了便于理解主要思路和提取关键代码,下面使用尚硅谷硅谷甄选项目的实现方案代码讲解(和vue-element-admin的大差不差)。

先看下用户信息:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在用户 store 中过滤用户的异步路由(用路由的名字进行过滤区分-所以要保证名字的唯一):

//用于过滤当前用户需要展示的异步路由
// asyncRoute 所有异步路由 routes 用户拥有权限的路由
function filterAsyncRoute(asnycRoute: any, routes: any) {
    return asnycRoute.filter((item: any) => {
        if (routes.includes(item.name)) {
            if (item.children && item.children.length > 0) {
                // 新的 item.children 也需要进行同样的过滤操作 
                item.children = filterAsyncRoute(item.children, routes);
            }
            return true;
        }
    })
}

获取用户个人信息后再 store 中操作路由:

//计算当前用户需要展示的异步路由
let userAsyncRoute = filterAsyncRoute(cloneDeep(asnycRoute), result.data.routes);
//菜单需要的数路由数据
this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute];
//目前路由器管理的只有常量路由,再异步获取路由路由信息后,异步路由、任意路由动态追加到路由管理中
[...userAsyncRoute, anyRoute].forEach((route: any) => {
    router.addRoute(route);
});

路由表:

//对外暴露配置路由(常量路由):全部用户都可以访问到的路由
export const constantRoute = [
    {
        //登录
        path: '/login',
        component: () => import('@/views/login/index.vue'),
        name: 'login',
        meta: {
            title: '登录',//菜单标题
            hidden: true,//代表路由标题在菜单中是否隐藏  true:隐藏 false:不隐藏
            icon: "Promotion",//菜单文字左侧的图标,支持element-plus全部图标
        }
    }
    ,
    {
        //登录成功以后展示数据的路由
        path: '/',
        component: () => import('@/layout/index.vue'),
        name: 'layout',
        meta: {
            title: '',
            hidden: false,
            icon: ''
        },
        redirect: '/home',
        children: [
            {
                path: '/home',
                component: () => import('@/views/home/index.vue'),
                meta: {
                    title: '首页',
                    hidden: false,
                    icon: 'HomeFilled'
                }
            }
        ]
    },
    {
        //404
        path: '/404',
        component: () => import('@/views/404/index.vue'),
        name: '404',
        meta: {
            title: '404',
            hidden: true,
            icon: 'DocumentDelete'
        }
    },
    {
        path: '/screen',
        component: () => import('@/views/screen/index.vue'),
        name: 'Screen',
        meta: {
            hidden: false,
            title: '数据大屏',
            icon: 'Platform'
        }
    }]

//异步路由
export const asnycRoute = [
    {
        path: '/acl',
        component: () => import('@/layout/index.vue'),
        name: 'Acl',
        meta: {
            title: '权限管理',
            icon: 'Lock'
        },
        redirect: '/acl/user',
        children: [
            {
                path: '/acl/user',
                component: () => import('@/views/acl/user/index.vue'),
                name: 'User',
                meta: {
                    title: '用户管理',
                    icon: 'User'
                }
            },
            {
                path: '/acl/role',
                component: () => import('@/views/acl/role/index.vue'),
                name: 'Role',
                meta: {
                    title: '角色管理',
                    icon: 'UserFilled'
                }
            },
            {
                path: '/acl/permission',
                component: () => import('@/views/acl/permission/index.vue'),
                name: 'Permission',
                meta: {
                    title: '菜单管理',
                    icon: 'Monitor'
                }
            }
        ]
    }
    ,
    {
        path: '/product',
        component: () => import('@/layout/index.vue'),
        name: 'Product',
        meta: {
            title: '商品管理',
            icon: 'Goods',
        },
        redirect: '/product/trademark',
        children: [
            {
                path: '/product/trademark',
                component: () => import('@/views/product/trademark/index.vue'),
                name: "Trademark",
                meta: {
                    title: '品牌管理',
                    icon: 'ShoppingCartFull',
                }
            },
            {
                path: '/product/attr',
                component: () => import('@/views/product/attr/index.vue'),
                name: "Attr",
                meta: {
                    title: '属性管理',
                    icon: 'ChromeFilled',
                }
            },
            {
                path: '/product/spu',
                component: () => import('@/views/product/spu/index.vue'),
                name: "Spu",
                meta: {
                    title: 'SPU管理',
                    icon: 'Calendar',
                }
            },
            {
                path: '/product/sku',
                component: () => import('@/views/product/sku/index.vue'),
                name: "Sku",
                meta: {
                    title: 'SKU管理',
                    icon: 'Orange',
                }
            },
        ]
    }
]

//任意路由
export const anyRoute = {
    //任意路由
    path: '/:pathMatch(.*)*',
    redirect: '/404',
    name: 'Any',
    meta: {
        title: '任意路由',
        hidden: true,
        icon: 'DataLine'
    }
}

路由器对象(初始化的时候只注册了常量路由):

//创建路由器
let router = createRouter({
    //路由模式hash
    history: createWebHashHistory(),
    routes: constantRoute,
    //滚动行为
    scrollBehavior() {
        return {
            left: 0,
            top: 0
        }
    }
});

路由鉴权守卫(这里在某些业务情况下可以增加白名单,对权限进行再一次划分):

//路由鉴权:鉴权,项目当中路由能不能被的权限的设置(某一个路由什么条件下可以访问、什么条件下不可以访问)
import router from '@/router';
import setting from './setting';
import nprogress from 'nprogress';
//引入进度条样式
import "nprogress/nprogress.css";
nprogress.configure({ showSpinner: false });
//获取用户相关的小仓库内部token数据,去判断用户是否登录成功
import useUserStore from './store/modules/user';
import pinia from './store';
let userStore = useUserStore(pinia);
//全局守卫:项目当中任意路由切换都会触发的钩子
//全局前置守卫
router.beforeEach(async (to: any, from: any, next: any) => {
    document.title = `${setting.title} - ${to.meta.title}`
    //to:你将要访问那个路由
    //from:你从来个路由而来
    //next:路由的放行函数
    nprogress.start();
    //获取token,去判断用户登录、还是未登录
    let token = userStore.token;
    //获取用户名字
    let username = userStore.username;
    //用户登录判断
    if (token) {
        //登录成功,访问login,不能访问,指向首页
        if (to.path == '/login') {
            next({ path: '/' })
        } else {
            //登录成功访问其余六个路由(登录排除)
            //有用户信息
            if (username) {
                //放行
                next();
            } else {
                //如果没有用户信息,在守卫这里发请求获取到了用户信息再放行
                try {
                    //获取用户信息
                    await userStore.userInfo();
                    //放行
                    //万一:刷新的时候是异步路由,有可能获取到用户信息、异步路由还没有加载完毕,出现空白的效果
                    next({...to,replace:true})
                } catch (error) {
                    //token过期:获取不到用户信息了
                    //用户手动修改本地存储token
                    //退出登录->用户相关的数据清空
                    await userStore.userLogout();
                    next({ path: '/login', query: { redirect: to.path } })
                }
            }
        }

    } else {
        //用户未登录判断
        if (to.path == '/login') {
            next();
        } else {
            next({ path: '/login', query: { redirect: to.path } });
        }
    }
})
//全局后置守卫
router.afterEach((to: any, from: any) => {
    nprogress.done();
});

//第一个问题:任意路由切换实现进度条业务 ---nprogress
//第二个问题:路由鉴权(路由组件访问权限的设置)
//全部路由组件:登录|404|任意路由|首页|数据大屏|权限管理(三个子路由)|商品管理(四个子路由)
//用户未登录:可以访问login,其余六个路由不能访问(指向login)
//用户登录成功:不可以访问login[指向首页],其余的路由可以访问

这里用到了 next({…to,replace:true}) ,它的原理是:

router.addRoutes是同步方法,整体流程:

  1. 路由跳转,根据目标地址从router中提取route信息,由于此时还没addRouters,所以解析出来的route是个空的,不包含组件。
  2. 执行beforeEach钩子函数,然后内部会动态添加路由,但此时route已经生成了,不是说router.addRoutes后,这个route会自动更新,如果直接next(),最终渲染的就是空的。
  3. 调用next({ …to, replace: true }),会abort刚刚的跳转,然后重新走一遍上述逻辑,这时从router中提取的route信息就包含组件了,之后就和正常逻辑一样了。
    主要原因就是生成route是在执行beforeEach钩子之前。

上述解释摘自手摸手,带你用vue撸后台 系列二(登录权限篇) - 掘金 (juejin.cn)评论区

页面权限总结

页面权限:

  1. 用户登录后,服务端返回一个权限树(用树形结构呈现权限数据),然后我们去解析这个树形结构,得到我们需要的路由表(动态路由对象),本质上就是一个由路由对象为元素的数组。
  2. 然后通过 vue 中的动态路由,也就是 addRoutes,动态的添加路由。
  3. 最后根据路由去渲染多级菜单栏。

按钮权限

可以自定义一个全局指定,用于按钮权限的判断。

import pinia from '@/store';
import useUserStore from '@/store/modules/user';
let userStore =useUserStore(pinia)
export const isHasButton = (app: any) => {
    //获取对应的用户仓库
    //全局自定义指令:实现按钮的权限
    app.directive('has', {
        //代表使用这个全局自定义指令的DOM|组件挂载完毕的时候会执行一次
        mounted(el:any,options:any) {
            //自定义指令右侧的数值:如果在用户信息buttons数组当中没有
            //从DOM树上干掉
            if(!userStore.buttons.includes(options.value)){
               el.parentNode.removeChild(el);
            }
        },
    })
}

el 为该元素,options 为:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在 app.ts 中引入。

import {isHasButton} from '@/directive/has.ts';
isHasButton(app);

使用:

v-has="`btn.Trademark.add`"

按钮(功能)权限总结

服务端返回的权限树中包含了指定页面下指定按钮的数据,可以通过 v-if 或者 disable 来控制按钮权限。

接口权限

配合功能权限,一般由服务端进行处理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1964044.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机器学习算法——常规算法,在同的业务场景也需要使用不同的算法(二)

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

新兴材料中载流子迁移率的霍尔效应测量

这篇文章是发表在《自然电子学》&#xff08;Nature Electronics&#xff09;2024年7月刊上的一篇评论文章&#xff0c;标题为“Reporting Hall effect measurements of charge carrier mobility in emerging materials”&#xff0c;作者是Vladimir Bruevich和Vitaly Podzorov…

数据仓库及数仓架构概述

往期推荐 大数据HBase图文简介-CSDN博客 数仓常见名词解析和名词之间的关系-CSDN博客 目录 0. 前言 0.1 浅谈维度建模 0.2 数据分析模型 1. 何为数据仓库 1.1 为什么不直接用业务平台的数据而要建设数仓&#xff1f; 1.2 数据仓库特征 1.3 数据仓库和数据库区别 1.4 以…

LLM 各种技巧| Prompt Engineering 大总结|指南

LLM 各种技巧| Prompt Engineering 大总结|指南 截止至今&#xff0c;关于LLM 的优化与技巧层出不穷&#xff0c;几乎每个月都有新的技术和方法论被提出&#xff0c;因此本篇主要是要介绍在各种不同情境下&#xff0c;LLM 的各种Prompt Engineering 技巧&#xff0c;每篇都有附…

数据结构经典测试题5

1. int main() { char arr[2][4]; strcpy (arr[0],"you"); strcpy (arr[1],"me"); arr[0][3]&; printf("%s \n",arr); return 0; }上述代码输出结果是什么呢&#xff1f; A: you&me B: you C: me D: err 答案为A 因为arr是一个2行4列…

使用AWS CDK构建生产级VPC基础设施指南

简介 虽然有很多关于AWS的信息&#xff0c;但实际如何将这些服务投入生产使用&#xff0c;还是需要自己思考。本文将介绍我们是如何思考并实施这些工作的。 目前有很多AWS环境构建的方法&#xff0c;但在这里我们将使用AWS CDK进行说明。 ※ 本文不会涉及CDK的基本操作方法或…

Java每日一练,技术成长不间断

目录 题目1.下列关于继承的哪项叙述是正确的&#xff1f;2.Java的跨平台特性是指它的源代码可以在多个平台运行。&#xff08;&#xff09;3.以下 _____ 不是 Object 类的方法4.以下代码&#xff1a;5.下面哪个流类不属于面向字符的流&#xff08;&#xff09;总结 题目 选自牛…

AI系统测试方法|变异测试的流程及优化技术

变异测试是AI系统测试中较为常见的一种测试方法。通过引入人工制造的缺陷来评估系统的健壮性。在AI系统测试实践中&#xff0c;变异测试解决了对测试套件进行有效性和充分性评估的难题。本文将重点探讨变异测试在AI系统测试中的执行流程&#xff0c;呈现一个完整的测试框架&…

Apple intelligence 正式开启测试!第一波文本工具测试体验来啦!

Apple Intelligence 开启测试了&#xff01;苹果带着它的人工智能走进现实了&#xff01; 但是&#xff0c;坏消息是&#xff0c;目前Apple Intelligence仅支持美国地区使用美英语言的开发者账户使用&#xff0c;国行的小伙伴不要急着更新了。 本次测试内容为WWDC2024预告中公布…

PTA 6-2 多项式求值

6-2 多项式求值&#xff08;15分&#xff09; 本题要求实现一个函数&#xff0c;计算阶数为n&#xff0c;系数为a[0] ... a[n]的多项式 在x点的值。 函数接口定义&#xff1a; double f( int n, double a[], double x ); 其中n是多项式的阶数&#xff0c;a[]中存储系数&…

【Qt】QWidget的windowOpacity属性 cursor属性 font属性

一.windowOpacity属性 1.概念&#xff1a; windowOpacity属性是Qt中QWindow类的一个属性。它用于设置窗口的不透明度&#xff08;透明度&#xff09;。 窗口的不透明度值范围是0.0到1.0之间&#xff0c;其中0.0表示完全透明&#xff0c;1.0表示完全不透明。默认情况下&#…

Python科研数据可视化教程

原文链接&#xff1a;Python科研数据可视化教程https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247609609&idx4&sn8629ee10544f43b46993694d929843d0&chksmfa826ceecdf5e5f8ca4fbc72104c3488d5c53c0735f41f393c9a494aceddf2b93c8eb5622813&token214…

部署一个nodejs项目+配置server

1.jdk环境 tomcat服务器需要jdk环境 版本对应 ​ tomcat9>jdk1.8 ​ tomcat10>jdk17 配置系统变量JAVA_HOME 2.nginx平滑升级&#xff0c;不停服务升级nginx服务器&#xff0c;1.26.1升级到1.27.0 3.负载均衡&#xff0c;使用nginx管理后端服务器&#xff0c;…

基础复习(IO流)

1.File类 创建对象 File f1 new File("D:/resource/ab.txt"); File f2 new File("D:\\resource\\abc.txt"); 基础方法 创建与删除方法 1、public boolean createNewFile()&#xff1a;创建一个新文件&#xff08;文件内容为空&#xff09;&#xff0c;…

区块链技术在智能城市中的创新应用探索

随着全球城市化进程的加速和信息技术的快速发展&#xff0c;智能城市成为了未来城市发展的重要方向。在智能城市建设中&#xff0c;区块链技术作为一种去中心化、安全和透明的分布式账本技术&#xff0c;正逐渐展现出其在优化城市管理、提升公共服务和增强城市安全性方面的潜力…

1.6 树和二叉树

1.树的基本概念 2.二叉树的概念和性质 2.1.二叉树性质 1&#xff09;结点个数 2&#xff09;第i层&#xff0c;最多结点个数 3&#xff09;者深度为k,前k层最多结点个数 4&#xff09;叶子结点个数 2.2.完全二叉树性质 1&#xff09;结点个数 2&#xff09;第i层最多节…

云计算实训18——基于域名配置虚拟主机、基于ip配置虚拟主机、基于端口配置虚拟主机、配置samba、部署nfs服务器

一、配置文件的结构 1.首先查看配置文件 [rootstatic-server ~]# vim /usr/local/nginx/conf/nginx.conf 使用grep指令查看配置文件&#xff0c;同时不看空行不看注释 [rootstatic-server ~]# grep -Ev "#|^$" /usr/local/nginx/conf/nginx.conf 2.备份文件 将原有…

ComfyUI插件:ComfyUI Impact 节点(四)

前言&#xff1a; 学习ComfyUI是一场持久战&#xff0c;而 ComfyUI Impact 是一个庞大的模块节点库&#xff0c;内置许多非常实用且强大的功能节点 &#xff0c;例如检测器、细节强化器、预览桥、通配符、Hook、图片发送器、图片接收器等等。通过这些节点的组合运用&#xff0…

mediawiki 启用 Minerva 皮肤后报错 哎呀!您在$wgDefaultSkin定义的wiki默认皮肤minervaneue不可用。

嗨喽大家好啊我是 kx 这是个常见的问题&#xff0c;废话不多说直接上解决方法 Minerva 皮肤在他的官网有说明怎么办 连接放到下面&#xff1a; https://www.mediawiki.org/wiki/Skin:Minerva_Neue 懒得看的话我把官网的话复制下来了&#xff0c;直接看就行了&#xff1a; 安…

JAVA通过debezium实时采集mysql数据

前期准备 需要提前安装mysql并且开启binlog,需要准备kafka和zookeeper环境 示例采用debezium1.9.0版本 Maven配置 <version.debezium>1.9.0.Final</version.debezium> <dependency> <groupId>io.debezium</groupId> <artifactId>debe…