Spring Security 6.3 版本在框架中引入了一系列安全增强功能。
在本教程中,我们将讨论一些最显著的功能,重点介绍它们的优点和用途。
被动 JDK 序列化支持
Spring Security 6.3 包含被动 JDK 序列化支持。然而,在进一步讨论这个问题之前,让我们先了解一下它的问题和相关问题。
Spring Security 序列化设计
在 6.3 版之前,Spring Security对通过 JDK 序列化在不同版本中序列化和反序列化其类有严格的策略。此限制是框架为确保安全性和稳定性而做出的刻意设计决定。这样做的理由是防止使用不同版本的 Spring Security 反序列化在一个版本中序列化的对象时出现不兼容和安全漏洞。
本设计的一个关键方面是整个 Spring Security 项目使用一个全局的serialVersionUID。在 Java 中,序列化和反序列化过程使用唯一标识符serialVersionUID来验证加载的类是否与序列化对象完全对应。
通过维护每个 Spring Security 发布版本独有的全局serialVersionUID ,框架可确保一个版本的序列化对象不能使用另一个版本进行反序列化。 这种方法有效地创建了版本屏障,防止反序列化具有不匹配serialVersionUID值的对象。
例如, Spring Security 中的SecurityContextImpl类表示安全上下文信息。此类的序列化版本包含特定于该版本的 serialVersionUID。当尝试在不同版本的 Spring Security 中反序列化此对象时,serialVersionUID不匹配会阻止该过程成功。
序列化设计带来的挑战
在优先考虑增强安全性的同时,这种设计策略也带来了一些挑战。开发人员通常将 Spring Security 与其他 Spring 库(如Spring Session)集成,以管理用户登录会话。这些会话包含关键的用户身份验证和安全上下文信息,通常通过 Spring Security 类实现。此外,为了优化用户体验并增强应用程序的可扩展性,开发人员通常会将这些会话数据存储在各种持久存储解决方案中,包括数据库。
以下是由于序列化设计而产生的一些挑战。如果 Spring Security 版本发生变化,通过 Canary 发布流程升级应用程序可能会导致问题。在这种情况下,持久会话信息无法反序列化,可能需要用户重新登录。
另一个问题出现在使用 Spring Security 的远程方法调用 (RMI) 的应用程序架构中。例如,如果客户端应用程序在远程方法调用中使用 Spring Security 类,则必须在客户端序列化它们,并在另一端反序列化它们。如果两个应用程序不共享相同的 Spring Security 版本,则此调用会失败,从而导致InvalidClassException异常。
解决方法
解决此问题的典型方法如下。我们可以使用 JDK 序列化以外的其他序列化库,例如 Jackson 序列化。这样,我们就不用序列化 Spring Security 类了,而是获取所需详细信息的 JSON 表示,然后使用 Jackson 对其进行序列化。
另一种选择是扩展所需的 Spring Security 类,例如Authentication,并通过readObject和writeObject方法明确实现自定义序列化支持。
Spring Security 6.3 中的序列化变化
在 6.3 版本中,类序列化会与前一个次要版本进行兼容性检查。这确保升级到较新版本后可以无缝地反序列化 Spring Security 类。
授权
Spring Security 6.3 在 Spring Security 授权中引入了一些值得注意的变化。让我们在本节中探讨这些变化。
注释参数
Spring Security 的方法安全支持元注释。我们可以根据应用程序的用例采用注释并提高其可读性。例如,我们可以将 @PreAuthorize (“hasRole('USER')”)简化为以下内容:
@Target({ ElementType.METHOD}) |
接下来我们就可以在业务代码中使用这个@IsUser注解了:
@Service |
假设我们有另一个角色ADMIN 。我们可以为该角色创建一个名为@IsAdmin的注释。但是,这将是多余的。将此元注释用作模板并将角色作为注释参数包含会更合适。Spring Security 6.3 引入了定义此类元注释的功能。让我们用一个具体的例子来演示这一点:
要模板化元注释,首先我们需要定义一个 bean PrePostTemplateDefaults:
@Bean |
模板解析需要这个 bean 定义。
接下来,我们将为@PreAuthorize注释定义一个元注释@CustomHasAnyRole,它可以接受USER和ADMIN角色:
@Target({ ElementType.METHOD}) |
我们可以通过提供以下角色来使用这个元注释:
@Service |
在上面的例子中,我们提供了角色值 - USER和ADMIN作为注释参数。
确保返回值安全
Spring Security 6.3 中另一个强大的新功能是使用@AuthorizeReturnObject注释保护域对象的能力。此增强功能通过对方法返回的对象启用授权检查来实现更细粒度的安全性,确保只有授权用户才能访问特定的域对象。
让我们用一个例子来说明这一点。假设我们有以下带有iban和balance字段的Account类。要求只有具有读取权限的用户才能检索帐户余额。
public class Account { |
接下来,让我们定义AccountService类,它返回一个帐户实例:
@Service |
在上面的代码片段中,我们使用了@AuthorizeReturnObject注释。Spring Security 确保只有具有读取权限的用户才能访问Account实例。
错误处理
在上一节中,我们讨论了使用@AuthorizeReturnObject注释来保护域对象。一旦启用,未经授权的访问将导致AccessDeniedException。Spring Security 6.3 提供了MethodAuthorizationDeniedHandler接口来处理授权失败。
让我们用一个例子来说明这一点。让我们扩展第 3.2 节中的示例,并使用读取权限保护 IBAN。但是,我们打算提供一个屏蔽值,而不是对任何未经授权的访问返回AccessDeniedException 。
让我们定义MethodAuthorizationDeniedHandler接口的实现:
@Component |
在上面的代码片段中,如果存在AccessDeniedException ,我们将提供一个屏蔽值。此处理程序类可在getIban()方法中使用,如下所示:
@PreAuthorize("hasAuthority('read')") |
密码检查被破解
Spring Security 6.3 提供了一个用于检查泄露密码的实现。此实现根据泄露密码数据库 ( pwnedpasswords.com ) 检查提供的密码。因此,应用程序可以在注册时验证用户提供的密码。以下代码片段演示了用法。
首先定义一个HaveIBeenPwnedRestApiPasswordChecker类的bean定义:
@Bean |
接下来,使用此实现来检查用户提供的密码:
@RestController |
OAuth 2.0 令牌交换授权
Spring Security 6.3 还引入了对OAuth 2.0令牌交换 ( RFC 8693 ) 授权的支持,允许客户端在保留用户身份的同时交换令牌。此功能支持模拟等场景,其中资源服务器可以充当客户端来获取新令牌。让我们通过一个例子来详细说明这一点。
假设我们有一个名为 loan-service 的资源服务器,它为贷款账户提供各种 API。此服务是安全的,客户端需要提供访问令牌,该令牌必须具有贷款服务的受众(aud 声明)。
现在让我们假设 loan-service 需要调用另一个资源服务loan-product-service,该服务公开贷款产品的详细信息。 loan-product-service 也是安全的,并且需要具有loan-product-service受众的令牌。由于这两个服务的受众不同,因此 loan 服务的令牌不能用于loan-product-service。
在这种情况下,资源服务器 loan-service 应该成为客户端,并将现有令牌交换为保留原始令牌身份的 loan-product-service 的新令牌。
Spring Security 6.3为令牌交换授权提供了OAuth2AuthorizedClientProvider类的新实现,名为TokenExchangeOAuthorizedClientProvider 。
结论
在本文中,我们讨论了 Spring Security 6.3 中引入的各种新功能。
显著的变化是授权框架的增强、被动 JDK 序列化支持和 OAuth 2.0 令牌交换支持。