【学习笔记】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

【学习笔记】Spring Security 01 认识Spring Security的重要特征(Features)_第1张图片

学习地址: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>

属性参数

<properties>
	
	<spring-security.version>6.1.4spring-security.version>
	<spring.version>6.0.11spring.version>
properties>

其他引入方式查看官网: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();
    
  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);
    

2.1.3 SS的密码存储格式

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

{id}encodedPassword
  • 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

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

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

2.1.4 密码匹配问题

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

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

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

DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)

通过使用"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

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

// 只创建一个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();

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

因此,对于生产环境来说,它仍然不被认为是安全的。对于生产,您应该在外部对密码进行哈希处理(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"

解决方法

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

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

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

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

详细的映射列表参考: 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));
2.1.7.2 Argon2PasswordEncoder

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

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

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

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

2.1.7.4 密码存储配置

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

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

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

2.1.8 修改密码配置

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

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

http.passwordManagement(Customizer.withDefaults())

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

http
    .passwordManagement((management) -> management
        .changePasswordPage("/update-password")
    )

具体用法如下:

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

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

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

    http
        .passwordManagement(Customizer.withDefaults());
    

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

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

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

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

你可能感兴趣的:(学习笔记,spring,学习)