spring — Spring Security 5.7与6.0差异性对比

news2024/9/24 7:24:13

1. spring security

Spring Security 是一个提供身份验证、授权和针对常见攻击保护的框架。 凭借对保护命令式和反应式应用程序的一流支持,它成为基于Spring的标准安全框架。

Spring Security 在最近几个版本中配置的写法都有一些变化,很多常见的方法都废弃了,并且将在未来的 Spring Security7 中移除,Spring Security5.7 开始(对应 Spring Boot2.7 开始)将重新梳理一下主要功能变化。以下是spring security的版本列表:
在这里插入图片描述

2. spring security 新增功能

2.1 WebSecurityConfigurerAdapter

  • gh-11923 - Remove WebSecurityConfigurerAdapter. Instead, create a SecurityFilterChain bean.

这一条更新改变了我们配置SpringSecurity的方式,WebSecurityConfigurerAdapter 过期了,在目前最新的 Spring Security6.1 中,这个类已经完全被移除了,作为替代,我们需要创建类型为SecurityFilterChain的bean。
在这里插入图片描述
以前WebSecurityConfigurerAdapter+HttpSecurity 的方式为:

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
    }
}

SecurityFilterChain+HttpSecurity 新的写法为:

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
        return http.build();
    }

}

以前WebSecurityConfigurerAdapter+WebSecurity 的方式为:

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/api1", "/api2");
    }
}

SecurityFilterChain+WebSecurity 新的写法为:

@Configuration
public class SecurityConfiguration {
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().antMatchers("/api1", "/api2");
    }
}

以前WebSecurityConfigurerAdapter配置AuthenticationManager是通过重写父类的方法来获取这个 Bean:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

6.0后则需要单独创建AuthenticationManager这个bean:

@Configuration
public class SecurityConfig {

    @Autowired
    UserService userService;

    @Bean
    AuthenticationManager authenticationManager() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userService);
        ProviderManager pm = new ProviderManager(daoAuthenticationProvider);
        return pm;
    }
}

也可以从 HttpSecurity 中提取出来 AuthenticationManager:

@Configuration
public class SpringSecurityConfiguration {

    AuthenticationManager authenticationManager;

    @Autowired
    UserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder.userDetailsService(userDetailsService);
        authenticationManager = authenticationManagerBuilder.build();

        http.csrf().disable().cors().disable().authorizeHttpRequests().antMatchers("/api/v1/account/register", "/api/v1/account/auth").permitAll()
            .anyRequest().authenticated()
            .and()
            .authenticationManager(authenticationManager)
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        return http.build();
    }
}

Spring Security 的底层实际上就是一堆过滤器,所以我们之前在 configure(HttpSecurity) 方法中的配置,实际上就是配置过滤器链。现在过滤器链的配置,我们通过提供一个 SecurityFilterChain Bean 来配置过滤器链,SecurityFilterChain 是一个接口,这个接口只有一个实现类 DefaultSecurityFilterChain,构建 DefaultSecurityFilterChain 的第一个参数是拦截规则,也就是哪些路径需要拦截,第二个参数则是过滤器链,如果给了一个空集合,不设置忽略的路径请求, Spring Security 会拦截下所有的请求,然后在一个空集合中走一圈就结束了,相当于不拦截任何请求。

2.2 使用 Lambda表达式

and() 方法将被移除:
在这里插入图片描述
从上面 and 方法的注释中小伙伴们可以看到,官方现在是在推动基于 Lambda 的配置来代替传统的链式配置。

2.3 自定义 JSON 登录

Spring Security 默认处理登录数据的过滤器是 UsernamePasswordAuthenticationFilter,在这个过滤器中,系统会通过 request.getParameter(this.passwordParameter) 的方式将用户名和密码读取出来,很明显这就要求前端传递参数的形式是 key-value。
步骤为:

  1. 首先我们获取请求头,根据请求头的类型来判断请求参数的格式。
  2. 如果是 JSON 格式的参数,就在 if 中进行处理,否则说明是 key-value 形式的参数,那么我们就调用父类的方法进行处理即可。JSON 格式的参数的处理逻辑和 key-value 的处理逻辑是一致的,唯一不同的是参数的提取方式不同而已。
  3. 最后,我们还需要对这个过滤器进行配置:
@Configuration
public class SecurityConfig {

    @Autowired
    UserService userService;

