springboot集成shiro和前后端分离配置

一,springboot集成shiro

1,导入依赖

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.4.0</version>
        </dependency>

2,Realm

shiro以来这个来进行认证和授权

package com.chen.admin.Realm;

import com.alibaba.fastjson.JSON;
import com.chen.admin.dto.UserDto;
import com.chen.admin.entity.Role;
import com.chen.admin.entity.User;
import com.chen.admin.service.RoleService;
import com.chen.admin.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import java.util.*;

/**
 * @Author @Chenxc
 * @Date 24-7-2 10:13
 */
@Component
public class UserPasswordRealm extends AuthorizingRealm {

    @Autowired
    private RoleService roleService;

    @Autowired
    private UserService userService;

    @Autowired
    private PasswordEncoder encoder;

   // @Autowired
    //private StringRedisTemplate stringRedisTemplate;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        UserDto user = (UserDto) principalCollection.getPrimaryPrincipal();

        //使用redis存放用户角色和权限
//        Object o = stringRedisTemplate.opsForValue().get("user:" + user.getId());
//        if(null == o){
//            List<Role> roleList = roleService.getRoleByUserId(user.getId());
//            if(null != roleList){
//                Set<String> roles = new HashSet<>();
//                Set<String> permissions = new HashSet<>();
//                for (Role role : roleList) {
//                    roles.add(role.getName());
//                    List<String> permission = roleService.getPermissionByRoleId(role.getId());
//                    if(null != permission){
//                        permissions.addAll(permission);
//                    }
//                }
//                user.setRoleList(roles);
//                user.setPermissions(permissions);
//            }
//            stringRedisTemplate.opsForValue().set("user:"+user.getId(), JSON.toJSONString(user));
//        }else{
//            String json = (String)o;
//            user = JSON.parseObject(json,UserDto.class);
//        }
//        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//        simpleAuthorizationInfo.addRoles(user.getRoleList());
//        simpleAuthorizationInfo.addStringPermissions(user.getPermissions());

	 //本地存放用户角色和权限
        List<Role> roleList = roleService.getRoleByUserId(user.getId());
        if(null != roleList){
            Set<String> roles = new HashSet<>();
            Set<String> permissions = new HashSet<>();
            for (Role role : roleList) {
                roles.add(role.getName());
                List<String> permission = roleService.getPermissionByRoleId(role.getId());
                if (null != permission) {
                    permissions.addAll(permission);
                }
            }
            user.setRoleList(roles);
            user.setPermissions(permissions);
        }
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRoles(user.getRoleList());
        simpleAuthorizationInfo.addStringPermissions(user.getPermissions());
        return simpleAuthorizationInfo;
    }


    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();
        char[] password1 = token.getPassword();

        if(null == username || null  == password1){
            throw new AuthenticationException("用户名或密码错误");
        }

        String password = new String(password1);
        User user = userService.getUserByUsername(username);
        if(null == user){
            throw new AuthenticationException("用户名或密码错误");
        }
        boolean matches = encoder.matches(password, user.getPassword());
        if (!matches) {
            throw new AuthenticationException("用户名或密码错误");
        }

        if(user.getEnable().equals("0")){
            throw new DisabledAccountException("用户已禁用");
        }

        UserDto dto = new UserDto();
        BeanUtils.copyProperties(user,dto);
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(dto, password, getName());
        return authenticationInfo;
    }
}

3,shiro配置

package com.chen.admin.config;


import com.chen.admin.dao.RedisSessionDao;
import com.chen.admin.filter.ShiroFormAuthenticationFilter;
import com.chen.admin.system.Constant;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @Author @Chenxc
 * @Date 24-7-2 10:10
 */
@Configuration
public class ShiroConfig {

    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 设置为true则会在代理对象的方法执行过程中进行权限校验
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    //上面的两个开启注解的支持,如果没有这两个方法,doGetAuthorizationInfo不会执行,加了@RequiresPermissions注解后会包找不到url


    //session保存在内存
    @Bean
    public DefaultWebSessionManager defaultWebSessionManager(){
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setGlobalSessionTimeout(Constant.expireTime * 1000);
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        //修改Cookie中的SessionId的key,默认为JSESSIONID,自定义名称
        sessionManager.setSessionIdCookie(new SimpleCookie(Constant.SHIRO_COOKIE_ID));
        return sessionManager;
    }


    //session保存在redis
    @Bean
    public DefaultWebSessionManager defaultWebSessionManager(RedisSessionDao redisSessionDao){
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setGlobalSessionTimeout(Constant.expireTime * 1000);
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionDAO(redisSessionDao);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        //修改Cookie中的SessionId的key,默认为JSESSIONID,自定义名称
        sessionManager.setSessionIdCookie(new SimpleCookie(Constant.SHIRO_COOKIE_ID));
        return sessionManager;
    }

//上面的session存放选一个即可

    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(DefaultWebSessionManager defaultWebSessionManager,Realm realm){
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 取消Cookie中的RememberMe参数
        manager.setRememberMeManager(null);
        manager.setRealm(realm);
        manager.setSessionManager(defaultWebSessionManager);
        return manager;
    }



