美妙的 Spring Boot 3.4:Spring Security

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

Spring Security 6.4.1 是您处理认证和授权项的一站式商店,而且这个版本真是太棒了(doozie)!发布说明 充满可能性

发布说明是个谎言!

我的意思是,它们不是谎言。它们只是没有很好地捕捉和传达这个版本有多么出色。与许多以前的版本相比,这个版本有更多面向用户的“玩具”(功能)。这可能是自 Spring Security 至少长出 Java 配置 DSL 以来我最喜欢的版本!

看看那些发布说明。看到那些关于 Passkeys一次性令牌登录 (One-Time Token Login) 的微小章节了吗?是的。那就是谎言。这些内容值得拥有自己的章节!我们会回到它们。我保证。让我们快速看看其他内容。有太多不同的内容了。

  • 方法安全和 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 后台通道(back-channel)支持现在接受类型为 logout+jwt 的退出令牌。
  • Spring RestClient 现在可以通过配置 OAuth2ClientHttpRequestInterceptor 来进行受保护资源请求。您可以在发出 HTTP 请求时,让它将您的令牌提供给下游服务。
  • 令牌交换(token exchange)现在支持刷新令牌(refresh tokens)。
  • SAML 2.0 支持也有了飞跃性的改进!OpenSAML 5 支持已到位。EntityID 的 registrationId 得到了简化。
  • 断言方(Asserting Parties)现在可以根据元数据的过期时间在后台刷新。
  • 您现在可以签署依赖方(relying party)的元数据。
  • 为了与 SAML 2.0 标准保持一致,元数据端点也使用了 application/samlmetadata+xml MIME 类型。
  • 在普通的传统(plain 'ol)Spring Web 应用中,我们支持 CSRF BREACH 令牌。
  • 以及更可定制的 记住我 (Remember Me) cookie。
  • 并且 Spring Security 过滤器链会标记更多无效配置。
  • 并且 ServerHttpSecurity 现在将 ServerWebExchangeFirewall 对象作为 bean 加载。
  • 这个版本还带来了改进的、粒度更粗的集成,用于分别观察授权、认证和请求的可观测性。
  • AclAuthorizationStrategyImpl 支持 RoleHierarchy 类型,这个类型也相当新!
  • Kotlin 支持也有了显著改进!
  • 从技术上讲,Spring Authorization Server 不属于 Spring Security 的一部分,但为了简单起见,我在这里提及它。它包含许多新功能,包括明显更一致和简洁的 DSL 配置。在典型的 Spring Security 应用中,用一行 Java 代码以及在 properties 或 YAML 文件中的几行配置就可以启动并运行一个完整的 OAuth IDP。太棒了 (SO NICE)。

现在,让我们回到 Passkeys 和一次性令牌的讨论。

更好的密码

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

我们会有一个想要锁定(保护)的 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)等替代方法的建议。让我们看看他们的一些建议。

创建强力密码

如果您的系统将使用密码,有很多需要了解。幸好,Spring Security 可以为您完成以下大部分(全部?)指导。

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

最好的密码就是没有密码

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

通过优先考虑这些方法,NIST 强调向多因素认证(MFA)和加密解决方案过渡,将其作为传统密码的更安全替代方案。

使用一次性令牌(OTTs)实现无密码

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

Spring Security 不提供与您的电子邮件提供商或首选消息应用程序等集成。您可以使用 Sendgrid、Twilio 或其他一百万个服务中的任何一个来实现这一点。但 Spring Security 提供了通过链接创建和认证所需的基础设施(plumbing)。

这是我们需要添加到 Spring Security 配置中的一小段代码,以便它打印


	@Bean
	SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
				// ..
				.oneTimeTokenLogin(configurer -> configurer.tokenGenerationSuccessHandler(
                        (request, response, oneTimeToken) -> {
                            var msg = "go to http://localhost: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 联盟。FIDO 联盟得到了普遍支持!以下是一些 最杰出的成员。名单包括 DELL、Apple、Google、Intuit、NTT DOCOMO、Microsoft、meta、LastPass、DashLane、美国银行、1Password、Intel、CISCO、CVS Health、美国运通、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("http://localhost:8080")
                )
                // ...
                .build();
	}

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

要查看实际效果,请退出登录:localhost:8080/logout

不仅非常安全,而且对用户来说更容易!是不是很酷?

订阅 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助力您的进步。

了解更多

获取支持

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

了解更多

即将举行的活动

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

查看全部