    @Bean
    JsonLoginFilter jsonLoginFilter() {
        JsonLoginFilter filter = new JsonLoginFilter();
        filter.setAuthenticationSuccessHandler((req,resp,auth)->{
            resp.setContentType("application/json;charset=utf-8");
            PrintWriter out = resp.getWriter();
            //获取当前登录成功的用户对象
            User user = (User) auth.getPrincipal();
            user.setPassword(null);
            RespBean respBean = RespBean.ok("登录成功", user);
            out.write(new ObjectMapper().writeValueAsString(respBean));
        });
        filter.setAuthenticationFailureHandler((req,resp,e)->{
            resp.setContentType("application/json;charset=utf-8");
            PrintWriter out = resp.getWriter();
            RespBean respBean = RespBean.error("登录失败");
            if (e instanceof BadCredentialsException) {
                respBean.setMessage("用户名或者密码输入错误,登录失败");
            } else if (e instanceof DisabledException) {
                respBean.setMessage("账户被禁用,登录失败");
            } else if (e instanceof CredentialsExpiredException) {
                respBean.setMessage("密码过期,登录失败");
            } else if (e instanceof AccountExpiredException) {
                respBean.setMessage("账户过期,登录失败");
            } else if (e instanceof LockedException) {
                respBean.setMessage("账户被锁定,登录失败");
            }
            out.write(new ObjectMapper().writeValueAsString(respBean));
        });
        filter.setAuthenticationManager(authenticationManager());
        filter.setFilterProcessesUrl("/login");
        return filter;
    }

    @Bean
    AuthenticationManager authenticationManager() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userService);
        ProviderManager pm = new ProviderManager(daoAuthenticationProvider);
        return pm;
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        //开启过滤器的配置
        http.authorizeHttpRequests()
                //任意请求,都要认证之后才能访问
                .anyRequest().authenticated()
                .and()
                //开启表单登录,开启之后,就会自动配置登录页面、登录接口等信息
                .formLogin()
                //和登录相关的 URL 地址都放行
                .permitAll()
                .and()
                //关闭 csrf 保护机制,本质上就是从 Spring Security 过滤器链中移除了 CsrfFilter
                .csrf().disable();
        http.addFilterBefore(jsonLoginFilter(), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

}

这里就是配置一个 JsonLoginFilter 的 Bean,并将之添加到 Spring Security 过滤器链中即可。在 Spring Boot3 之前(Spring Security6 之前),上面这段代码就可以实现 JSON 登录了。
但是从 Spring Boot3 开始,则存在问题,具体表现就是:当你调用登录接口登录成功之后,再去访问系统中的其他页面,又会跳转回登录页面,说明访问登录之外的其他接口时,系统不知道你已经登录过了。

在 Spring Boot3 之前,Spring Security 过滤器链中有一个名为 SecurityContextPersistenceFilter 的过滤器,这个过滤器在 Spring Boot2.7.x 中废弃了,但是还在使用,在 Spring Boot3 中则被从 Spring Security 过滤器链中移除了,取而代之的是一个名为 SecurityContextHolderFilter 的过滤器。

先来看 SecurityContextPersistenceFilter 的核心逻辑:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
    SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
    try {
        SecurityContextHolder.setContext(contextBeforeChainExecution);
        chain.doFilter(holder.getRequest(), holder.getResponse());
    }
    finally {
        SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
        SecurityContextHolder.clearContext();
        this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
    }
}
  1. 首先,这个过滤器位于整个 Spring Security 过滤器链的第三个,是非常靠前的。
  2. 当登录请求经过这个过滤器的时候,首先会尝试从 SecurityContextRepository(上文中的 this.repo)中读取到 SecurityContext 对象,这个对象中保存了当前用户的信息,第一次登录的时候,这里实际上读取不到任何用户信息。
  3. 将读取到的 SecurityContext 存入到 SecurityContextHolder 中,默认情况下,SecurityContextHolder 中通过 ThreadLocal 来保存 SecurityContext 对象,也就是当前请求在后续的处理流程中,只要在同一个线程里,都可以直接从 SecurityContextHolder 中提取到当前登录用户信息。
  4. 请求继续向后执行。
  5. 在 finally 代码块中,当前请求已经结束了,此时再次获取到 SecurityContext,并清空 SecurityContextHolder 防止内存泄漏
  6. 然后调用 this.repo.saveContext 方法保存当前登录用户对象(实际上是保存到 HttpSession 中)
  7. 以后其他请求到达的时候,执行前面第 2 步的时候,就读取到当前用户的信息了,在请求后续的处理过程中,Spring Security 需要知道当前用户的时候,会自动去 SecurityContextHolder 中读取当前用户信息。

