领先一步
VMware 提供培训和认证,助您加速进步。
了解更多我谨代表社区,很高兴地宣布 Spring Security 5.0.0.RC1 版本发布。此版本解决了 150 多个 问题。以下是此版本的主要亮点:
WebFlux 安全亮点
OAuth 2.0 亮点
核心亮点
之前,Spring Security 将 ServerWebExchange.getPrincipal() 用作已认证用户信息的唯一来源。已认证用户会被复制到 Reactor 的 Context 中,以支持以 Reactor Context 作为其唯一来源的方法安全。拥有多个唯一来源显然不是理想的做法。
Spring Security 现在使用 Reactor 的 Context 作为已认证用户信息的唯一来源。用户仍然可以从 ServerWebExchange.getPrincipal() 访问,但该值也来自 Reactor 的 Context。
您可以使用 ReactiveSecurityContextHolder 将 SecurityContext 读取和写入 Reactor 的 Context。例如,这演示了如何检索当前登录用户的消息。
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 登录 功能上,进行了许多更新和完善,以使其达到最终效果。我们非常期待在接下来的几周内将此功能发布给 Spring 社区。
除了更新之外,还添加了一些新功能:
OAuth 2.0 登录的 Spring Boot 2.0 自动配置
通过 sub 声明验证,防止在 UserInfo 响应 中出现令牌替换攻击
通过 ImplicitGrantConfigurer 支持 隐式授权
OAuth2AuthorizedClient 代表一个已授权客户端。当最终用户(资源所有者)授予客户端访问其受保护资源的授权时,该客户端就被视为“已授权”。此类用于将 OAuth2AccessToken 与 ClientRegistration (客户端)和资源所有者(即授予授权的 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";
}
}
我们将为 OAuth2AuthorizedClient 和 OAuth2AccessToken 提供 HandlerMethodArgumentResolver 的实现。
作为直接使用 OAuth2AuthorizedClientService 的替代方案,您将能够将 OAuth2AuthorizedClient 或 OAuth2AccessToken 解析为 @Controller 方法参数。
我们很快将开始规划我们的功能列表,以提供对OAuth 2.0 资源服务器角色的支持,敬请期待。
密码存储进行了重大改造,以提供更安全的默认设置和迁移密码存储方式的能力。默认的 PasswordEncoder 现在是 DelegatingPasswordEncoder,这是一个非被动更改。此更改可确保密码现在默认使用 BCrypt 进行编码,允许验证旧格式的密码,并允许将来升级密码存储。
您可以使用 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)
第一个密码将具有 PasswordEncoder id bcrypt 和 encodedPassword $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG。匹配时,它将委托给 BCryptPasswordEncoder。
第二个密码将具有 PasswordEncoder id noop 和 encodedPassword password。匹配时,它将委托给 NoOpPasswordEncoder。
第三个密码将具有 PasswordEncoder id pbkdf2 和 encodedPassword 5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc。匹配时,它将委托给 Pbkdf2PasswordEncoder。
第四个密码将具有 PasswordEncoder id scrypt 和 encodedPassword $e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=。匹配时,它将委托给 SCryptPasswordEncoder。
最后一个密码将具有 PasswordEncoder id sha256 和 encodedPassword 97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0。匹配时,它将委托给 StandardPasswordEncoder。
传递到构造函数中的 idForEncode 决定了将使用哪个 PasswordEncoder 来编码密码。在我们上面构造的 DelegatingPasswordEncoder 中,这意味着对 password 进行编码的结果将委托给 BCryptPasswordEncoder,并带有 {bcrypt} 前缀。最终结果将是:
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
匹配是基于 id 以及构造函数中提供的 id 到 PasswordEncoder 的映射来完成的。我们在 密码存储格式 中的示例提供了一个工作示例,说明了如何完成此操作。默认情况下,调用 matches(CharSequence, String) 时,如果密码和未映射的 id(包括 null id)一起使用,将导致 IllegalArgumentException。此行为可以通过 DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder) 进行自定义。
通过使用 id,我们可以匹配任何密码编码,但使用最新密码编码来编码密码。这一点很重要,因为与加密不同,密码哈希的设计方式使得无法简单地恢复明文。由于无法恢复明文,因此很难迁移密码。虽然用户迁移 NoOpPasswordEncoder 很简单,但我们将其默认包含进来,以简化入门体验。
如果您没有使用显式 PasswordEncoder 或依赖旧的核心 PasswordEncoder,则需要进行迁移。
如果您的密码以明文形式存储,升级哈希就像获取明文密码并对其进行编码一样简单。
String encoded = passwordEncoder.encode(plainTextPassword);
如果您的密码以其他格式存储,则无法更新哈希。要迁移这些密码,您必须确定存储密码的算法,并将所有密码都加上 {id} 前缀。例如,如果密码是用 sha256 哈希的
97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
请确保所有密码都加上 {sha256} 前缀,例如:
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
虽然此密码未以安全格式存储,但它允许将其他密码以安全格式存储。我们也可以要求用户更改密码,这将更新其存储的哈希格式。
对于细心的读者来说,可以明显看出,通过在明文密码前加上 {noop} 来前缀,也可以迁移明文密码。例如,对于密码
password
您可以简单地在密码前加上 {noop},例如:
{noop}password
这将起作用,但不安全,因此不推荐在生产环境中使用。
如果您之前使用了核心中已弃用且过时的 PasswordEncoder,它已被移除,因为它要求用户提供 salt 并使用 SaltSource(也已移除)。核心中的每个 PasswordEncoder 实现都已迁移到新的加密 API,并在 Javadoc 中提供了迁移说明。例如 MessageDigestPasswordEncoder。
尽管不安全,但用户可以通过提供 NoOpPasswordEncoder 作为 @Bean 来恢复到以前的行为。如果应用程序利用了 AuthenticationManagerBuilder,则必须在 AuthenticationManagerBuilder 中显式提供 NoOpPasswordEncoder。例如,如果您有:
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 来更新用户密码的格式。