百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

使用Spring Security在进行用户身份认证时,密码如何存储详解

toyiye 2024-06-21 19:13 13 浏览 0 评论

Spring Security的PasswordEncoder接口用于执行密码单向的转换,以允许安全地存储密码。假定PasswordEncoder是单向转换,那么当密码转换需要双向转换时(例如,存储用于向数据库进行身份验证的凭证),就不需要使用它。通常,PasswordEncoder用于存储需要在身份验证时与用户提供的密码进行比较的密码。

密码存储历史

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

然后,开发人员被鼓励通过单向哈希(例如SHA-256)来存储密码。当用户试图进行身份验证时,将哈希密码与他们键入的密码的哈希进行比较。这意味着系统只需要存储密码的单向哈希。如果发生了入侵,那么只有密码哈希被暴露了。由于哈希是一种算法,而且根据哈希来猜测密码在计算上非常困难,因此不值得花力气去找出系统中的每个密码。为了破解这种方式,恶意用户决定创建称为彩虹表(https://en.wikipedia.org/wiki/Rainbow_table)的查找表。他们不是每次都猜测每个密码,而是计算一次密码并将其存储在一个查找表中。

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

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

现在鼓励开发人员利用自适应单向函数来存储密码。使用自适应单向函数进行密码验证是有意的资源密集型(即CPU、内存等)。一个自适应单向函数允许配置一个“工作因子”,它可以随着硬件变得更好而增长。建议将“工作因子”调整为在您的系统上花费大约1秒的时间来验证密码。这样做的好处是让攻击者很难破解密码,但又不至于让自己的系统负担过重。Spring Security试图为“工作因子”提供一个良好的起点,但是鼓励用户为自己的系统定制“工作因子”,因为不同系统的性能会有很大的不同。应该使用的自适应单向函数示例包括bcrypt、PBKDF2、scrypt和argon2。

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

DelegatingPasswordEncoder

在Spring Security 5.0之前,默认的PasswordEncoder是NoOpPasswordEncoder,它需要纯文本密码。根据密码历史部分,您可能认为默认的PasswordEncoder现在类似于BCryptPasswordEncoder。

然而,这忽略了三个现实问题:

  1. 有许多应用程序使用不能轻易迁移的旧密码编码
  2. 密码存储的最佳实践将再次更改。
  3. 作为一个框架,Spring Security不能频繁地进行破坏性更改

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

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

您可以使用PasswordEncoderFactories轻松地构造DelegatingPasswordEncoder的实例。

示例:创建默认DelegatingPasswordEncoder

PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();

或者,您可以创建自己的自定义实例。例如:

创建自定义DelegatingPasswordEncoder

String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(idForEncode, encoders);

密码存储格式

密码的一般格式为:

DelegatingPasswordEncoder存储格式

{id}encodedPassword

这样,id是用于查找应该使用哪个PasswordEncoder的标识符,而encodedPassword是所选PasswordEncoder的原始编码密码。id必须在密码的开头,以{开始,以}结束。如果找不到id,则id为null。例如,下面可能是使用不同id编码的密码列表。所有的原始密码都是“password”。

示例:DelegatingPasswordEncoder编码密码示例

{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. 第一个密码的密码编码id为bcrypt,编码密码为$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG。匹配时,它将委托给BCryptPasswordEncode
  2. 第二个密码将有一个PasswordEncoder id为noop和一个编码密码为password。匹配时,它将委托给NoOpPasswordEncoder
  3. 第三个密码的密码编码器id为pbkdf2,编码密码为5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc。匹配时,它将委托给Pbkdf2PasswordEncoder
  4. 第四个密码的密码编码id为scrypt,编码密码为$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=。匹配时,它将委托给SCryptPasswordEncoder
  5. 最终密码的密码编码id为sha256,编码密码为97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfff8410849f27605abcbc0。匹配时,它将委托给StandardPasswordEncoder

一些用户可能会担心存储格式是为潜在的黑客提供的。这不是一个问题,因为密码的存储并不依赖于算法是秘密的。此外,对于攻击者来说,如果没有前缀,大多数格式都很容易识别。例如,BCrypt密码通常以$2a$开头。

密码编码

传入构造函数的idForEncode确定了将使用哪个PasswordEncoder对密码进行编码。在我们上面构造的DelegatingPasswordEncoder中,这意味着编码密码的结果将被委托给BCryptPasswordEncoder,并以{bcrypt}作为前缀。

最终结果如下:

DelegatingPasswordEncoder编码的例子

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

密码匹配

匹配是基于{id}和id到构造函数中提供的PasswordEncoder的映射来完成的。我们的密码存储格式示例提供了如何实现此功能的示例。默认情况下,调用带有密码和没有映射的id(包括null id)的匹配(CharSequence, String)的结果将导致IllegalArgumentException。可以使用DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)自定义此行为。

通过使用id,我们可以匹配任何密码编码,但是使用最现代的密码编码编码密码。这一点很重要,因为与加密不同,密码哈希被设计成没有简单的方法来恢复明文。由于没有办法恢复明文,这使得迁移密码变得困难。虽然迁移NoOpPasswordEncoder对用户来说很简单,但我们选择在默认情况下包含它,以简化入门体验。

开始体验

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

示例:withDefaultPasswordEncoder

User user = User.withDefaultPasswordEncoder()
  .username("user")
  .password("password")
  .roles("user")
  .build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

如果要创建多个用户,还可以重用构建器。

示例:withdefaultpasswordencoder重用构建器

UserBuilder users = User.withDefaultPasswordEncoder();
User user = users
  .username("user")
  .password("password")
  .roles("USER")
  .build();
User admin = users
  .username("admin")
  .password("password")
  .roles("USER","ADMIN")
  .build();

这将哈希存储的密码,但密码仍然暴露在内存和已编译的源代码中。

因此,对于生产环境来说,它仍然不安全。对于生产,您应该在外部散列您的密码。

用Spring Boot CLI编码

正确编码密码的最简单方法是使用Spring Boot CLI(https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-cli.html)。

例如,下面的代码将对password的密码进行编码,以便与DelegatingPasswordEncoder一起使用:

Spring Boot CLI 编码密码使用实例

spring encodepassword password
{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6

故障排除

当存储的其中一个密码不具有密码存储格式中描述的id时,会发生以下错误。

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
    at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233)
    at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)