    @Bean
    public ShiroFilterFactoryBean shiroFilterChainDefinition(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(defaultWebSecurityManager);
        factoryBean.setLoginUrl("/login");
        //factoryBean.setSuccessUrl("/loginSuccess");
        Map<String, String> map = new LinkedHashMap<>();
        //map.put("/login","anon");
        map.put("/static/**","anon");
        map.put("/ws/**","authc");
        map.put("/**", "authc");

        // 配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
//        shiroFilterFactoryBean.setLoginUrl("/login/unauth");
        LinkedHashMap<String, Filter> filtsMap = new LinkedHashMap<>();
        // 这里使用自定义的filter
        filtsMap.put("authc", new ShiroFormAuthenticationFilter());
        factoryBean.setFilters(filtsMap);
        factoryBean.setFilterChainDefinitionMap(map);
        return factoryBean;
    }

}

如果上面使用redis还需要集成 AbstractSessionDAO 来读取redis中的数据来认证和授权

RedisSessionDao:

package com.chen.admin.dao;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.chen.admin.system.Constant;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.JSONPObject;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SimpleSession;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collection;
import java.util.concurrent.TimeUnit;

/**shiro集成redis
 * @author @Chenxc
 * @date 2024/7/3 0:46
 **/

@Component
public class RedisSessionDao extends AbstractSessionDAO {


    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    protected Serializable doCreate(Session session) {
        Serializable serializable = this.generateSessionId(session);
        this.assignSessionId(session,serializable);
        redisTemplate.opsForValue().set(session.getId(),session, Constant.expireTime, TimeUnit.SECONDS);
        return serializable;
    }

    @Override
    protected Session doReadSession(Serializable serializable) {
        if(serializable == null){
            return null;
        }
        SimpleSession o = (SimpleSession)redisTemplate.opsForValue().get(serializable);
        if(o == null){
            return null;
        }
        return o;
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        if(session != null && session.getId() != null){
            session.setTimeout( Constant.expireTime * 1000);
            redisTemplate.opsForValue().set(session.getId(),session, Constant.expireTime,TimeUnit.SECONDS);
        }
    }

    @Override
    public void delete(Session session) {
        if (session != null && session.getId() != null) {
            redisTemplate.opsForValue().getOperations().delete(session.getId());
        }
    }

    @Override
    public Collection<Session> getActiveSessions() {
        return redisTemplate.keys("*");
    }
}

二,前后端分离配置

如果是前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据,可以继承FormAuthenticationFilter重写里面的两个方法即可:

org.apache.shiro.web.filter.authc.FormAuthenticationFilter#onAccessDenied
org.apache.shiro.web.filter.authc.FormAuthenticationFilter#onLoginSuccess
package com.chen.admin.filter;

import com.alibaba.fastjson.JSON;
import com.chen.admin.system.Result;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

/**

 * @author @Chenxc
 * @date 2024/7/8 1:29
 **/
public class ShiroFormAuthenticationFilter extends FormAuthenticationFilter {
    private static final Logger log = LoggerFactory.getLogger(ShiroFormAuthenticationFilter.class);

    /** 未登录时返回json
     * @auther: @Chenxc		//作者
     * @Description //TODO 	//描述
     * @param: 	//参数
     * @return: 	//返回值
     * @date:  	//创建日期
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (this.isLoginRequest(request, response)) {
            if (this.isLoginSubmission(request, response)) {
                if (log.isTraceEnabled()) {
                    log.trace("Login submission detected.  Attempting to execute login.");
                }

                return this.executeLogin(request, response);
            } else {
                if (log.isTraceEnabled()) {
                    log.trace("Login page view.");
                }

                return true;
            }
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Attempting to access a path which requires authentication.  Forwarding to the Authentication url [" + this.getLoginUrl() + "]");
            }

            //this.saveRequestAndRedirectToLogin(request, response);
            response.setContentType("application/json; charset=utf-8");
            response.setCharacterEncoding("UTF-8");
            PrintWriter out = response.getWriter();
            out.println(JSON.toJSONString(Result.unauthenticated()));
            out.flush();
            out.close();
            return false;
        }
    }


    /** 登录成功后返回json
     * @auther: @Chenxc		//作者
     * @Description //TODO 	//描述
     * @param: 	//参数
     * @return: 	//返回值
     * @date:  	//创建日期
     */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
        if (log.isTraceEnabled()) {
            log.trace("Attempting to access a path which requires authentication.  Forwarding to the Authentication url [" + this.getLoginUrl() + "]");
        }

