Bootiful Spring Boot 3.4: Spring Security

工程 | Josh Long | 2024 年 11 月 24 日 | ...

Spring Security 6.4.1 是你处理身份验证和授权的“一站式商店”,而这个版本简直是重磅炸弹!发行说明充满了可能性

发行说明是骗人的!

我的意思是,它们并非全然是谎言。只是它们没有很好地捕捉和传达这个版本有多么出色。这个版本用户可见的新功能比以往许多版本都要多。这可能是我最喜欢的 Spring Security 版本,至少自从它开始拥有 Java 配置 DSL 以来是这样!

看看那些发布说明。看到了那些关于无密码认证一次性令牌登录的简短章节了吗?是的。那是谎言。这些东西应该有它们自己的篇章!我们会回过头来谈论它们。我保证。我们先快速看一下其他部分。太多的不同部分了。

  • 在方法安全性和Spring Security组件模型方面取得了巨大改进,包括对Spring Framework的元注解机制和注解模板的改进支持。
  • AOT和GraalVM原生镜像应用程序现在可以与Spring Security的@AuthorizeReturnObject功能正确配合,并且可以在@PreAuthorize@PostAuthorize生命周期回调中引用Bean。
  • 一个方便的SecurityAnnotationScanners API提供了一种扫描安全注解的方式,可以将Spring Security的选择和模板功能添加到自定义注解中。
  • OAuth 2.0支持变得更好了!oauth2Login()方法现在接受OAurth2AuthorizationRequestResolver作为@Bean
  • ClientRegistrations现在支持外部获取的配置。
  • 响应式Spring Security DSL现在支持login page()
  • OIDC后通道支持现在接受logout+jwt类型的退出令牌
  • 现在可以使用OAuth2ClientHttpRequestInterceptor配置Spring RestClient来发出受保护的资源请求。在发出HTTP请求时,您可以让它向下游服务提供您的令牌。
  • 令牌交换现在支持刷新令牌
  • SAML 2.0支持也取得了巨大飞跃!OpenSAML 5支持现已推出。EntityIDs的registrationId已简化。
  • 断言方现在可以根据元数据的过期时间在后台进行刷新。
  • 现在可以签署信赖方元数据
  • 为了与SAML 2.0标准保持一致,元数据端点也使用application/samlmetadata+xml MIME类型。
  • 在普通的Spring Web应用程序中,我们支持CSRF BREACH令牌。
  • 以及更多可定制的Remember Me cookie
  • Spring Security过滤器链会标记更多无效配置。
  • ServerHttpSecurity现在将ServerWebExchangeFirewall对象作为Bean进行拾取。
  • 此版本还为授权、身份验证和请求观察分别带来了改进的、更细粒度的可观察性集成。
  • AclAuthorizationStrategyImpl支持RoleHierarchy类型,这也是一个相当新的类型!
  • Kotlin支持也得到了显著改进!
  • 从技术上讲,Spring Authorization Server不是Spring Security的一部分,但为了简单起见,我在此提及。它包含了许多新功能,包括一个明显更一致、更简洁的DSL配置。在一个典型的Spring Security应用程序中,只需一行Java代码和一些属性或YAML文件中的配置行,就可以让一个完整的OAuth IDP正常运行。太棒了。

现在,让我们回到关于无密码认证和一次性令牌的讨论。

更好的密码

让我们来看一个简单的应用程序。

我们将有一个需要锁定的HTTP控制器

package com.example.bootiful_34.security;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.security.Principal;
import java.util.Map;

@Controller
@ResponseBody
class SecuredController {

	@GetMapping("/admin")
	Map<String, String> admin(Principal principal) {
		return Map.of("admin", principal.getName());
	}

	@GetMapping("/")
	Map<String, String> hello(Principal principal) {
		return Map.of("user", principal.getName());
	}

}

要锁定它,我们需要定义一个SecurityFilterChain,其中包含一些常规项:HTTP表单登录、一些授权规则等。


	@Bean
	SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
				.authorizeHttpRequests(requests -> requests
                        .requestMatchers("/admin").hasRole("ADMIN")
                        .requestMatchers("/error").permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin(Customizer.withDefaults())
                // ...
                .build();
	}