解决该错误的最简单方法是切换到显式提供编码密码的PasswordEncoder。解决这个问题最简单的方法是找出你的密码目前是如何被存储的,并明确地提供正确的密码编码器。

如果您从Spring Security 4.2.x迁移。您可以通过公开的NoOpPasswordEncoder bean恢复到以前的行为。

或者,您可以使用正确的id作为所有密码的前缀,并继续使用DelegatingPasswordEncoder。例如,如果你正在使用BCrypt,你会迁移你的密码从一些类似:

$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

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

有关映射的完整列表,请参考PasswordEncoderFactories上的Javadoc(https://docs.spring.io/spring-security/site/docs/5.0.x/api/org/springframework/security/crypto/factory/PasswordEncoderFactories.html)。

BCryptPasswordEncoder

BCryptPasswordEncoder实现使用广泛支持的bcrypt算法对密码进行哈希。为了使其更能抵抗密码破解,bcrypt故意放慢速度。与其他自适应单向函数一样,应该将其调整为在系统上花费大约1秒的时间验证密码。BCryptPasswordEncoder的默认实现使用强度10。建议您在自己的系统上调优和测试强度参数,以便验证密码大约需要1秒。

示例:BCryptPasswordEncoder

// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

Argon2PasswordEncoder

Argon2PasswordEncoder实现使用Argon2(https://en.wikipedia.org/wiki/Argon2)算法对密码进行哈希。Argon2是密码哈希竞赛的获胜者(https://en.wikipedia.org/wiki/Password_Hashing_Competition)。为了在自定义硬件上破解密码,Argon2是一个需要大量内存的缓慢算法。与其他自适应单向函数一样,应该将其调整为在系统上花费大约1秒的时间验证密码。Argon2PasswordEncoder的当前实现需要BouncyCastle。

示例:Argon2PasswordEncoder

// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder实现使用PBKDF2(https://en.wikipedia.org/wiki/PBKDF2)算法对密码进行哈希。阻止密码破解的,PBKDF2是一种故意很慢的算法。与其他自适应单向函数一样,应该将其调整为在系统上花费大约1秒的时间验证密码。当需要FIPS认证时,这种算法是一个很好的选择。

示例:Pbkdf2PasswordEncoder

// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

SCryptPasswordEncoder

SCryptPasswordEncoder实现使用scrypt(https://en.wikipedia.org/wiki/Scrypt)算法对密码进行哈希。为了阻止在定制硬件scrypt上的密码破解,这是一个故意慢的算法,需要大量的内存。与其他自适应单向函数一样,应该将其调整为在系统上花费大约1秒的时间验证密码。

示例:SCryptPasswordEncoder

// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

其他PasswordEncoders

还有很多其他的PasswordEncoder实现完全是为了向后兼容而存在的。它们都已弃用,以表明它们不再被认为是安全的。然而,由于很难迁移现有的遗留系统,因此没有删除它们的计划。

密码存储配置

Spring Security默认使用DelegatingPasswordEncoder。但是,这可以通过将PasswordEncoder公开为Spring bean来进行定制。

如果您从Spring Security 4.2.x迁移。您可以通过公开NoOpPasswordEncoder bean恢复到以前的行为。

恢复到NoOpPasswordEncoder被认为是不安全的。相反,您应该迁移到使用DelegatingPasswordEncoder来支持安全的密码编码。

示例:NoOpPasswordEncoder

@Bean
public static NoOpPasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}

XML配置要求NoOpPasswordEncoder bean名称为passwordEncoder。

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码