        //this.saveRequestAndRedirectToLogin(request, response);
        response.setContentType("application/json; charset=utf-8");
        response.setCharacterEncoding("UTF-8");
        PrintWriter out = response.getWriter();
        Result result = Result.success("登录成功");
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        String header = httpServletResponse.getHeader("Set-Cookie");
        if(null != header){
            String[] split = header.split(";");
            if(null != split && split.length != 0){
                String tokenStr = split[0].split("=")[1];
                result.setData(tokenStr);
            }
        }
        out.println(JSON.toJSONString(result));
        out.flush();
        out.close();
        return false;
    }
}

最后controller

package com.chen.admin.controller;

import com.chen.admin.entity.Menu;
import com.chen.admin.service.MenuService;
import com.chen.admin.system.Result;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author @Chenxc
 * @date 2024/7/15 23:46
 **/
@RestController
@RequestMapping("/menu")
@RequiresPermissions({"MENU"})//访问该controller需要的权限
public class MenuController {

    @Autowired
    private MenuService menuService;


    @RequestMapping("/list")
    public Result<Menu> list(){
        return menuService.menuList();
    }


    @RequestMapping("/save")
    public Result<Menu> save(Menu menu){
        return menuService.saveMenu(menu);
    }


    @RequestMapping("/delete")
    public Result delete(Long id){
        return menuService.delete(id);
    }

    @RequestMapping("/update")
    public Result update(Menu menu){
        return menuService.updateMenu(menu);
    }

}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LLM的原理理解6-10:6、前馈步骤7、使用向量运算进行前馈网络的推理8、注意力层和前馈层有不同的功能9、语言模型的训练方式10、GPT-3的惊人性能

目录 LLM的原理理解6-10: 6、前馈步骤 7、使用向量运算进行前馈网络的推理 8、注意力层和前馈层有不同的功能 注意力:特征提取 前馈层:数据库 9、语言模型的训练方式 10、GPT-3的惊人性能 一个原因是规模 大模型GPT-1。它使用了768维的词向量,共有12层,总共有1.…

大模型系列11-ray

大模型系列11-ray PlasmaPlasmaStore启动监听处理请求 ProcessMessagePlasmaCreateRequest请求PlasmaCreateRetryRequest请求PlasmaGetRequest请求PlasmaReleaseRequestPlasmaDeleteRequestPlasmaSealRequest ObjectLifecycleManagerGetObjectSealObject ObjectStoreRunnerPlas…

开源动态表单form-create-designer 扩展个性化配置的最佳实践教程

在开源低代码表单设计器 form-create-designer 的右侧配置面板里&#xff0c;field 映射规则为开发者提供了强大的工具去自定义和增强组件及表单配置的显示方式。通过这些规则&#xff0c;你可以简单而高效地调整配置项的展示&#xff0c;提升用户体验。 源码地址: Github | G…

美创科技入选2024数字政府解决方案提供商TOP100!

11月19日&#xff0c;国内专业咨询机构DBC德本咨询发布“2024数字政府解决方案提供商TOP100”榜单。美创科技凭借在政府数据安全领域多年的项目经验、技术优势与创新能力&#xff0c;入选收录。 作为专业数据安全产品与服务提供商&#xff0c;美创科技一直致力于为政府、金融、…

地平线 bev_cft_efficientnetb3 参考算法-v1.2.1

01 概述 在自动驾驶感知算法中 BEV 感知成为热点话题&#xff0c;BEV 感知可以弥补 2D 感知的缺陷构建 3D “世界”&#xff0c;更有利于下游任务和特征融合。 地平线集成了基于 bev 的纯视觉算法&#xff0c;目前已支持 ipm-based 、lss-based、 transformer-based&#xff…

C#里怎么样检测文件的属性?

C#里怎么样检测文件的属性? 对于文件来说,在C#里有一种快速的方法来检查文件的属性。 比如文件是否已经压缩, 文件是否加密, 文件是否是目录等等。 属性有下面这么多: 例子演示如下: /** C# Program to View the Information of the File*/ using System; using Syste…

最新‌VSCode保姆级安装教程(附安装包)

文章目录 一、VSCode介绍 二、VSCode下载 下载链接&#xff1a;https://pan.quark.cn/s/19a303ff81fc 三、VSCode安装 1.解压安装文件&#xff1a;双击打开并安装VSCode 2.勾选我同意协议&#xff1a;然后点击下一步 3.选择目标位置&#xff1a;点击浏览 4.选择D盘安装…

传输控制协议(TCP)和用户数据报协议(UDP)

