java.lang.IllegalArgumentException: The class with xxUserDetails and the nam is not in the allowlist
java.lang.IllegalArgumentException: The class with xxUserDetails and the nam is not in the allowlist
解决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的默认白名单机制是主动安全防护,防止以下风险:
- 任意类实例化(特别是通过构造器)
- 敏感方法调用(如Runtime.exec())
- 内存破坏攻击
- 拒绝服务攻击(DoS)
最佳实践建议
-
最小权限原则:
- 只允许确实需要序列化的类
- 避免将整个领域模型标记为可序列化
-
安全审计:
# 检查项目中所有实现Serializable的类 grep -r "implements Serializable" src/main/java/ -
版本控制:
// 始终定义serialVersionUID private static final long serialVersionUID = 1L; -
敏感字段处理:
@JsonIgnore // 避免序列化敏感字段 private String securityAnswer; -
定期依赖检查:
// build.gradle plugins { id 'org.owasp.dependencycheck' version '8.0.2' }
总结
当Spring Security OAuth2遇到not in allowlist错误时,本质是安全反序列化机制在保护系统。通过:
- 添加Jackson注解(推荐大多数场景)
- 配置Mixin映射(复杂需求)
可安全地将自定义UserDetails类加入白名单。
关键决策点:
- 选择方案一:当类结构简单且可直接修改时
- 选择方案二:当需要精细控制或无法修改源码时
安全提示:无论选择哪种方案,都应确保自定义类没有安全隐患,避免绕过安全机制引入漏洞。
good day!!!
更多推荐


所有评论(0)