【SpringSecurity源码】过滤器链加载流程


theme: smartblue
highlight: a11y-dark

一、前言及准备

1.1 SpringSecurity过滤器链简单介绍

在Spring Security中,过滤器链(Filter Chain)是由多个过滤器(Filter)组成的,这些过滤器按照一定的顺序对进入应用的请求进行处理。每个过滤器可以执行不同的安全操作,如身份验证、授权、安全上下文的建立等。

过滤器是一种典型的AOP思想,我们将通过源码分析这些过滤器如何被加载以及组成过滤器链。

1.2 SpringSecurity配置类

该类主要对SpringSecurity进行一系列配置,后续过滤器链的初始化和加载也是基于这个配置类。当前配置类仅供参考。注意里面两个很重要的方法 #configure(WebSecurity web)以及#configure(WebSecurity web),他们对过滤器链的初始化很重要。

@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter {// 自定义登录成功或失败处理器,退出登录处理器@Autowiredprivate MyAuthenticationService myAuthenticationService;// 自定义验证码过滤器@Autowiredprivate ValidateCodeFilter validateCodeFilter;// 自定义权限不足处理@Autowiredprivate MyAccessDeniedHandler myAccessDeniedHandler;// 权限相关Service@Autowiredprivate PermissionService permissionService;@Overridepublic void configure(WebSecurity web) throws Exception {//解决静态资源被拦截的问题web.ignoring().antMatchers("/css/**", "/images/**", "/js/**", "/code/**");}/*** http请求方法** @param http* @throws Exception*/@Overrideprotected void configure(HttpSecurity http) throws Exception {// 加入用户名密码验证过滤器的前面http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);// 查询数据库所有权限列表List<Permission> list = permissionService.list();for (Permission permission : list) {// 添加请求权限http.authorizeRequests().antMatchers(permission.getPermissionUrl()).hasAuthority(permission.getPermissionTag());}// 设置权限不足的信息http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);http.formLogin()// 开启表单认证.loginPage("/toLoginPage")// 自定义登录页面.loginProcessingUrl("/login")//表单提交的路径.usernameParameter("username").passwordParameter("password")//自定义input的name值.successForwardUrl("/")//登录成功之后跳转的路径.successHandler(myAuthenticationService).failureHandler(myAuthenticationService)//登录成功或者失败的处理.and().logout().logoutUrl("/logout").logoutSuccessHandler(myAuthenticationService).and().rememberMe()//开启记住我功能.tokenValiditySeconds(1209600)//token失效时间 默认是2周.rememberMeParameter("remember-me")//自定义表单input值.tokenRepository(getPersistentTokenRepository()).and().authorizeRequests().antMatchers("/toLoginPage").permitAll()//放行登录页面.anyRequest().authenticated();//关闭csrf防护http.csrf().disable();//加载同源域名下iframe页面http.headers().frameOptions().sameOrigin();// 开启跨域支持http.cors().configurationSource(corsConfigurationSource());}} 

1.3 过滤器链加载方法入口

Spring boot启动中会加载spring.factories文件, 在文件中有对应针对Spring Security的过滤器链
的配置信息,所以spring.factories来找过滤器链加载方法入口

image.png

所以我们的过滤器链加载方法入口位于org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration#springSecurityFilterChain()

二、 源码导读

SpringSecurity的过滤器链创建本质就是读取SecurityConfig配置类,并且通过核心配置方法:#configure(WebSecurity web)以及#configure(WebSecurity web)生成一个个配置对象,最终每个配置对象会转换成一个过滤器对象,这些过滤器对象组成过滤器链。

前面已经提到了,配置类中的 #configure(WebSecurity web)以及#configure(WebSecurity web)非常重要。他们将分别加载到一个HttpSecurity对象和一个名为ignoredRequests的ArrayList之中,
并且过滤器链也是由这两部分转换后组成

2.1 SpringSecurity配置信息的加载

由前面的内容我们已经定位到过滤器链加载的方法入口位于:
org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration#springSecurityFilterChain()

@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {boolean hasConfigurers = webSecurityConfigurers != null&& !webSecurityConfigurers.isEmpty();if (!hasConfigurers) {WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter() {});webSecurity.apply(adapter);}// 加载方法return webSecurity.build();
}

我们从该方法的webSecurity.build()按照下图一路点击,最终会进入到org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder#doBuild()
方法,该方法是加载配置和过滤器链的主要方法

image.png

org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder#doBuild()如下