一、传输控制协议&#xff08;TCP&#xff09; 传输控制协议&#xff08;Transmission Control Protocol&#xff0c;TCP&#xff09;是一种面向连接的、可靠的、基于字节流的传输层通信协议&#xff0c;由 IETF 的 RFC 793 定义。 它通过三次握手建立连接&#xff0c;确保数…

linux从0到1——shell编程9

声明&#xff01; 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团队无关&a…

nature communications论文 解读

题目《Transfer learning with graph neural networks for improved molecular property prediction in the multi-fidelity setting》 这篇文章主要讨论了如何在多保真数据环境&#xff08;multi-fidelity setting&#xff09;下&#xff0c;利用图神经网络&#xff08;GNNs&…

基于Qt/C++/Opencv实现的一个视频中二维码解析软件

本文详细讲解了如何利用 Qt 和 OpenCV 实现一个可从视频和图片中检测二维码的软件。代码实现了视频解码、多线程处理和界面更新等功能&#xff0c;是一个典型的跨线程图像处理项目。以下分模块对代码进行解析。 一、项目的整体结构 项目分为以下几部分&#xff1a; 主窗口 (M…

【Elasticsearch入门到落地】2、正向索引和倒排索引

接上篇《1、初识Elasticsearch》 上一篇我们学习了什么是Elasticsearch&#xff0c;以及Elastic stack(ELK)技术栈介绍。本篇我们来什么是正向索引和倒排索引&#xff0c;这是了解Elasticsearch底层架构的核心。 上一篇我们学习到&#xff0c;Elasticsearch的底层是由Lucene实…

鸿蒙主流路由详解

鸿蒙主流路由详解 Navigation Navigation更适合于一次开发,多端部署,也是官方主流推荐的一种路由控制方式,但是,使用起来入侵耦合度高,所以,一般会使用HMRouter,这也是官方主流推荐的路由 Navigation官网地址 个人源码地址 路由跳转 第一步-定义路由栈 Provide(PageInfo) pag…

java使用itext生成pdf

一、利用Adobe Acrobat DC软件创建pdf模板 备好Adobe Acrobat DC软件 1.excel/jpg/png文件转pdf文件 右击打开我们要转换的文件 2.然后点击 添加 域 3.可以看到域的名字 4.调整字体大小/对齐方式等 5.保存 二&#xff0c;代码部分 首先 上依赖 <dependency><group…

生成对抗网络模拟缺失数据,辅助PAMAP2数据集仿真实验

PAMAP2数据集是一个包含丰富身体活动信息的数据集&#xff0c;它为我们提供了一个理想的平台来开发和测试HAR模型。本文将从数据集的基本介绍开始&#xff0c;逐步引导大家通过数据分割、预处理、模型训练&#xff0c;到最终的性能评估&#xff0c;在接下来的章节中&#xff0c…

全面解析:HTML页面的加载全过程(一)--输入URL地址,与服务器建立连接

用户输入URL地址&#xff0c;与服务器建立连接 用户在浏览器地址栏输入一个URL 浏览器开始执行以下三步操作操作&#xff1a;url解析、DNS查询、TCP连接 第一步&#xff1a;URL解析 什么是URL&#xff1f; URL(Uniform Resource Locator&#xff0c;统一资源定位符)是互联网…

STM32总体架构简单介绍

目录 一、引言 二、STM32的总体架构 1、三个被动单元 &#xff08;1&#xff09;内部SRAM &#xff08;2&#xff09;内部闪存存储器 &#xff08;3&#xff09;AHB到APB的桥&#xff08;AHB to APBx&#xff09; 2、四个主动&#xff08;驱动&#xff09;单元 &#x…

postman 调用 下载接口(download)使用默认名称(response.txt 或随机名称)

官网地址&#xff1a;https://www.postman.com 介绍 Postman 是一款流行的 API 开发和测试工具&#xff0c;用于发送 HTTP 请求、测试接口、调试服务器响应以及进行 API 文档管理。它支持多种请求类型&#xff08;如 GET、POST、PUT、DELETE 等&#xff09;&#xff0c;并且功能…

JavaScript将至

JS是什么&#xff1f; 是一种运行在客户端&#xff08;浏览器&#xff09;的编程语言&#xff0c;实现人机交互效果 作用捏&#xff1f; 网页特效 (监听用户的一些行为让网页作出对应的反馈) 表单验证 (针对表单数据的合法性进行判断) 数据交互 (获取后台的数据, 渲染到前…

Vue.js 学习总结(13)—— Vue3 version 计数介绍

前言 Vue3.5 提出了两个重要概念&#xff1a;version计数和双向链表&#xff0c;作为在内存和计算方面性能提升的最大功臣。既然都重要&#xff0c;那就单挑 version 计数来介绍&#xff0c;它在依赖追踪过程中&#xff0c;起到快速判断依赖项有没有更新的作用&#xff0c;所以…