• 【学习笔记】Spring Security 01 认识Spring Security的重要特征(Features)


    Spring Security

    零、概述

    Spring Security(简称SS)是一个高可用的、可自定义的身份认证和鉴权控制的框架。

    类似的框架还有Shiro。

    需求场景:

    现今流行的web开发中,安全的第一位

    原本的鉴权开发流程:springweb自带的过滤器、拦截器等等。涉及到的方面

    • 功能权限
    • 访问权限
    • 菜单权限

    使用过滤器需要大量的原生代码——冗余。

    所以为了解决这些问题,就需要框架来帮助我们实现。

    SS 官网地址以及官方文档

    官方地址spring.io/projects/spring-security

    官方文档阅读地址:https://docs.spring.io/spring-security/reference/servlet/authentication/index.html#servlet-authentication

    image-20231007185421368

    学习地址:kuangshenshuo 的B站视频

    【【狂神说Java】SpringBoot整合SpringSecurity】https://www.bilibili.com/video/BV1KE411i7bC?vd_source=939c126663135132623f2393e41d7a8a

    一、Spring Security快速开始

    Spring Security使用的是面向切面编程的思想,也就是说不需要再刻意改动业务逻辑代码,只需要简单的配置,就可以快速接入使用。

    1.1 Maven引入SS

    直接上pom文件,推荐结合Springboot使用

    <dependencies>
    	
    	<dependency>
    		<groupId>org.springframework.bootgroupId>
    		<artifactId>spring-boot-starter-securityartifactId>
    	dependency>
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    属性参数

    <properties>
    	
    	<spring-security.version>6.1.4spring-security.version>
    	<spring.version>6.0.11spring.version>
    properties>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    其他引入方式查看官网:https://docs.spring.io/spring-security/reference/getting-spring-security.html

    官方提供的示例:

    The completed application can be found in our samples repository. For your convenience, you can download a minimal Reactive Spring Boot + Spring Security application by [clicking here](https://start.spring.io/starter.zip?type=maven-project&language=java&packaging=jar&jvmVersion=1.8&groupId=example&artifactId=hello-security&name=hello-security&description=Hello Security&packageName=example.hello-security&dependencies=webflux,security).

    完整的应用程序可以在我们的示例存储库中找到。 为了您的方便,您可以通过单击此处下载最小的反应式 Spring Boot + Spring 安全性应用程序。

    1.2 Hello Web Security

    二、认识Spring Security的重要特征(Features)

    2.1 鉴权认证(Authentication)和 密码存储(Password Strorage)

    SS提供了全面的身份验证,可以验证任何尝试访问特定资源的人员身份。一般指当用户输入账户密码后进行验证,验证身份后执行授权。

    SS提供了单向转换的密码安全存储功能(无法提供双向密码验证),通常,使用PasswordEncoder存储需要在身份验证时与用户提供的密码比较的密码。

    2.1.1 密码存储历史

    了解即可。

    小结密码的发展史如下:

    明文——单向哈希——盐+哈希——自适应函数——长期凭证+短期凭据(比如bcrypt密码加密配合token令牌)

    多年来,存储密码的标准机制已经发展。 最初,密码以明文形式存储。 密码被认为是安全的,因为数据存储密码保存在访问它所需的凭据中。 但是,恶意用户能够通过使用SQL注入等攻击找到获取用户名和密码的大型“数据转储”的方法。 随着越来越多的用户凭据公开,安全专家意识到我们需要做更多的事情来保护用户的密码。

    然后鼓励开发人员在通过单向哈希(例如SHA-256)运行密码后存储密码。 当用户尝试进行身份验证时,哈希密码将与他们键入的密码的哈希进行比较。 这意味着系统只需要存储密码的单向哈希。 如果发生违规,则仅公开密码的单向哈希。 由于哈希是单向的,并且在计算上很难猜测给定哈希的密码,因此不值得努力找出系统中的每个密码。 为了击败这个新系统,恶意用户决定创建称为彩虹表的查找表( Rainbow Tables。 他们不是每次都猜测每个密码,而是计算一次密码并将其存储在查找表中。

    为了降低彩虹表的有效性,鼓励开发人员使用加盐密码。 将为每个用户的密码生成随机字节(称为盐),而不是仅使用密码作为哈希函数的输入。 盐和用户的密码将通过哈希函数运行以生成唯一的哈希。 盐将以明文形式与用户密码一起存储。 然后,当用户尝试进行身份验证时,哈希密码将与存储的盐的哈希值和他们键入的密码进行比较。 独特的盐意味着彩虹表不再有效,因为每个盐和密码组合的哈希值都不同。

    在现代,我们意识到加密哈希(如SHA-256)不再安全。 原因是使用现代硬件,我们可以每秒执行数十亿次哈希计算。 这意味着我们可以轻松地单独破解每个密码

    现在鼓励开发人员利用自适应单向函数来存储密码。 使用自适应单向函数验证密码是有意占用大量资源的(它们有意使用大量 CPU、内存或其他资源)。 自适应单向功能允许配置一个“工作因子”,该因子可以随着硬件的改进而增长。 我们建议将“工作因子”调整为大约需要一秒钟来验证系统上的密码。 这种权衡是使攻击者难以破解密码,但又不会太昂贵,以免给您自己的系统带来过多的负担或激怒用户。

    Spring Security 试图为“工作因素”提供一个良好的起点,但我们鼓励用户为自己的系统自定义“工作因素”,因为性能因系统而异。 应该使用的自适应单向函数的示例包括 bcrypt, PBKDF2, scrypt 以及 argon2

    由于自适应单向函数有意占用大量资源,因此验证每个请求的用户名和密码可能会显著降低应用程序的性能。 Spring 安全性(或任何其他库)无法加快密码验证的速度,因为安全性是通过使验证资源密集来获得的。 建议用户将长期凭据(即用户名和密码)交换为短期凭据(例如会话和 OAuth 令牌等)。 可以快速验证短期凭据,而不会造成任何安全性损失。

    2.1.2 DelegatingPasswordEncoder委派密码编码器

    SS提供了自己的解决方案

    在 Spring Security 5.0 之前,默认的 PasswordEncoderNoOpPasswordEncoder(需要纯文本密码)。

    相当于“密码存储历史”部分的 “BCryptPasswordEncoder”。 但是,这忽略了三个现实世界的问题:

    • 许多应用程序使用无法轻松迁移(easily migrate)的旧密码。
    • 密码存储的最佳做法永远都在更新。
    • 作为一个框架,Spring Security不能经常进行重大更改。

    为了解决这些问题,Spring Security 引入了 “DelegatingPasswordEncoder”,它通过以下方式解决了所有问题:

    • 确保使用当前密码存储建议对密码进行编码
    • 允许验证现代和传统格式的密码
    • 允许未来升级编码方式

    以下是官方提供的案例https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html,只做了简单的搬运

    您可以使用PasswordEncoderFactories方法轻松构建一个DelegatingPasswordEncoder 的实例:

    1. 创建一个默认的委派密码编码器

      PasswordEncoder passwordEncoder =
          PasswordEncoderFactories.createDelegatingPasswordEncoder();
      
      • 1
      • 2
    2. 或者,可以创建一个定义的委派密码编码器

      String idForEncode = "bcrypt";
      Map encoders = new HashMap<>();
      encoders.put(idForEncode, new BCryptPasswordEncoder());
      encoders.put("noop", NoOpPasswordEncoder.getInstance());
      encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
      encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
      encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
      encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
      encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
      encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
      encoders.put("sha256", new StandardPasswordEncoder());
      
      PasswordEncoder passwordEncoder =
          new DelegatingPasswordEncoder(idForEncode, encoders);
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14

    2.1.3 SS的密码存储格式

    密码一般的存储格式如下(委派密码编码器的存储格式:

    {id}encodedPassword
    
    • 1
    • id是用于查找"PasswordEncoder"应使用的标识符

    • encodedPassword是所"PasswordEncoder"的原始编码密码。

    • id必须位于密码的开头,以 开头{,以 结尾}

    • 如果没有找到id,意味着id被设置为空

    以下具体举例,所有的原始密码都是“password”:

    {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
    {noop}password
    {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
    {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
    {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
    
    • 1
    • 2
    • 3
    • 4
    • 5

    显而易见,**密码前段的大括号{}作为标识符,存储的该密码的加密方式。**决定"PasswordEncoder"使用哪个来编码密码。

    Note:一些用户可能担心存储格式是为潜在的黑客提供的。这不是一个问题,因为密码的存储不依赖于算法的秘密。此外,大多数格式在没有前缀的情况下很容易被攻击者识别出来。例如,BCrypt 密码通常以" 2 a 2a 2a"。

    2.1.4 密码匹配问题

    对应的,密码匹配问题也是根据大括号中的``id来决定使用哪种PasswordEncoder`进行匹配的。

    默认情况下,执行方法matches(CharSequence, String)校验密码以及调用未映射的id(包括 null id)的结果为产生异常IllegalArgumentException

    可以使用以下方法进行定制化:

    DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)
    
    • 1

    通过使用"id",我们可以匹配任何密码编码,甚至使用最现代的密码编码对密码进行编码。

    这很重要,因为与加密不同,密码哈希的设计使得没有简单的方法可以恢复明文。由于无法恢复明文,因此很难迁移密码。为此我们选择了默认包含方便用户迁移的编码方式NoOpPasswordEncoder"(根据上面的案例可以发现,noop是就是明文),以简化入门体验

    image-20231007202303764

    2.1.5 快速体验SS的委派密码

    如果您正在制作演示或样本,那么花时间对用户的密码进行哈希处理会有点麻烦。有一些便利的机制可以使这变得更容易,但这仍然不适合生产。

    案例1:默认的密码编码案例(bcrypt)

    UserDetails user = User.withDefaultPasswordEncoder()
      .username("user")
      .password("password")
      .roles("user")
      .build();
    System.out.println(user.getPassword());
    // {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果需要创建多个用户,也是可以复用这个构建器(不需要创建新的实例)

    // 只创建一个UserBuilder
    UserBuilder users = User.withDefaultPasswordEncoder();
    // 构建不同的角色
    UserDetails user = users
      .username("user")
      .password("password")
      .roles("USER")
      .build();
    UserDetails admin = users
      .username("admin")
      .password("password")
      .roles("USER","ADMIN")
      .build();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    尽管这已经对存储的密码进行哈希处理,但密码仍然暴露在内存和编译的源代码中。

    因此,对于生产环境来说,它仍然不被认为是安全的。对于生产,您应该在外部对密码进行哈希处理hash your passwords externally),原文推荐使用SpringBoot CLI。

    2.1.6 常用故障排查

    2.1.6.1 IllegalArgumentException

    当存储的密码之一没有 id时,会发生以下错误

    java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
    
    • 1

    解决方法

    解决此问题的最简单方法是弄清楚您的密码当前是如何存储的,并明确提供正确的PasswordEncoder

    • 可以通过公开密码(noop)恢复到之前的加密方式。(个人理解就是跳过ss的用户加密,只套一个{noop}的壳

    • 给所有的密码上加上正确的前缀(已知加密方式的情况下)

      从
      $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
      改成
      {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
      
      • 1
      • 2
      • 3
      • 4

    详细的映射列表参考: PasswordEncoderFactories

    2.1.7 常见的案例

    简单总结一下:使用的方法大致是以下几个方法

    • 创建一个对应的编码器
    • 使用编码器的encode(String)方法得到SS的加密结果
    • 使用编码器的matches(原密码,加密后的密码)方法进行密码匹配
    2.1.7.1 BCryptPasswordEncoder

    广泛支持的 bcrypt 算法对密码进行哈希处理。 为了使其更能抵抗密码破解,bcrypt故意变慢。 与其他自适应单向函数一样,应将其调整为大约需要 1 秒来验证系统上的密码。

    推荐:根据系统硬件水平测试调整,设置密码强度使得校验密码需要约1秒。

    // Create an encoder with strength 16
    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
    String result = encoder.encode("myPassword");
    assertTrue(encoder.matches("myPassword", result));
    
    • 1
    • 2
    • 3
    • 4
    2.1.7.2 Argon2PasswordEncoder

    Argon2是密码哈希竞赛的获胜者。 为了防止自定义硬件上的密码破解,Argon2 是一种故意缓慢的算法,需要大量内存。 与其他自适应单向函数一样,应将其调整为大约需要 1 秒来验证系统上的密码。

    // 创建一个全默认的编码器
    Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
    String result = encoder.encode("myPassword");
    assertTrue(encoder.matches("myPassword", result));
    
    • 1
    • 2
    • 3
    • 4
    2.1.7.3 其他PasswrodEncoder

    还有大量其他 “PasswordEncoder” 实现完全是为了向后兼容而存在的。

    它们都已弃用,以指示它们不再被视为安全。 但是,没有计划删除它们,因为很难迁移现有的遗留系统。

    2.1.7.4 密码存储配置

    SS默认使用委派密码编码器。

    如果要使用原来的密码编码方式,就使用无操作密码编译器即可:

    @Bean
    public static NoOpPasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    // 声明一个"NoOpPasswordEncoder"的bean 名称为"passwordEncoder".
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.1.8 修改密码配置

    大多数应用不仅用户提交密码,还必须支持修改密码。

    正常我们需要提供一个端口给管理员进行密码管理,你可以通过配置SS来提供该端口,比如原来的应用中修改密码的端口是/change-password,你可以像这样配置SS:

    http.passwordManagement(Customizer.withDefaults())
    
    • 1

    如果不是/change-password,也可以像这样配置

    http
        .passwordManagement((management) -> management
            .changePasswordPage("/update-password")
        )
    
    • 1
    • 2
    • 3
    • 4

    具体用法如下:

    要使这段代码生效,您需要在Spring Security的配置文件中进行相应的配置。具体步骤如下:

    1. 打开Spring Security的配置文件(通常是SecurityConfig.javaSecurityConfig.kt)。

    2. 在配置文件中找到适当的位置,添加以下代码片段:

      http
          .passwordManagement(Customizer.withDefaults());
      
      • 1
      • 2

      或者,如果您使用XML配置:

      <http>
          <password-management customizer-ref="org.springframework.security.config.Customizer#withDefaults" />
      http>
      
      • 1
      • 2
      • 3
    3. 如果您的应用程序中的密码更改端点是/change-password,则无需进行其他配置。否则,您可以使用.changePassword().changePasswordUrl("/your-change-password-url")方法来指定自定义的密码更改端点URL。

    4. 保存并关闭配置文件。

    这样,当您的应用程序启动时,Spring Security将根据您的配置自动启用密码管理功能,并提供一个标准的密码更改URL供密码管理器使用。

  • 相关阅读:
    PDF转Markdown的开源工具解析
    C语言字符串函数和内存函数的介绍与模拟实现
    数据中心IT设备硬件智能化运维管理探索
    深入解析:批处理文件中EOF命令的妙用
    6.串口、时钟
    关于QTableWidget的it所占内存的释放问题
    对标 VSCode?JetBrains 下一代编辑器 Fleet
    使用 VirtualBox+Vagrant 创建 CentOS7 虚拟机
    K8S部署ECK采集日志
    统计学习方法03—朴素贝叶斯算法
  • 原文地址:https://blog.csdn.net/Xcong_Zhu/article/details/133810719