@Override
protected final O doBuild() throws Exception {synchronized (configurers) {buildState = BuildState.INITIALIZING;beforeInit();// 【1】.初始化,主要构建HttpSecurity对象以及内部的配置对象init();buildState = BuildState.CONFIGURING;beforeConfigure();// 【2】ignoredRequests集合的构建configure();buildState = BuildState.BUILDING;// 【3】组装过滤器链O result = performBuild();buildState = BuildState.BUILT;return result;}

可以看到,当前的configurers加载的就是我们自定义的配置类

image.png

2.1.1 构建带有配置信息的HttpSecurity对象

我们从上述的doBuild()方法体中点击init()方法进入其内部

org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder#init()

private void init() throws Exception {Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();for (SecurityConfigurer<O, B> configurer : configurers) {// 构建HttpSecurity对象configurer.init((B) this);}for (SecurityConfigurer<O, B> configurer : configurersAddedInInitializing) {configurer.init((B) this);}
}

接着点击构建HttpSecurity的对象的方法configurer.init()方法

org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#init(),发现真正构建HttpSecurity的方法其实是 getHttp()

public void init(final WebSecurity web) throws Exception {// 真正构建HttpSecurity的方法final HttpSecurity http = getHttp();// 将HttpSecurity对象作为SecurityFilterChainBuilder对象返回web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class);web.securityInterceptor(securityInterceptor);});
}

点击进入 getHttp()

protected final HttpSecurity getHttp() throws Exception {/**省略其他**/// 【1】.新建一个HttpSecurity对象http = new HttpSecurity(objectPostProcessor, authenticationBuilder,sharedObjects);if (!disableDefaults) {// 【2】.设置HttpSecurity对象中configurers配置对象以及filter过滤器对象http.csrf().and() .addFilter(new WebAsyncManagerIntegrationFilter()) .exceptionHandling().and().headers().and().sessionManagement().and().securityContext().and().requestCache().and().anonymous().and().servletApi().and().apply(new DefaultLoginPageConfigurer<>()).and().logout();/**省略**/}// 【3】.执行SecurityConfig中的configure(HttpSecurity http)方法configure(http);return http;
}

观察上述代码我们发现,首先通过new HttpSecurity()新建了一个HttpSecurity对象,并且在下方设置HttpSecurity对象中configurers属性以及filter属性。

当执行完下面代码时,即【2】处的代码

        http.csrf().and() //【添加CsrfConfigurer】.addFilter(new WebAsyncManagerIntegrationFilter())  // 【添加 WebAsyncManagerIntegrationFilter】.exceptionHandling().and() // 【添加 ExceptionHandlingConfigurer】.headers().and() // 【添加 HeadersConfigurer】.sessionManagement().and()// 【添加SessionManagementConfigurer】.securityContext().and()// 【添加SecurityContextConfigurer】.requestCache().and()// 【添加RequestCacheConfigurer】.anonymous().and()// 【添加AnonymousConfigurer】.servletApi().and()// 【添加ServletApiConfigurer】.apply(new DefaultLoginPageConfigurer<>()).and()//【添加DefaultLoginPageConfigurer】.logout(); // 【添加LogoutConfigure】r

HttpSecurity对象中configurers属性以及filter如下,此时size分别为 101

image.png
image.png

执行了【2】的代码后会执行【3】的代码:
configure(http);
该方法会调用SecurityConfig配置类文件的configure()方法,由于此时传入的HttpSecurity对象,所以调用的
SecurityConfig配置类文件中的configure(HttpSecurity http),代码如下(注意该部分为自定义的配置文件中方法,需要伙伴自己去编写,根据自己需求进行配置。当前仅供参考!):

@Override
protected void configure(HttpSecurity http) throws Exception {// 加入用户名密码验证过滤器的前  【添加ValidateCodeFilter】http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);// 查询数据库所有权限列表List<Permission> list = permissionService.list();【添加ExpressionUrlAuthorizationConfigurer】for (Permission permission : list) {// 添加请求权限http.authorizeRequests().antMatchers(permission.getPermissionUrl()).hasAuthority(permission.getPermissionTag());}// 设置权限不足的信息 http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);http.formLogin()// 开启表单认证 【添加FormLoginConfigurer】.loginPage("/toLoginPage")// 自定义登录页面.loginProcessingUrl("/login")//表单提交的路径.usernameParameter("username").passwordParameter("password")//自定义input的name值.successForwardUrl("/")//登录成功之后跳转的路径.successHandler(myAuthenticationService).failureHandler(myAuthenticationService)//登录成功或者失败的处理.and().logout().logoutUrl("/logout").logoutSuccessHandler(myAuthenticationService).and().rememberMe()//开启记住我功能 【添加 RememberMeConfigurer】.tokenValiditySeconds(1209600)//token失效时间 默认是2周.rememberMeParameter("remember-me")//自定义表单input值.tokenRepository(getPersistentTokenRepository()).and().authorizeRequests().antMatchers("/toLoginPage").permitAll()//放行登录页面.anyRequest().authenticated();//关闭csrf防护// 【移除CsrfConfigurer】http.csrf().disable();//加载同源域名下iframe页面http.headers().frameOptions().sameOrigin();// 开启跨域支持// 【添加CorsConfigurer】http.cors().configurationSource(corsConfigurationSource());}

执行完该方法后,configurers属性以及filter属性如下,size变为132

image.png
image.png

HttpSecurity对象构建完成!,之后configurers中每个Configurer对象最终都会转换为Filter对象

2.1.2 列表集合 ignoredRequests 的创建

当执行完init()后,HttpSecurity对象构建完成,并且存放在一个叫做securityFilterChainBuilders的集合对象中。

image.png

此时我们回到org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder#doBuild(),在执行init()方法之后会有一个configure()方法,点击进去可以看到:

private void configure() throws Exception {Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();for (SecurityConfigurer<O, B> configurer : configurers) {// 【加载配置类SecurityConfig中的configure(WebSecurity web)方法】configurer.configure((B) this);}
}

注意看

configurer.configure((B) this);

这句代码,此时的this是一个WebSecurity对象
image.png

仔细想想,在配置类文件中,不正是有一个#configure(WebSecurity web)配置方法吗?没错,这句代码正是执行了配置文件中的该方法。

#configure(WebSecurity web)方法体:

image.png

执行完这一行代码之后,ignoredRequests变量已经被赋值,而且变量中的值是和上面配置方法中配置对应的。

image.png

OK,此时SpringSecurity配置信息的加载就已经完成,接下来就是将这些配置信息转换成过滤器并组成过滤器链

2.2 过滤器链的加载

加载完配置文件后,我们的配置信息分别被存放在两部分ignoredRequestssecurityFilterBulders之中,下面再来看看这两部分是如何构建成过滤器链的。

2.2.1 ignoredRequests 转换成过滤器链

image.png
此时我们再次回到此时我们回到org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder#doBuild()
找到构造过滤器链的方法performBuild()

org.springframework.security.config.annotation.web.builders.WebSecurity#performBuild()

@Override
protected Filter performBuild() throws Exception {/**忽略部分代码**/【1】计算过滤器链长度int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);【2】将ignoredRequests集合中的对象转为过滤器加入到过滤器链中for (RequestMatcher ignoredRequest : ignoredRequests) {securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));}【3】将securityFilterChainBuilder转为过滤器加入到过滤器链中for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {securityFilterChains.add(securityFilterChainBuilder.build());}/**忽略部分代码**/}/**忽略部分代码**/return result;
}

【1】处代码计算过滤器链长度chainSize,Debug中计算结果如下

image.png

【2】处代码将ignoredRequests集合中的对象转为过滤器加入到过滤器链securityFilterChains
中,执行结果如下

image.png

ignoredRequests转换成过滤器链完成

【3】处的代码就是 securityFilterBulders的的处理。下面接着讲。

2.2.2 securityFilterBulders 转换成过滤器链

在上面的org.springframework.security.config.annotation.web.builders.WebSecurity#performBuild() 方法中通过观察看到,处理securityFilterBulders代码就一句话

securityFilterChains.add(securityFilterChainBuilder.build());

我们从securityFilterChainBuilder.build(),按照下面顺序一路点击:

image.png

我们将会定位到
org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder#configure()

private void configure() throws Exception {【1】获取配置对象Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();for (SecurityConfigurer<O, B> configurer : configurers) {【2】将配置对象加入到过滤器链configurer.configure((B) this);}
}

在【1】处,我们将得到从securityFilterChainBuilder中得到configurers配置列表

image.png

在【2】处,会循环配置列表对每个配置对象处理并且最终加入到过滤器链中。
比如当前是ExceptionHandlingConfigurer
那么就会执行
org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer#configure()

@Override
public void configure(H http) {AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);// 【1】创建 ExceptionTranslationFilterExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint, getRequestCache(http));AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);exceptionTranslationFilter = postProcess(exceptionTranslationFilter);//【2】 ExceptionTranslationFilter加入到过滤器链http.addFilter(exceptionTranslationFilter);
}