我们需要让Spring Security了解我们系统中的用户,所以让我们提供一个UserDetailsService

package com.example.bootiful_34.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@ImportRuntimeHints(UiResourcesRuntimeHintsRegistrar.class)
class SecurityConfiguration {

	@Bean
	UserDetailsService userDetailsService() {
		var josh = User.withUsername("josh").password("pw").roles("USER").build();
		var rob = User.withUsername("rob").password("pw").roles("USER", "ADMIN").build();
		return new InMemoryUserDetailsManager(josh, rob);
	}
	
	// ...
	
}

这个应用程序有两个用户:joshrobjosh只有一个角色USER,而rob同时拥有ADMINUSER

在非平凡的应用程序中,您应该使用一个指向身份提供者的替代`UserDetailsService`实现。或者,至少,一个不存储纯文本密码的SQL数据库。我在这里注册用户和他们的密码,但一个设计良好的系统将尽可能减少密码的使用。您关于密码的常识性智慧可能错了。美国国家标准与技术研究所(美国商务部的一个组织)去年(2024年)发布了其更新的密码定义指南

NIST特别出版物800-63B提供了关于创建、处理、续订和存储密码(称为“记忆秘密”)的具体指导,并概述了对一次性令牌和基于硬件的身份验证器(例如,YubiKeys/WebAuthn)等替代方案的建议。让我们来看看他们的一些建议。

创建足够强的密码

如果您的系统需要密码, there’s a lot to know. 幸运的是,Spring Security 可以为您完成以上大部分(甚至全部?)的指导。

用户选择的密码必须至少八个字符长,由CSP(凭证服务提供商)随机生成的密码必须至少六个字符长。不鼓励复杂性规则(例如,要求大小写混合、数字和符号)。密码不得被任意限制(例如,限制最大长度或排除某些字符)。密码必须与常用、泄露或预期密码的列表进行比对(例如,字典单词、重复模式、服务特定术语)。如果密码被标记为弱密码,用户必须选择一个更健壮的替代方案。建议使用密码强度计或反馈来帮助用户创建健壮的密码。该指南建议允许复制粘贴功能,以鼓励使用密码管理器,并提供输入的密码的可选显示,以最大程度地减少输入错误。该文档建议将连续登录失败次数限制在不超过100次,并实施验证码、增加延迟时间或其他自适应措施,以防止因滥用导致账户被锁定。有趣的是,它建议重新验证策略应根据保证级别而有所不同(例如,高保证级别需要每12小时或30分钟无活动后重新验证)。另一方面,密码更改应被任意或定期要求。只有在有证据表明存在泄露的情况下,才应强制更改。它建议使用单向密钥派生函数(例如,PBKDF2、BCrypt或Argon2)对密码进行哈希和加盐处理。Spring Security默认使用BCrypt。

最好的密码是没有密码

NIST建议使用一次性令牌(OTP)等密码替代方案。单因素OTP设备生成基于时间或基于计数器的OTP。OTP必须具有密码学安全性,并具有至少20位的熵。多因素OTP设备在生成OTP之前需要第二个身份验证因素(例如,PIN或生物特征)。带外身份验证利用辅助通信渠道(例如,移动设备)进行OTP传递或确认,这需要安全的渠道并限制弱方法。还鼓励使用基于硬件的身份验证器(例如,YubiKeys、WebAuthn)。WebAuthn是FIDO联盟的一部分,并被鼓励作为一种健壮且抗网络钓鱼的身份验证方法。像YubiKeys这样的硬件身份验证器使用密码学协议来确保安全。多因素密码学设备必须结合“你拥有的东西”(例如,YubiKey)与“你知道的东西”(例如,PIN)或“你是什么”(例如,生物特征)。密钥应保留在防篡改硬件内,并且无法提取。

通过优先考虑这些方法,NIST强调了向多因素身份验证(MFA)和密码学解决方案过渡,作为比传统密码更安全的选择。