这就是 Spring Security 认证的一个大致流程。

然而,到了 Spring Boot3 之后,这个过滤器被 SecurityContextHolderFilter 取代了,我们来看下 SecurityContextHolderFilter 过滤器的一个关键逻辑:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws ServletException, IOException {
    Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
    try {
        this.securityContextHolderStrategy.setDeferredContext(deferredContext);
        chain.doFilter(request, response);
    }
    finally {
        this.securityContextHolderStrategy.clearContext();
        request.removeAttribute(FILTER_APPLIED);
    }
}

通过这段逻辑代码我们发现前面的逻辑基本上还是一样的,不一样的是 finally 中的代码,finally 中少了一步向 HttpSession 保存 SecurityContext 的操作。
未处理加入SecurityContextHolderFilter过滤器之前用户登录成功后,用户信息没有保存到 HttpSession,导致下一次请求到达的时候,无法从 HttpSession 中读取到 SecurityContext 存到 SecurityContextHolder 中,在后续的执行过程中,Spring Security 就会认为当前用户没有登录。这就是问题的原因!

问题解决:

Spring Security 提供了一个修改的入口,在 org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#successfulAuthentication 方法中,源码如下:

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
        Authentication authResult) throws IOException, ServletException {
    SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
    context.setAuthentication(authResult);
    this.securityContextHolderStrategy.setContext(context);
    this.securityContextRepository.saveContext(context, request, response);
    this.rememberMeServices.loginSuccess(request, response, authResult);
    if (this.eventPublisher != null) {
        this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    }
    this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

这个方法是当前用户登录成功之后的回调方法,小伙伴们看到,在这个回调方法中,有一句 this.securityContextRepository.saveContext(context, request, response);,这就表示将当前登录成功的用户信息存入到 HttpSession 中。

在当前过滤器中,securityContextRepository 的类型是 RequestAttributeSecurityContextRepository,这个表示将 SecurityContext 存入到当前请求的属性中,那很明显,在当前请求结束之后,这个数据就没了。在 Spring Security 的自动化配置类中,将 securityContextRepository 属性指向了 DelegatingSecurityContextRepository,这是一个代理的存储器,代理的对象是 RequestAttributeSecurityContextRepository 和 HttpSessionSecurityContextRepository,所以在默认的情况下,用户登录成功之后,在这里就把登录用户数据存入到 HttpSessionSecurityContextRepository 中了。

当我们自定义了登录过滤器之后,就破坏了自动化配置里的方案了,这里使用的 securityContextRepository 对象就真的是 RequestAttributeSecurityContextRepository 了,所以就导致用户后续访问时系统以为用户未登录。

那么解决方案很简单,我们只需要为自定义的过滤器指定 securityContextRepository 属性的值就可以了,如下:

@Bean
JsonLoginFilter jsonLoginFilter() {
    JsonLoginFilter filter = new JsonLoginFilter();
    filter.setAuthenticationSuccessHandler((req,resp,auth)->{
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        //获取当前登录成功的用户对象
        User user = (User) auth.getPrincipal();
          user.setPassword(null);
        RespBean respBean = RespBean.ok("登录成功", user);
        out.write(new ObjectMapper().writeValueAsString(respBean));
    });
    filter.setAuthenticationFailureHandler((req,resp,e)->{
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        RespBean respBean = RespBean.error("登录失败");
        if (e instanceof BadCredentialsException) {
            respBean.setMessage("用户名或者密码输入错误,登录失败");
        } else if (e instanceof DisabledException) {
            respBean.setMessage("账户被禁用,登录失败");
        } else if (e instanceof CredentialsExpiredException) {
            respBean.setMessage("密码过期,登录失败");
        } else if (e instanceof AccountExpiredException) {
            respBean.setMessage("账户过期,登录失败");
        } else if (e instanceof LockedException) {
            respBean.setMessage("账户被锁定,登录失败");
        }
        out.write(new ObjectMapper().writeValueAsString(respBean));
    });
    filter.setAuthenticationManager(authenticationManager());
    filter.setFilterProcessesUrl("/login");
    filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());
    return filter;
}

