领先一步
VMware 提供培训和认证,助力您的进步。
了解更多Spring Security 6.4.1 是您处理认证和授权项的一站式商店,而且这个版本真是太棒了(doozie)!发布说明 充满可能性!
发布说明是个谎言!
我的意思是,它们不是谎言。它们只是没有很好地捕捉和传达这个版本有多么出色。与许多以前的版本相比,这个版本有更多面向用户的“玩具”(功能)。这可能是自 Spring Security 至少长出 Java 配置 DSL 以来我最喜欢的版本!
看看那些发布说明。看到那些关于 Passkeys 和 一次性令牌登录 (One-Time Token Login) 的微小章节了吗?是的。那就是谎言。这些内容值得拥有自己的章节!我们会回到它们。我保证。让我们快速看看其他内容。有太多不同的内容了。
@AuthorizeReturnObject
特性,并且可以在 @PreAuthorize
和 @PostAuthorize
生命周期回调中引用 bean。SecurityAnnotationScanners
API 提供了一种扫描安全注解的方式,以便为自定义注解添加 Spring Security 的选择和模板特性。oauth2Login()
方法现在接受 OAurth2AuthorizationRequestResolver
作为 @Bean
。ClientRegistrations
现在支持外部获取的配置。login page()
logout+jwt
的退出令牌。RestClient
现在可以通过配置 OAuth2ClientHttpRequestInterceptor
来进行受保护资源请求。您可以在发出 HTTP 请求时,让它将您的令牌提供给下游服务。registrationId
得到了简化。application/samlmetadata+xml
MIME 类型。记住我 (Remember Me)
cookie。ServerWebExchangeFirewall
对象作为 bean 加载。AclAuthorizationStrategyImpl
支持 RoleHierarchy
类型,这个类型也相当新!现在,让我们回到 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);
}
// ...
}
这个应用有两个用户:josh
和 rob
。josh
有一个角色 USER
,而 rob
同时拥有 ADMIN
和 USER
两个角色。
在一个非简单的应用中,您会希望使用指向身份提供者的替代 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)和加密解决方案过渡,将其作为传统密码的更安全替代方案。
因此,在 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
。
不仅非常安全,而且对用户来说更容易!是不是很酷?