Spring Security 5.0.0.RC1 发布

发布 | Rob Winch | 2017年11月1日 | ...

我代表社区,很高兴地宣布 Spring Security 5.0.0.RC1 的发布。此版本解决了 150 多个 问题。以下是此版本的亮点

ReactiveSecurityContextHolder

以前,Spring Security 使用 ServerWebExchange.getPrincipal() 作为已认证用户的真实来源。已认证的用户被复制到 Reactor 的 Context 中以支持方法安全,后者使用 Reactor Context 作为其真实来源。拥有多个真实来源显然不是理想的。

Spring Security 现在使用 Reactor 的 Context 作为已认证用户的真实来源。用户仍然可以从 ServerWebExchange.getPrincipal() 中访问,但此值也来自 Reactor 的 Context。

您可以使用 ReactiveSecurityContextHolder 读取和写入 Reactor 的 Context 中的 SecurityContext。例如,这演示了如何检索当前登录用户的邮件。

Authentication authentication =
    new TestingAuthenticationToken("user", "password", "ROLE_USER");

Mono<String> messageByUsername = ReactiveSecurityContextHolder.getContext()
  .map(SecurityContext::getAuthentication)
  .map(Authentication::getName)
  .flatMap(this::findMessageByUsername)
  // In a WebFlux application the `subscriberContext`
  // is automatically setup using `ReactorContextWebFilter`
  .subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication));

StepVerifier.create(messageByUsername)
	.expectNext("Hi user")
	.verifyComplete();

其中 this::findMessageByUsername 定义为

Mono<String> findMessageByUsername(String username) {
	return Mono.just("Hi " + username);
}

OAuth 2.0 客户端支持

已经进行了许多更新和改进,以完成新的**OAuth 2.0 登录**功能。我们非常高兴能够在接下来的几周内将其发布给 Spring 社区。

除了更新之外,还添加了一些新功能

OAuth2AuthorizedClient / Service

OAuth2AuthorizedClient 代表一个**授权客户端**。当最终用户(资源所有者)已授予客户端访问其受保护资源的授权时,客户端被认为是“已授权的”。此类用于将OAuth2AccessTokenClientRegistration(客户端)和资源所有者关联,资源所有者是授予授权的Principal最终用户。

OAuth2AuthorizedClientService的主要作用是管理OAuth2AuthorizedClient实例。从开发人员的角度来看,它提供了查找与客户端关联的OAuth2AccessToken的功能,以便可以将其用于向资源服务器发起请求。

OAuth2AuthorizedClientService可以注册为ApplicationContext中的@Bean(尽管不是必需的),以便开发人员可以查找与客户端关联的OAuth2AccessToken

例如

@Controller
public class GoogleCalendarController {

	@Autowired
	private OAuth2AuthorizedClientService authorizedClientService;

	@RequestMapping("/calendar")
	public String calendar(OAuth2AuthenticationToken authentication) {
		OAuth2AuthorizedClient authorizedClient =
			this.authorizedClientService.loadAuthorizedClient(
				"google", authentication.getName());

		OAuth2AccessToken accessToken = authorizedClient.getAccessToken();

		// ...

		return "calendar";
	}
}

OAuth 2.0 支持的下一步是什么?

HandlerMethodArgumentResolver(s)

我们将为OAuth2AuthorizedClientOAuth2AccessToken提供HandlerMethodArgumentResolver的实现。

作为直接使用OAuth2AuthorizedClientService的替代方案,您将能够将OAuth2AuthorizedClientOAuth2AccessToken解析为@Controller方法参数。

资源服务器支持

很快我们将开始计划我们的功能日志以提供对**OAuth 2.0 资源服务器**角色的支持,敬请期待。

密码存储已更新

密码存储经历了重大改版,以提供更安全的默认设置和迁移密码存储方式的能力。默认的PasswordEncoder现在是DelegatingPasswordEncoder,这是一个非被动更改。此更改确保密码现在默认使用 BCrypt 进行编码,允许验证旧格式的密码,并允许将来升级密码存储。

构建 DelegatingPasswordEncoder

您可以使用PasswordEncoderFactories轻松构建一个实例。

PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();

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

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);

密码存储格式

密码的通用格式为

{id}encodedPassword