再比如当前是HeaderConfigurer,那么会执行org.springframework.security.config.annotation.web.configurers.HeadersConfigurer#configure()

@Override
public void configure(H http) {【1】创建 HeaderWriterFilterHeaderWriterFilter headersFilter = createHeaderWriterFilter();【2】加入到过滤器链http.addFilter(headersFilter);
}

也就是说,具体是什么类型的Filter,是根据当前Configurer而决定的

当我们执行完所有的configure()方法,再次回到org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder#doBuild()

可以看到filter已经被加载出来

image.png

但是,过滤器是有一定顺序的,此时加载出来的过滤器并没有处理顺序。
当方法执行到 performBuild()内部时,会进行排序
org.springframework.security.config.annotation.web.builders.HttpSecurity#performBuild()

@Override
protected DefaultSecurityFilterChain performBuild() {【对过滤器进行排序】filters.sort(comparator);return new DefaultSecurityFilterChain(requestMatcher, filters);
}

排序后的过滤器链:

image.png

到这里,拦截器链加载结束!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于SpringBoot+Vue的法律咨询系统

课题背景 二十一世纪互联网的出现&#xff0c;改变了几千年以来人们的生活&#xff0c;不仅仅是生活物资的丰富&#xff0c;还有精神层次的丰富。在互联网诞生之前&#xff0c;地域位置往往是人们思想上不可跨域的鸿沟&#xff0c;信息的传播速度极慢&#xff0c;信息处理的速…

