抢先一步
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 社区。
除了更新之外,下面还增加了一些新功能
Spring Boot 2.0 对 OAuth 2.0 登录的自动配置
使用 `sub` 声明验证,防止对 UserInfo Response 进行令牌替换攻击
通过 `ImplicitGrantConfigurer` 支持 隐式授权 (Implicit Grant)
OAuth2AuthorizedClient
代表一个授权客户端 (Authorized Client)。当最终用户(资源所有者 Resource Owner)授予客户端访问其受保护资源的授权时,客户端被视为“已授权”。此类用于将 OAuth2AccessToken
与 ClientRegistration
(客户端 Client)以及授予授权的 Principal
最终用户(资源所有者 Resource Owner)关联起来。
OAuth2AuthorizedClientService
的主要作用是管理 OAuth2AuthorizedClient
实例。从开发人员的角度来看,它提供了查找与客户端关联的 OAuth2AccessToken
的能力,以便可以使用该令牌向资源服务器 (Resource Server)发起请求。
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 资源服务器 (Resource Server) 角色提供支持,敬请期待。
密码存储经过了重大改进,以提供更安全的默认设置和迁移密码存储方式的能力。默认的 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
的映射来完成的。我们在密码存储格式中的示例提供了一个关于如何完成此操作的实际示例。默认情况下,使用密码和未映射的 id
(包括 null id)调用 matches(CharSequence, String)
的结果将导致 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
,则必须将 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 更新用户密码的格式。