Spring Batch使用XStream反序列化报ForbiddenClassException异常
之前参考StackOverflow上题为「Caused by: java.lang.NullPointerException: Cannot invoke "com.thoughtworks.xstream.XStream.fromXML(String)" because "this.xstream" is null」的方案排查问题,未解决。
运行Spring Batch作业时step1执行失败,核心错误为XStream反序列化抛出org.springframework.oxm.UnmarshallingFailureException,嵌套异常是com.thoughtworks.xstream.security.ForbiddenClassException: com.mkyong.model.Report,即XStream安全机制拦截了自定义Report类的反序列化操作,完整错误栈如下:
[StepExecution: id=1, version=2, name=step1, status=FAILED, exitStatus=FAILED, readCount=0, filterCount=0, writeCount=0 readSkipCount=0, writeSkipCount=0, processSkipCount=0, commitCount=0, rollbackCount=1, exitDescription=org.springframework.oxm.UnmarshallingFailureException: XStream unmarshalling exception; nested exception is com.thoughtworks.xstream.security.ForbiddenClassException: com.mkyong.model.Report at org.springframework.oxm.xstream.XStreamMarshaller.convertXStreamException(XStreamMarshaller.java:885) at org.springframework.oxm.xstream.XStreamMarshaller.doUnmarshal(XStreamMarshaller.java:863) at org.springframework.oxm.xstream.XStreamMarshaller.unmarshalXmlStreamReader(XStreamMarshaller.java:820) at org.springframework.oxm.xstream.XStreamMarshaller.unmarshalXmlEventReader(XStreamMarshaller.java:811) at org.springframework.oxm.support.AbstractMarshaller.unmarshalStaxSource(AbstractMarshaller.java:412) at org.springframework.oxm.support.AbstractMarshaller.unmarshal(AbstractMarshaller.java:355) at org.springframework.batch.item.xml.StaxEventItemReader.doRead(StaxEventItemReader.java:273) at org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader.read(AbstractItemCountingItemStreamItemReader.java:93) at org.springframework.batch.core.step.item.SimpleChunkProvider.doRead(SimpleChunkProvider.java:99) at org.springframework.batch.core.step.item.SimpleChunkProvider.read(SimpleChunkProvider.java:180) at org.springframework.batch.core.step.item.SimpleChunkProvider$1.doInIteration(SimpleChunkProvider.java:126) at org.springframework.batch.repeat.support.RepeatTemplate.getNextResult(RepeatTemplate.java:375) at org.springframework.batch.repeat.support.RepeatTemplate.executeInternal(RepeatTemplate.java:215) at org.springframework.batch.repeat.support.RepeatTemplate.iterate(RepeatTemplate.java:145) at org.springframework.batch.core.step.item.SimpleChunkProvider.provide(SimpleChunkProvider.java:118) at org.springframework.batch.core.step.item.ChunkOrientedTasklet.execute(ChunkOrientedTasklet.java:71) at org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:407) at org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:331) at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140) at org.springframework.batch.core.step.tasklet.TaskletStep$2.doInChunkContext(TaskletStep.java:273) at org.springframework.batch.core.scope.context.StepContextRepeatCallback.doInIteration(StepContextRepeatCallback.java:82) at org.springframework.batch.repeat.support.RepeatTemplate.getNextResult(RepeatTemplate.java:375) at org.springframework.batch.repeat.support.RepeatTemplate.executeInternal(RepeatTemplate.java:215) at org.springframework.batch.repeat.support.RepeatTemplate.iterate(RepeatTemplate.java:145) at org.springframework.batch.core.step.tasklet.TaskletStep.doExecute(TaskletStep.java:258) at org.springframework.batch.core.step.AbstractStep.execute(AbstractStep.java:208) at org.springframework.batch.core.job.SimpleStepHandler.handleStep(SimpleStepHandler.java:152) at org.springframework.batch.core.job.flow.JobFlowExecutor.executeStep(JobFlowExecutor.java:68) at org.springframework.batch.core.job.flow.support.state.StepState.handle(StepState.java:68) at org.springframework.batch.core.job.flow.support.SimpleFlow.resume(SimpleFlow.java:169) at org.springframework.batch.core.job.flow.support.SimpleFlow.start(SimpleFlow.java:144) at org.springframework.batch.core.job.flow.FlowJob.doExecute(FlowJob.java:137) at org.springframework.batch.core.job.AbstractJob.execute(AbstractJob.java:320) at org.springframework.batch.core.launch.support.SimpleJobLauncher$1.run(SimpleJobLauncher.java:149) at org.springframework.core.task.SyncTaskExecutor.execute(SyncTaskExecutor.java:50) at org.springframework.batch.core.launch.support.SimpleJobLauncher.run(SimpleJobLauncher.java:140) at com.mkyong.App.main(App.java:25) Caused by: com.thoughtworks.xstream.security.ForbiddenClassException: com.mkyong.model.Report at com.thoughtworks.xstream.security.NoTypePermission.allows(NoTypePermission.java:26) at com.thoughtworks.xstream.mapper.SecurityMapper.realClass(SecurityMapper.java:74) at com.thoughtworks.xstream.mapper.MapperWrapper.realClass(MapperWrapper.java:125) at com.thoughtworks.xstream.mapper.CachingMapper.realClass(CachingMapper.java:47) at com.thoughtworks.xstream.core.util.HierarchicalStreams.readClassType(HierarchicalStreams.java:29) at com.thoughtworks.xstream.core.TreeUnmarshaller.start(TreeUnmarshaller.java:135) at com.thoughtworks.xstream.core.AbstractTreeMarshallingStrategy.unmarshal(AbstractTreeMarshallingStrategy.java:32) at com.thoughtworks.xstream.XStream.unmarshal(XStream.java:1421) at org.springframework.oxm.xstream.XStreamMarshaller.doUnmarshal(XStreamMarshaller.java:860) ... 35 more ]
job-report.xml
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:batch="http://www.springframework.org/schema/batch" xmlns:task="http://www.springframework.org/schema/task" xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <batch:job id="reportJob"> <batch:step id="step1"> <batch:tasklet> <batch:chunk reader="xmlItemReader" writer="mongodbItemWriter" commit-interval="1"> </batch:chunk> </batch:tasklet> </batch:step> </batch:job> <bean id="mongodbItemWriter" class="org.springframework.batch.item.data.MongoItemWriter"> <property name="template" ref="mongoTemplate" /> <property name="collection" value="report" /> </bean> <bean id="xmlItemReader" class="org.springframework.batch.item.xml.StaxEventItemReader"> <property name="fragmentRootElementName" value="record" /> <property name="resource" value="classpath:xml/report.xml" /> <property name="unmarshaller" ref="reportUnmarshaller" /> </bean> <bean id="reportUnmarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"> <property name=""></property> <property name="aliases"> <util:map id="aliases"> <entry key="record" value="com.mkyong.model.Report" /> </util:map> </property> <property name="converters"> <array> <ref bean="reportConverter" /> </array> </property> </bean> <bean id="reportConverter" class="com.mkyong.converter.ReportConverter" /> </beans>
context.xml
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd"> <!-- 作业元数据存储在内存中 --> <bean id="jobRepository" class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean"> <property name="transactionManager" ref="transactionManager" /> </bean> <bean id="transactionManager" class="org.springframework.batch.support.transaction.ResourcelessTransactionManager" /> <bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher"> <property name="jobRepository" ref="jobRepository" /> </bean> </beans>
database.xml
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:mongo="http://www.springframework.org/schema/data/mongo" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/data/mongo http://www.springframework.org/schema/data/mongo/spring-mongo.xsd"> <!-- 连接MongoDB --> <mongo:mongo-client host="localhost" port="27017" /> <mongo:db-factory dbname="yourdb" /> <bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate"> <constructor-arg name="mongoDbFactory" ref="mongoDbFactory" /> </bean> </beans>
Report.java
@AllArgsConstructor @NoArgsConstructor @Data @Builder public class Report { private int id; private Date date; private long impression; private int clicks; private BigDecimal earning; }
曾尝试添加typePermissions配置放开权限,但写法存在错误,配置如下:
<bean id="reportUnmarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"> <property name="typePermissions" ref="reportConverter"> <util:list value-type="com.thoughtworks.xstream.security.TypePermission"> <array> <value>TypePermission.ANY</value> </array> </util:list> </property> <property name="aliases"> <util:map id="aliases"> <entry key="record" value="com.mkyong.model.Report" /> </util:map> </property> <property name="converters"> <array> <ref bean="reportConverter" /> </array> </property> </bean>
XStream 1.4.7及以上版本默认开启类型安全白名单机制,默认仅允许反序列化JDK基础类型,所有自定义业务类必须显式加入允许列表才能正常反序列化,否则就会抛出ForbiddenClassException。
之前的权限配置存在三处错误:
- 错误给
typePermissions属性添加了无关的ref="reportConverter"引用 - 列表内部嵌套了多余的
<array>标签,不符合Spring集合配置规范 - 直接写字符串
TypePermission.ANY无法被Spring识别为对应的静态常量,不会生效
提供两种配置方案,根据使用场景选择:
方案1:显式指定允许反序列化的类(生产环境推荐,安全性最高)
直接通过XStreamMarshaller的supportedClasses属性,把需要反序列化的自定义类加入白名单即可,同时删除原配置里空的无效<property name=""></property>节点,修正后的reportUnmarshaller配置如下:
<bean id="reportUnmarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"> <!-- 显式声明允许反序列化的自定义类 --> <property name="supportedClasses"> <list> <value>com.mkyong.model.Report</value> <!-- 若Report类关联其他自定义POJO,一并添加到此处 --> </list> </property> <property name="aliases"> <util:map id="aliases"> <entry key="record" value="com.mkyong.model.Report" /> </util:map> </property> <property name="converters"> <array> <ref bean="reportConverter" /> </array> </property> </bean>
方案2:放开全类型权限(仅本地调试使用,禁止生产环境用)
如果是本地开发调试想快速绕过权限校验,可以通过Spring的FieldRetrievingFactoryBean注入XStream内置的全权限常量,配置方式如下:
<!-- 先注入XStream的全类型权限常量 --> <bean id="anyTypePermission" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"> <property name="staticField" value="com.thoughtworks.xstream.security.TypePermission.ANY"/> </bean> <bean id="reportUnmarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"> <property name="typePermissions"> <list> <ref bean="anyTypePermission"/> </list> </property> <property name="aliases"> <util:map id="aliases"> <entry key="record" value="com.mkyong.model.Report" /> </util:map> </property> <property name="converters"> <array> <ref bean="reportConverter" /> </array> </property> </bean>
- 必须删除原配置中空的
<property name=""></property>无效节点,避免Spring解析配置时抛出异常 - 如果反序列化的类中关联了其他自定义类型,需要把关联类也加入白名单,否则会继续抛出同类ForbiddenClassException
- 生产环境严禁使用全权限放开配置,避免触发XStream反序列化远程代码执行漏洞
内容的提问来源于stack exchange,提问作者Jeff Cook