Kafka应用Demo:按主题订阅消费消息

安装环境 Kafka安装可参考官方网站的指导(https://kafka.apache.org/quickstart), 按步骤解压压缩包&#xff0c;修改配置。然后再启动zookeeper和kafka-server即可。 需要注意的一点&#xff1a;如果是在VMware虚拟机上启动的kafka, 需要修改一下server.properties配置文件&am…

AI浪潮再起,2024年中国大模型产业深度解析

国内 AI大模型产业发展深度分析 2024 人工智能技术的迅猛发展&#xff0c;使AI大模型成为科技竞争的核心、产业变革的先锋、经济增长的新动力。我国已将人工智能列为国家战略&#xff0c;出台系列政策扶持其发展&#xff0c;为AI大模型产业创造优越环境&#xff0c;展现巨大潜力…

CentOS 7安装配置docker

CentOS 7、8安装、配置docker 这里宿主机的型号选择是centos7.9.2009的版本 1.宿主机关闭防火墙和selinux&#xff0c;配置ipv4 #设置SELinuxdisabled vim /etc/selinux/config SELinuxdisabled 查看防火墙状态&#xff1a;firewall-cmd --state 关闭防火墙&#xff1a;syst…

FloodFill算法---BFS

目录 一、前言 二、算法模板套路 2.1 创建所需的全局变量&#xff1a; 2.2 BFS模板&#xff1a; 2.3 细节处理&#xff1a; 三、例题练习 3.1 例题1&#xff1a;图像渲染 3.2 例题2&#xff1a;岛屿数量 3.3 例题3&#xff1a;岛屿的最大面积 3.4 例题4&#xff1a;被…

在做题中学习(54):点名

LCR 173. 点名 - 力扣&#xff08;LeetCode&#xff09; 此题有不同的几种解法&#xff1a; 解法一&#xff1a;暴力枚举 O(n); 解法二&#xff1a;哈希表 把原数组丢入哈希表&#xff0c;遍历哈希表&#xff0c;看看哪个数值为0即可。 O(n)空间O(n)时间 解法三&…

OpenAI推出新模型GPT-4o:可实时交互,检测人的情绪,支持多模态输出

GPT-4o作为OpenAI新发布的人工智能模型&#xff0c;据官方及媒体报道&#xff0c;是面向全球用户发布的&#xff0c;包括中国在内的用户理论上应该能够通过相应平台和应用访问。不过&#xff0c;实际可用性还需考虑地区政策、网络访问限制以及具体平台是否在中国有本地化服务等…

1694jsp宿舍管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 宿舍管理系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统采用web模式&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库…

网络安全快速入门(十一)vi/vim

11.1 了解vi 前面我i们已经在基础命令中大致了解了vi&#xff0c;本章我们针对vi来细讲一下&#xff0c;vi和vim 11.1.1 什么是vi/vim&#xff1f; vi和vim&#xff0c;都是一个模块化的文本编辑工具&#xff0c;换句话讲&#xff0c;通过vi下的一系列的命令&#xff0c;可以实…

Redis 源码安装和入门介绍

Linux下的redis源码安装 redis介绍 Redis 是一个开源&#xff08;BSD许可&#xff09;的&#xff0c;内存中的数据结构存储系统&#xff0c;它可以用作数据库、缓存和消息中间件。它支持多种类型的数据结构&#xff0c;如 字符串&#xff08;strings&#xff09;&#xff0c;…

专访安克创新CEO阳萌:仿生算法与存算一体芯片的兴起

在这篇博客中&#xff0c;我们将探讨人工智能的未来发展方向&#xff0c;特别是围绕大模型、存算一体芯片以及仿生算法的讨论。通过对安克创新CEO阳萌的专访内容进行分析&#xff0c;我们将尝试解答一些关于AI发展的关键问题&#xff0c;并对未来的技术趋势进行预测。 引言 …

AD原理图设置:如何在编译工程时,报未连接线或引脚错误

如下图&#xff0c;AD默认在编译原理图时&#xff0c;如果出现未连接的引脚或线时&#xff0c;并不会报相关的错误&#xff0c;这样做其实很危险 所以&#xff0c;我们应该让它提示错误 具体配置方法&#xff1a; 1、找到工程选项 2、切换到第二个选项“Connection Matrix”&a…

OBS插件--源录制

源录制 将应用这个滤镜的源录制成视频保存下来&#xff0c;可以选择音轨&#xff0c;也可以针对应用此滤镜的源单独的推流等。 如果在直播或录制视频的过程中场景里面布置了多个源&#xff0c;而只想保存其中一个源的视频或音频这个插件非常使用。 下面截图演示下操作步骤&a…

面试中的算法(查找缺失的整数)

在一个无序数组里有99个不重复的正整数&#xff0c;范围是1~100&#xff0c;唯独缺少1个1~100中的整数。如何找出这个缺失的整数? 一个很简单也很高效的方法&#xff0c;先算出1~100之和&#xff0c;然后依次减去数组里的元素&#xff0c;最后得到的差值&#xff0c;就是那个缺…

数据库入门(sql文档+命令行)

一.基础知识 1.SQL&#xff08;Structured Query Language&#xff09;结构化查询语言分类&#xff1a; DDL数据定义语言用来定义数据库对象&#xff1a;数据库、表、字段DML数据操作语言对数据库进行增删改查DQL数据查询语言查询数据库中表的信息DCL数据控制语言用来创建数据…

安装adobe系列,提示错误代码146解决办法

安装Adobe系列产品如PS、PR、Lrc等产品时&#xff0c;会因为各种各样的错误导致安装失败&#xff01;今天小编为大家带来的是安装adobe系列&#xff0c;提示错误代码146解决办法&#xff0c;收藏起来吧&#xff01; 方法一&#xff1a;就是传说中的万能大法&#xff0c;关机重启…

苍穹外卖项目---------收获以及改进(9-12)

①Spring Task-------实现系统定时任务 概念&#xff1a; 应用场景&#xff1a; 使用步骤&#xff1a; 实现订单超时和前一天派送中的订单的自动任务处理&#xff1a; Component Slf4j public class Mytask {Autowiredprivate OrderServiceimpl orderServiceimpl;/*** 处理订…

基于uniapp+vue3+ts小程序项目实战之项目初始化

&#x1f680; 作者 &#xff1a;“二当家-小D” &#x1f680; 博主简介&#xff1a;⭐前荔枝FM架构师、阿里资深工程师||曾任职于阿里巴巴担任多个项目负责人&#xff0c;8年开发架构经验&#xff0c;精通java,擅长分布式高并发架构,自动化压力测试&#xff0c;微服务容器化k…

OpenCV使用 Kinect 和其他兼容 OpenNI 的深度传感器(75)

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇:使用 OpenCV 创建视频(74) 下一篇 :OpenCV使用 Orbbec Astra 3D 相机(76) 目的&#xff1a;​ 通过 VideoCapture 类支持与 OpenNI 兼容的深度传感器&#xff08;Kinect、XtionPRO 等&#xff09;。…

【数据结构】解密链表之旅(单链表篇)

前言 哈喽大家好&#xff0c;我是野生的编程萌新&#xff0c;首先感谢大家的观看。数据结构的学习者大多有这样的想法&#xff1a;数据结构很重要&#xff0c;一定要学好&#xff0c;但数据结构比较抽象&#xff0c;有些算法理解起来很困难&#xff0c;学的很累。我想让大家知道…