最后调用 setSecurityContextRepository 方法设置一下就行。Spring Boot3.x 之前之所以不用设置这个属性,是因为这里虽然没保存最后还是在 SecurityContextPersistenceFilter 过滤器中保存了。

在登录成功之后,开发者自己手动将数据存入到 HttpSession 中,这样就能确保下个请求到达的时候,能够从 HttpSession 中读取到有效的数据存入到 SecurityContextHolder 中。

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

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

相关文章

Unity通过代码切换材质

效果展示 代码 using System.Collections; using System.Collections.Generic; using UnityEngine;public class MaterialSwitcher : MonoBehaviour {public Material newMaterial; // 新材质private Material oldMaterial; // 旧材质private Renderer renderer; // 渲染器组件…

C# Onnx Paddle模型 OCR识别服务

效果 项目 可运行程序exe下载 Demo&#xff08;完整源码&#xff09;下载

中小学分班查询系统如何制作?这个方法值得借鉴

暑假即将结束&#xff0c;新学年即将开始&#xff0c;学校面临着一个重要的任务&#xff0c;那就是学生的分班问题。这个问题涉及到新生入学的分班&#xff0c;以及低年级学生升入高年级时的分班。对于负责分班的老师们来说&#xff0c;这无疑增加了不少工作量和挑战。 在开学…

弹性布局,网格布局,JavaScript

弹性盒子布局&#xff08;Flexbox Layout&#xff09;&#xff1a;通过display: flex;设置容器为弹性盒子&#xff0c;可以实现更复杂的自适应和响应式布局。 网格布局&#xff08;Grid Layout&#xff09;&#xff1a;通过display: grid;设置容器为网格布局&#xff0c;可以将…

编译运行LeGO-LOAM算法(跑.bag包)

ubuntu18.04 ros melodic 官方链接&#xff1a;https://github.com/RobustFieldAutonomyLab/LeGO-LOAM 文章目录 1、下载并编译 gtsam2、下载并编译 LeGO-LOAM3、测试运行 1、下载并编译 gtsam 我下载的是gtsam-4.0.2&#xff0c;官网用的gtsam-4.0.0&#xff0c;因为我用的实…

关于个人微信API接口的开发

个人微信开发API接口可拓展功能说明 1、个人微信多账号管理、聚合聊天、多个微信号同时登陆、多个微信号集中在一个窗口进行聊天&#xff0c;实现一人多号同时沟通快速提升沟通效率&#xff0c;提升微信营销效率。 2、客服灵活分配&#xff1a;客服主管可自由分配微信号给指定…

如何使用njsscan识别Node.JS应用中的不安全代码

关于njsscan njsscan是一款功能强大的静态应用程序测试&#xff08;SAST&#xff09;工具&#xff0c;可以帮助广大研究人员找出Node.JS应用程序中不安全的代码模式。该工具使用了 libsast的简单模式匹配器和语法感知语义代码模式搜索工具 semgrep实现其功能。 工具安装 当前…

2023年华数杯数学建模C题思路代码分析 - 母亲身心健康对婴儿成长的影响

# 1 赛题 C 题 母亲身心健康对婴儿成长的影响 母亲是婴儿生命中最重要的人之一&#xff0c;她不仅为婴儿提供营养物质和身体保护&#xff0c; 还为婴儿提供情感支持和安全感。母亲心理健康状态的不良状况&#xff0c;如抑郁、焦虑、 压力等&#xff0c;可能会对婴儿的认知、情…

华为测试开发秋招三面,两轮技术综合面试+HR,险过!

流程上是三轮面试&#xff0c;两轮的技术综合面试HR面试&#xff0c;先面前两轮&#xff0c;前两轮过了&#xff0c;再联系面试者HR面试&#xff0c;面试的岗位是测试岗&#xff0c;两轮面试在1个半小时左右。 面试时&#xff0c;对自己要有自信&#xff0c;对自己的简历也要吃…

flutter:Future、Stream、RxDart

Future 在Flutter中&#xff0c;Future是Dart语言中的一个类&#xff0c;用于表示异步操作的结果。与Future相关的的重要关键字包括async和await。 async&#xff1a;这个关键字用于在方法或函数声明前添加&#xff0c;以指示该方法为异步方法。在异步方法中&#xff0c;执行…

zsh中安装ros-<ros2-distro>-turtlebot3*失败 || 以humble为例

在zsh中尝试使用 sudo apt install ros-<ros2-distro>-turtlebot3* 安装turtlebot3相关仿真包失败&#xff0c;报错E: 无法定位软件包。 但是在bash中尝试使用同样的命令却可以安装。 原因是zsh中如果要使用通配符&#xff0c;那么一定要放在字符串里&#xff0c;以上…

冒泡排序【Java算法】

文章目录 1. 概念2. 思路3. 代码实现 1. 概念 比较前后相邻的两个数据&#xff0c;如果前面数据大于后面的数据&#xff0c;就将这两个数据互换。这样对数组的第0个数据到第 N - 1 个数据进行一次遍历后&#xff0c;最大的一个数据就 “沉” 到数组的第 N - 1 个位置。 N N - …

UI自动化测试之日志记录和单元测试

设置日志 很多公司要求自动化测试需要有日志记录。 记录程序运行时的状态和结果&#xff0c;方便排查问题。 实现步骤&#xff1a; ①在项目中创建config目录&#xff08;不用创建包&#xff09;&#xff0c;复制粘贴log.conf文件 ②在项目中创建logs目录&#xff0c;用于…

第三方材料检测实验室LIMS系统源码 lims源码

实验室LIMS系统采用国际规范的业务管理流程和严格的质量控制体系&#xff0c;对每个检测流程节点采用 “人、机、料、法、环、测”进行质量控制&#xff0c;可记录&#xff0c;可追溯。强大的数据查询和统计分析能力&#xff0c;提高工作效率&#xff1b;自动化地采集实验室原始…

轻量化YOLOv5改进 | 结合repghost结构冲参数化网络,实现轻量化和加速推理,

RepGhost: A Hardware-Efficient Ghost Module via Re-parameterization 论文总结本文改进repghost 核心代码测试参数量和计算量🔥🔥🔥 “引入RepGhostNet以加速CNN网络推理” “网络宽度的自定义调整:无缝嵌入YOLOv5” “通过结构重参数化优化网络性能” “实现高效和…

nexus迁移

数据和配置迁移 打包两个目录&#xff0c;配置nexus-2.13.0-01和数据sonatype-work 数据量大可以split分割之后迁移再合并 大概看下文件目录 [roottest nexus]# tree -L 3 . ├── nexus-2.13.0-01 │ ├── bin │ │ ├── jsw │ │ ├── nexus │ │ …

拥抱未来:2023年及以后十大DevOps工具的最终清单!

软件开发和 IT 运营的世界在不断发展&#xff0c;DevOps 已成为寻求敏捷性、效率和协作的组织的重要方法。随着我们进入 2023 年及以后&#xff0c;DevOps 格局将随着新工具的引入和现有工具的改进而不断发展。在本文中&#xff0c;我们列出了将在未来几年塑造行业的十大DevOps…

2023年08月IDE流行度最新排名

点击查看最新IDE流行度最新排名&#xff08;每月更新&#xff09; 2023年08月IDE流行度最新排名 顶级IDE排名是通过分析在谷歌上搜索IDE下载页面的频率而创建的 一个IDE被搜索的次数越多&#xff0c;这个IDE就被认为越受欢迎。原始数据来自谷歌Trends 如果您相信集体智慧&am…

如何克服看到别人优于自己而感到的焦虑和迷茫?

文章目录 每日一句正能量前言简述自己的感受怎么做如何调整自己的心态后记 每日一句正能量 行动是至于恐惧的良药&#xff0c;而犹豫、拖延&#xff0c;将不断滋养恐惧。 前言 虽然清楚知识需要靠时间沉淀&#xff0c;但在看到自己做不出来的题别人会做&#xff0c;自己写不出的…

2023华数杯数学建模A题2023华数杯A 题隔热材料的结构优化控制研究

问题1 问题1&#xff1a; 该问题需要建立一个数学模型来描绘织物整体热导率与单根纤维热导率之间的关系。这个模型需要考虑织物的结构&#xff08;如纤维的排列、空隙大小和分布等&#xff09;以及纤维和空隙中的空气对热传导的贡献。此外&#xff0c;我们需要根据织物的整体热…