其中id是用于查找应使用哪个PasswordEncoder的标识符,encodedPassword是为所选PasswordEncoder编码的原始密码。id必须位于密码的开头,以{开头,以}结尾。如果找不到id,则id将为null。例如,以下可能是使用不同id编码的密码列表。所有原始密码均为“password”。

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG (1)
{noop}password (2)
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc (3)
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=  (4)
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 (5)
  1. 第一个密码的PasswordEncoder id 为bcryptencodedPassword$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG。匹配时,它将委托给BCryptPasswordEncoder

  2. 第二个密码的PasswordEncoder id 为noopencodedPasswordpassword。匹配时,它将委托给NoOpPasswordEncoder

  3. 第三个密码的 PasswordEncoder id 为 pbkdf2,编码后的密码为 5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc。匹配时将委托给 Pbkdf2PasswordEncoder

  4. 第四个密码的 PasswordEncoder id 为 scrypt,编码后的密码为 $e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=。匹配时将委托给 SCryptPasswordEncoder

  5. 最后一个密码的 PasswordEncoder id 为 sha256,编码后的密码为 97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0。匹配时将委托给 StandardPasswordEncoder

密码编码

传递给构造函数的 idForEncode 决定了哪个 PasswordEncoder 将用于编码密码。在我们上面构建的 DelegatingPasswordEncoder 中,这意味着 password 的编码结果将委托给 BCryptPasswordEncoder 并以 {bcrypt} 为前缀。最终结果将如下所示

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

密码匹配

匹配是根据 id 以及构造函数中提供的 idPasswordEncoder 的映射关系进行的。我们在 密码存储格式 中的示例提供了如何执行此操作的工作示例。默认情况下,使用密码和未映射的 id(包括空 id)调用 matches(CharSequence, String) 的结果将导致 IllegalArgumentException。此行为可以使用 DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder) 进行自定义。

通过使用 id,我们可以匹配任何密码编码,但可以使用最新的密码编码来编码密码。这一点很重要,因为与加密不同,密码哈希的设计使得无法以简单的方式恢复明文。由于无法恢复明文,因此难以迁移密码。虽然用户可以轻松迁移 NoOpPasswordEncoder,但我们选择默认包含它,以便简化入门体验。

迁移到 Spring Security 5

如果您没有使用显式的 PasswordEncoder 或依赖于旧的核心 PasswordEncoder,则需要进行迁移。

迁移现有密码

如果您的密码以明文形式存储,则升级哈希就像获取明文密码并对其进行编码一样简单。

String encoded = passwordEncoder.encode(plainTextPassword);

如果您的密码以其他格式存储,则无法更新哈希。要迁移这些密码,您必须确定密码存储的算法,并为所有密码添加 {id} 前缀。例如,如果密码使用 sha256 哈希

97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

确保所有密码都以 {sha256} 为前缀,例如

{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

虽然此密码未以安全格式存储,但它允许其他密码以安全格式存储。我们还可以要求用户更改密码,这将更新其存储的哈希值。

对于敏锐的读者,您可能很清楚,您还可以通过为明文密码添加 {noop} 前缀来迁移它们。例如,对于密码

password

您可以简单地为密码添加 {noop} 前缀,例如

{noop}password

这将起作用,但它**不安全**,因此不建议在生产环境中使用。

PasswordEncoder 和 SaltSource

如果您在核心代码中使用旧的和已弃用的 PasswordEncoder,则已将其删除,因为它要求用户提供盐并使用 SaltSource(也已删除)。核心中的每个 PasswordEncoder 实现都已迁移到新的加密 API,Javadoc 中提供了迁移说明。一个示例是 MessageDigestPasswordEncoder

恢复到以前的行为(不安全)

虽然不安全,但用户可以通过提供 NoOpPasswordEncoder 作为 @Bean 来恢复到以前的行为。如果应用程序利用 AuthenticationManagerBuilder,则必须将 NoOpPasswordEncoder 显式提供给 AuthenticationManagerBuilder。例如,如果您有

auth
	.inMemoryAuthentication()
		.withUser("user").password("password").roles("USER");

您可以使用以下方法恢复到以前的行为

auth
	.inMemoryAuthentication()
		.passwordEncoder(NoOpPasswordEncoder.getInstance())
		.withUser("user").password("password").roles("USER");

入门体验

如果您正在构建演示或示例,则花费时间来哈希用户密码会有些麻烦。有一些便捷机制可以使此操作更容易,但这仍然不适用于生产环境。

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

如果您正在创建多个用户,还可以重复使用构建器。

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();

这确实对存储的密码进行了哈希处理,但密码仍会暴露在内存中以及编译后的源代码中。因此,对于生产环境而言,它仍然不被认为是安全的。对于生产环境,您应该在外部哈希您的密码。

自动密码迁移(未来)

使用 DelegatingPasswordEncoder,我们将能够在用户身份验证后更新密码格式。我们可以向 PasswordEncoder 添加一个默认方法,该方法返回一个类型(即 PasswordMatch),用于说明密码是否匹配。当密码匹配且使用旧格式时,PasswordMatch 还将包含密码的最新编码的成员。当 Spring Security 看到新格式的建议时,它可以使用 API 更新用户密码的格式。

项目站点 | 参考 | 帮助

获取 Spring 新闻通讯

与 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部