解决Spring Security OAuth2中自定义UserDetails类的反序列化问题

问题背景

在使用Spring Security OAuth2 Authorization Server项目时,自定义的UserDetails实现类CustomUserDetails在运行中报错:

java.lang.IllegalArgumentException: The class with com.style.persistence.userdetails.CustomUserDetails and name of com.style.persistence.userdetails.CustomUserDetails is not in the allowlist...

完整的错误堆栈显示,问题发生在JdbcOAuth2AuthorizationService尝试反序列化授权信息时,系统拒绝处理CustomUserDetails类。

问题分析

根本原因

Spring Security出于安全考虑,默认只允许反序列化核心类,以防止潜在的不安全反序列化攻击。默认白名单如下:

// org.springframework.security.jackson2.SecurityJackson2Modules.AllowlistTypeIdResolver
private static final Set<String> ALLOWLIST_CLASS_NAMES;
static {
    Set<String> names = new HashSet<>();
    names.add("java.util.ArrayList");
    names.add("java.util.Collections$EmptyList");
    // ...其他核心类...
    names.add("org.springframework.security.core.context.SecurityContextImpl");
    names.add("java.util.Arrays$ArrayList");
    ALLOWLIST_CLASS_NAMES = Collections.unmodifiableSet(names);
}

当OAuth2授权服务尝试从数据库加载授权信息时,如果其中包含自定义的UserDetails实现类(如CustomUserDetails),Jackson反序列化器会拒绝处理,因为该类不在安全白名单中。

关键点

  • 安全机制:防止恶意类通过反序列化执行任意代码
  • 触发场景:当授权信息被持久化到数据库后再次读取时
  • 错误位置JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper.parseMap()

解决方案

方案一:使用Jackson注解(推荐,简单场景)

在自定义的CustomUserDetails类上添加Jackson注解:

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

@JsonSerialize
@JsonIgnoreProperties(ignoreUnknown = true)
public class CustomUserDetails implements UserDetails, Serializable {
    
    // 类实现...
}

注解说明

  • @JsonSerialize:明确声明该类可被序列化
  • @JsonIgnoreProperties(ignoreUnknown = true):忽略JSON中的未知属性,防止因字段不匹配导致反序列化失败

优势

  • 实现简单,只需添加两个注解
  • 无需额外配置类
  • 适合大多数自定义UserDetails场景

方案二:使用Mixin(复杂序列化控制)

1. 创建自定义反序列化器
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.MissingNode;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.io.IOException;
import java.util.Set;

public class CustomUserDetailsDeserializer extends JsonDeserializer<CustomUserDetails> {

    @Override
    public CustomUserDetails deserialize(JsonParser jp, DeserializationContext context) 
        throws IOException {
        
        ObjectMapper mapper = (ObjectMapper) jp.getCodec();
        JsonNode jsonNode = mapper.readTree(jp);
        
        // 提取各个字段值
        String username = readJsonNode(jsonNode, "username").asText();
        String password = readJsonNode(jsonNode, "password").asText("");
        // 其他字段提取...
        
        // 构造CustomUserDetails对象
        return new CustomUserDetails(username, password, ...);
    }

    private JsonNode readJsonNode(JsonNode jsonNode, String field) {
        return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
    }
}
2. 创建Mixin接口
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonDeserialize(using = CustomUserDetailsDeserializer.class)
@JsonAutoDetect(
    fieldVisibility = JsonAutoDetect.Visibility.ANY,
    getterVisibility = JsonAutoDetect.Visibility.NONE,
    isGetterVisibility = JsonAutoDetect.Visibility.NONE
)
@JsonIgnoreProperties(ignoreUnknown = true)
public abstract class CustomUserDetailsMixin {}
3. 注册Mixin到ObjectMapper
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.jackson2.SecurityJackson2Modules;

@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        
        // 注册Spring Security默认模块
        mapper.registerModules(SecurityJackson2Modules.getModules(getClass().getClassLoader()));
        
        // 注册自定义Mixin
        mapper.addMixIn(CustomUserDetails.class, CustomUserDetailsMixin.class);
        
        return mapper;
    }
}

适用场景

  • 需要精细控制序列化/反序列化过程
  • 自定义类有特殊字段处理需求
  • 类结构复杂且需要版本兼容

方案对比与推荐

特性 Jackson注解方案 Mixin方案
实现复杂度 ⭐⭐ (简单) ⭐⭐⭐⭐ (复杂)
配置量 几行注解 多个类+配置
灵活性 基础控制 高度可控
侵入性 需修改实体类 不修改实体类
推荐场景 大多数情况 复杂序列化需求

推荐选择

  • 对于大多数项目,方案一(Jackson注解) 足够且高效
  • 仅在需要精细控制序列化行为或无法修改实体类时使用方案二

原理深入:为什么需要配置

关键源码分析

JdbcOAuth2AuthorizationService中,授权信息从数据库读取后需要反序列化:

// org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService
private class OAuth2AuthorizationRowMapper implements RowMapper<OAuth2Authorization> {
    
    private Map<String, Object> parseMap(String data) {
        try {
            return this.objectMapper.readValue(data, 
                new TypeReference<Map<String, Object>>() {});
        } catch (Exception ex) {
            throw new IllegalArgumentException(ex.getMessage(), ex);
        }
    }
}

objectMapper尝试反序列化包含CustomUserDetails的数据时,会检查类是否在白名单中。若未配置,则抛出观察到的异常。

安全考虑

Spring Security的默认白名单机制是主动安全防护,防止以下风险:

  1. 任意类实例化(特别是通过构造器)
  2. 敏感方法调用(如Runtime.exec())
  3. 内存破坏攻击
  4. 拒绝服务攻击(DoS)

最佳实践建议

  1. 最小权限原则

    • 只允许确实需要序列化的类
    • 避免将整个领域模型标记为可序列化
  2. 安全审计

    # 检查项目中所有实现Serializable的类
    grep -r "implements Serializable" src/main/java/
    
  3. 版本控制

    // 始终定义serialVersionUID
    private static final long serialVersionUID = 1L;
    
  4. 敏感字段处理

    @JsonIgnore // 避免序列化敏感字段
    private String securityAnswer;
    
  5. 定期依赖检查

    // build.gradle
    plugins {
        id 'org.owasp.dependencycheck' version '8.0.2'
    }
    

总结

当Spring Security OAuth2遇到not in allowlist错误时,本质是安全反序列化机制在保护系统。通过:

  1. 添加Jackson注解(推荐大多数场景)
  2. 配置Mixin映射(复杂需求)
    可安全地将自定义UserDetails类加入白名单。

关键决策点

  • 选择方案一:当类结构简单且可直接修改时
  • 选择方案二:当需要精细控制或无法修改源码时

安全提示:无论选择哪种方案,都应确保自定义类没有安全隐患,避免绕过安全机制引入漏洞。


good day!!!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