1. 引言

Spring Security 6.3 带来了一系列框架层面的安全增强。本文将重点介绍其中几个关键特性,包括它们的优势和实际用法。

2. 被动式 JDK 序列化支持

Spring Security 6.3 引入了被动式 JDK 序列化支持。在深入探讨前,我们先理解这个设计背后的问题和挑战。

2.1. Spring Security 序列化设计

在 6.3 版本之前,Spring Security 对跨版本的 JDK 序列化/反序列化有严格限制。这个设计是框架刻意为之,旨在确保安全性和稳定性。核心逻辑是:防止使用不同版本 Spring Security 反序列化对象时出现兼容性问题和安全漏洞。

关键设计点在于全局使用统一的 serialVersionUID。Java 序列化机制通过 serialVersionUID 验证加载类与序列化对象的匹配性。Spring Security 为每个发布版本维护全局唯一的 serialVersionUID,确保不同版本间无法互相反序列化,从而形成版本屏障。

例如 SecurityContextImpl 类的序列化版本包含特定版本的 serialVersionUID。当尝试用不同版本 Spring Security 反序列化时,serialVersionUID 不匹配会导致失败。

2.2. 序列化设计带来的挑战

虽然这个设计强化了安全性,但也带来了一些实际开发中的痛点:

  • 金丝雀发布升级问题:当 Spring Security 版本变更时,持久化的会话信息无法反序列化,可能导致用户需要重新登录
  • RMI 架构兼容性:在基于 RMI 的架构中,如果客户端和服务端使用不同 Spring Security 版本,远程调用会因 InvalidClassException 失败

2.3. 临时解决方案

常见的变通方案包括:

  1. 替换序列化库:使用 Jackson 等替代 JDK 序列化,将对象转为 JSON 再序列化
  2. 自定义序列化:继承 Spring Security 类(如 Authentication),通过 readObject/writeObject 实现自定义序列化

2.4. Spring Security 6.3 的序列化改进

6.3 版本实现了向前兼容的序列化机制:类序列化时会与前一个次版本进行兼容性检查。这确保升级到新版本时,Spring Security 类的反序列化可以无缝进行。

3. 授权机制增强

Spring Security 6.3 在授权机制上引入了多项重要改进。

3.1. 注解参数化

Spring Security 的方法安全支持元注解。我们可以基于业务场景创建可读性更强的注解。例如将 @PreAuthorize("hasRole('USER')") 简化为:

@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('USER')")
public @interface IsUser {
    String[] value();
}

然后在业务代码中使用:

@Service
public class MessageService {
    @IsUser
    public Message readMessage() {
        return "Message";
    }
}