使用一次性令牌(OTT)实现无密码登录

因此,在Spring Security应用程序的上下文中,一次性令牌通过依赖于只有用户自己才拥有的带外因素来验证用户——也许是用户接收短信的能力、访问其电子邮件等。您可能以前在其他地方使用过此功能。您访问一个网站,输入您的用户名,然后该网站会向您发送一封包含您可以点击登录的链接的电子邮件。这些有时被称为“魔法链接”。

Spring Security不提供与您喜欢的电子邮件提供商或消息应用程序的集成。您可以使用Sendgrid、Twilio或数百万个其他服务中的任何一个来实现。但Spring Security确实提供了创建和验证链接的基础架构。

这是我们在Spring Security配置中必须添加的一小段代码,以使其能够打印


	@Bean
	SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
				// ..
				.oneTimeTokenLogin(configurer -> configurer.tokenGenerationSuccessHandler(
                        (request, response, oneTimeToken) -> {
                            var msg = "go to https://:8080/login/ott?token=" + oneTimeToken.getTokenValue();
                            System.out.println(msg);
                            response.setContentType(MediaType.TEXT_PLAIN_VALUE);
                            response.getWriter().print("you've got console mail!");
                        }))
                // ..                        
                .build();
	}

看到了吗?这只是一个单独的lambda表达式,您可以在其中提供足够的信息来创建并向用户发送一个链接,一旦用户点击该链接,就可以让他们登录。简单!在这个例子中,我只是通过点击控制台中的链接来登录。同样,您可能发送电子邮件或其他更实际的方式。

这非常方便,并让用户不必担心或——更糟糕的是——共享两个密码。希望他们已经锁定了他们的电子邮件密码!我宁愿他们有一个不跨网站共享的良好密码,而不是十几个糟糕且共享的密码。

另一种方法——另一层安全性——可能比文本链接更复杂,以阻止潜在的黑客接触到。例如指纹、面部ID扫描或独立的硬件加密狗。问题是:如何在需要的地方集成这些东西?为了让它们正常工作,我们需要修改浏览器、服务器端处理逻辑、操作系统等,以便它们都能说同一种标准协议。

您会很高兴地知道,这几乎是每个人都在做的事情。该协议名为WebAuthn,其背后的组织名为FIDO Alliance。FIDO Alliance得到了普遍支持!以下是其一些最杰出的成员。名单包括DELL、Apple、Google、Intuit、NTT DOCOMO、Microsoft、meta、LastPass、DashLane、Bank Of America、1Password、Intel、CISCO、CVS Health、American Express、VISA,以及无数其他公司。关键是,那些在乎金钱的人都在支持这种方法。浏览器也是如此!所有主流浏览器——Chrome、Safari、Edge、Firefox、iOS上的Safari、Android上的Chrome、iOS上的Chrome以及iOS上的Edge——都支持它。它无处不在!现在,多亏了Spring Security的这个版本,它也可以轻松地集成到您的应用程序中。

以下是我们Spring Security过滤器链示例中的相关配置


	@Bean
	SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                // ... 
                .webAuthn(c -> c
                        .rpId("localhost")
                        .rpName("bootiful passkeys")
                        .allowedOrigins("https://:8080")
                )
                // ...
                .build();
	}

重启应用程序,然后登录到localhost:8080/webauthn/register。注册您的无密码认证。我在Apple生态系统中,所以它提示我在iPhone上进行FaceID,或者在我的macOS Apple Silicon笔记本电脑上使用TouchID。然后,浏览器将无密码认证存储在操作系统钥匙串中。它现在已通过iCloud进行联合。所以,我不仅有了一种有效的登录方式,而且它还与我的iCloud帐户绑定,因此我可以在一台设备上使用Face ID,在另一台设备上使用Touch ID登录。无需额外工作!

要查看效果,请注销:localhost:8080/logout

安全性不仅非常出色,而且对用户来说也更轻松!这不是很酷吗?

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

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

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

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

查看所有