但为每个角色创建独立注解(如 @IsAdmin)会显得冗余。Spring Security 6.3 支持参数化元注解,实现更灵活的模板化。具体步骤:

  1. 定义模板解析 Bean:

    @Bean
    PrePostTemplateDefaults prePostTemplateDefaults() {
     return new PrePostTemplateDefaults();
    }
    
  2. 创建参数化元注解:

    @Target({ ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @PreAuthorize("hasAnyRole({value})")
    public @interface CustomHasAnyRole {
     String[] value();
    }
    
  3. 在业务代码中动态指定角色:

    @Service
    public class MessageService {
     private final List<Message> messages;
    
     public MessageService() {
         messages = new ArrayList<>();
         messages.add(new Message(1, "Message 1"));
     }
     
     @CustomHasAnyRole({"'USER'", "'ADMIN'"})
     public Message readMessage(Integer id) {
         return messages.get(0);
     }
    
     @CustomHasAnyRole("'ADMIN'")
     public String writeMessage(Message message) {
         return "Message Written";
     }
     
     @CustomHasAnyRole({"'ADMIN'"})
     public String deleteMessage(Integer id) {
         return "Message Deleted";
     }
    }
    

3.2. 返回值安全控制

Spring Security 6.3 新增 @AuthorizeReturnObject 注解,支持对方法返回的领域对象进行细粒度安全控制。这确保只有授权用户才能访问特定领域对象。

示例场景:Account 类的 balance 字段只允许有 read 权限的用户访问:

public class Account {
    private String iban;
    private Double balance;

    // Constructor

    public String getIban() {
        return iban;
    }

    @PreAuthorize("hasAuthority('read')")
    public Double getBalance() {
        return balance;
    }
}

在服务层使用 @AuthorizeReturnObject 保护返回对象:

@Service
public class AccountService {
    @AuthorizeReturnObject
    public Optional<Account> getAccountByIban(String iban) {
        return Optional.of(new Account("XX1234567809", 2345.6));
    }
}

3.3. 错误处理优化

使用 @AuthorizeReturnObject 时,未授权访问会抛出 AccessDeniedExceptionSpring Security 6.3 提供 MethodAuthorizationDeniedHandler 接口实现自定义错误处理。

示例场景:对未授权访问返回脱敏值而非异常:

  1. 实现自定义处理器:

    @Component
    public class MaskMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler  {
     @Override
     public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
         return "****";
     }
    }
    
  2. 在方法上应用处理器:

    @PreAuthorize("hasAuthority('read')")
    @HandleAuthorizationDenied(handlerClass=MaskMethodAuthorizationDeniedHandler.class)
    public String getIban() {
     return iban;
    }
    

4. 弱密码检测

Spring Security 6.3 内置弱密码检测功能,通过比对 pwnedpasswords.com 数据库验证密码安全性。可在用户注册时实时检测密码是否已泄露。

实现步骤:

  1. 定义检测 Bean:

    @Bean
    public HaveIBeenPwnedRestApiPasswordChecker passwordChecker() {
     return new HaveIBeenPwnedRestApiPasswordChecker();
    }
    
  2. 在注册接口中使用:

    @RestController
    @RequestMapping("/register")
    public class RegistrationController {
     private final HaveIBeenPwnedRestApiPasswordChecker haveIBeenPwnedRestApiPasswordChecker;
    
     @Autowired
     public RegistrationController(HaveIBeenPwnedRestApiPasswordChecker haveIBeenPwnedRestApiPasswordChecker) {
         this.haveIBeenPwnedRestApiPasswordChecker = haveIBeenPwnedRestApiPasswordChecker;
     }
    
     @PostMapping
     public String register(@RequestParam String username, @RequestParam String password) {
         CompromisedPasswordDecision compromisedPasswordDecision = haveIBeenPwnedRestApiPasswordChecker.checkPassword(password);
         if (compromisedPasswordDecision.isCompromised()) {
         throw new IllegalArgumentException("Compromised Password.");
     }
    
         // ...
         return "User registered successfully";
     }
    }
    

5. OAuth 2.0 令牌交换授权

Spring Security 6.3 支持 OAuth 2.0 令牌交换授权(RFC 8693,允许客户端在保留用户身份的前提下交换令牌。这特别适用于资源服务器需要扮演客户端获取新令牌的场景(如模拟用户操作)。

典型场景:

  • loan-service 资源服务器需要调用 loan-product-service
  • 两个服务要求不同的 aud 声明(受众)
  • 原始令牌无法直接用于目标服务

解决方案loan-service 作为客户端,通过令牌交换获取适用于 loan-product-service 的新令牌,同时保留原始用户身份。

Spring Security 6.3 提供了 TokenExchangeOAuth2AuthorizedClientProvider 实现此功能。

6. 总结

Spring Security 6.3 带来了多项重要改进: ✅ 授权框架增强(参数化注解、返回值安全控制、自定义错误处理) ✅ 被动式 JDK 序列化支持 ✅ 内置弱密码检测 ✅ OAuth 2.0 令牌交换支持

这些特性显著提升了框架的易用性和安全性,特别是在微服务架构和复杂授权场景下。完整示例代码可在 GitHub 获取。


原始标题:Spring Security 6.3 – What’s New | Baeldung