OpenSAML2升级到3解决JDK1.8解析null
JDK1.8 下 XML 解析出的 Document 结构变了,OpenSAML2(2015年停更)无法解析导致空指针。OpenSAML3 支持 JDK1.8,需改包名 org.opensaml.xml → org.opensaml.core.xml,Configuration.getUnmarshallerFactory() 改成 XMLObjectProviderRegistrySupport,BasicParserPool 要先 initialize()。
用 JDK1.8 重构一个老项目,别的地方都还顺,偏偏解析第三方返回的 SAML 鉴权报文那段,一跑就空指针。
代码一行没改过,之前在老 JDK 上跑得好好的。换了 JDK 怎么就崩了?
查下来是个有点连锁反应的问题:JDK 升级 → XML 解析结果变了 → 老的 OpenSAML 包跟不上。这篇把排查和升级的过程记一下,踩过 SAML 这摊的应该用得上。
空指针出在哪
先定位。报错代码是这段,解析 SAML 报文字符串成 XMLObject:
public XMLObject buildXMLObject(String xmlObjectString) {
try {
BasicParserPool parser = new BasicParserPool();
parser.setNamespaceAware(true);
Document document = (Document) parser.parse(
new ByteArrayInputStream(xmlObjectString.getBytes()));
Element element = document.getDocumentElement();
// 问题就在这:unmarshaller 返回了 null
Unmarshaller unmarshaller = Configuration.getUnmarshallerFactory()
.getUnmarshaller(element);
// 上一行是 null,这里 unmarshall 直接 NPE
return unmarshaller.unmarshall(element);
} catch (XMLParserException e) {
throw new RuntimeException(e);
} catch (UnmarshallingException e) {
throw new RuntimeException(e);
}
}
getUnmarshaller(element) 拿到的是 null,所以下一行调 unmarshall 就空指针。
代码本身之前验证过没问题,那只能是环境变了。只好拿新老项目对同一份报文做对比 Debug。
根因:Document 结构变了
对比一下 Debug 时的 Document 对象,差异很明显:JDK1.8 解析出来的 XML Document 结构,跟老版本不一样了。
getUnmarshaller 是靠 element 的信息去匹配对应的解析器的,结构一变,它匹配不上,就返回了 null。
说白了,不是我代码的锅,是手里这个 OpenSAML 工具包压根不认 JDK1.8 解析出来的新结构。它太老了。
版本追踪
既然怀疑包老,那就查它的版本情况。
项目用的 OpenSAML 是 2.6.4。去 Maven 中央库一看,这个包最后更新停在 2015 年,而且 2.6.4 就是它的最后一个版本。
再翻到官网 WIKI,几个版本的状态是这样的(内容我按官方说明转述):
- OpenSAML 2:从 2016 年 7 月底起,V2 的 Java 分支停止一切安全维护,官方建议升级到 V3 或另寻替代。
- OpenSAML 3:也已到生命周期终点,不再受支持。
- OpenSAML 4:当前主力版本,基于 Java 11,支持 SAML 1.0 / 1.1 / 2.0;而基于 Java 8 的那条线也已经终止维护。
内容为对官方 WIKI 的转述,便于说明版本脉络。
对我这个 JDK1.8 的项目来说,4.x 要 Java 11 用不了,2.x 又太老不认新结构。那就只能落在 OpenSAML 3:它能适配 JDK1.8,虽然官方也已经不再支持了,但能跑通是第一位的。
V3 的包去哪找
这里卡了一下。既然 2.6.4 是 V2 的最后版本,那 V3 的包叫什么、在哪?
搜 org.opensaml 这个组织下的内容,发现一堆相关制品。
读 WIKI 才明白:OpenSAML 从 V3 开始不再是 V2 那种"一个大包搞定",而是拆成了一堆组件,按需引入。
对我来说手动一个个挑组件太麻烦,正好看到 spring-security 有对 SAML 的封装,顺手用它的依赖把一串组件带进来:
<!-- spring-security 的 SAML 支持,会自动把 OpenSAML 3 的一串组件带进来 -->
<dependency>
<groupId>org.springframework.security.extensions</groupId>
<artifactId>spring-security-saml2-core</artifactId>
<version>2.0.0.M31</version>
</dependency>
升级后代码怎么改
换了包,代码得跟着改,主要是三类变化。
启动时要初始化
OpenSAML 3 的组件用之前必须先做一次全局初始化,官方建议放在应用启动时:
public static void main(String[] args) throws InitializationException {
SpringApplication.run(App.class, args);
// OpenSAML 组件使用前必须初始化一次,否则后续解析拿不到解析器
InitializationService.initialize();
System.out.println("App started successfully");
}
包名和 API 变了
大量 import 飘红,主要是包名从 org.opensaml.xml 挪到了 org.opensaml.core.xml,还有些类直接没了:
try {
BasicParserPool parser = new BasicParserPool();
parser.setNamespaceAware(true);
// 新版 BasicParserPool 用之前要先 initialize(),这是新加的一步
parser.initialize();
Document doc = parser.parse(new ByteArrayInputStream(xmlObjectString.getBytes()));
Element element = doc.getDocumentElement();
// 老代码:Configuration.getUnmarshallerFactory() —— 新包里没了
// 换成 XMLObjectProviderRegistrySupport 来拿工厂
Unmarshaller unmarshaller = XMLObjectProviderRegistrySupport
.getUnmarshallerFactory().getUnmarshaller(element);
return unmarshaller.unmarshall(element);
} catch (UnmarshallingException e) {
throw new RuntimeException(e);
} catch (XMLParserException e) {
throw new RuntimeException(e);
} catch (ComponentInitializationException e) {
// 新增:initialize() 可能抛的异常,得接住
throw new RuntimeException(e);
}
签名校验也变了
用证书验签这块 API 改动不小:
BasicX509Credential basicCredential;
try {
X509Certificate cert = getCertificate(publicKey);
// 新版 BasicX509Credential 没有空构造,必须把证书传进去
basicCredential = new BasicX509Credential(cert);
// 它的 setPublicKey() 虽然还在,但禁止调用,一调直接报错
// 公钥现在从 getCertificate().getPublicKey() 走
} catch (Exception e) {
return false;
}
Signature signature = signableXMLObject.getSignature();
try {
// SignatureValidator 也没空构造了,验签改成静态方法调用
SignatureValidator.validate(signature, basicCredential);
return true;
} catch (SignatureException e) {
return false;
}
安全上多说一句
SAML 本质是在解析来自外部的 XML,这类场景天然要防 XML 相关攻击,比如 XXE(外部实体注入)。
OpenSAML 3 的 BasicParserPool 默认关掉了危险特性,相对安全,但如果你自己另起 XML 解析器,记得手动禁用外部实体、禁用 DTD。
还有验签这一步别偷懒省掉——SAML 报文如果不校验签名,等于谁都能伪造一份鉴权结果塞给你。
小结
这次问题的链条是这样的:JDK1.8 让 XML 解析出的 Document 结构变了,而 2015 年就停更的 OpenSAML 2 不认新结构,getUnmarshaller 返回 null,最终空指针。
解法是升到能适配 JDK1.8 的 OpenSAML 3,借 spring-security 的依赖把拆散的组件一把带进来。
升级要改三处:启动时 InitializationService.initialize()、包名 org.opensaml.xml → org.opensaml.core.xml、验签改用静态方法。
遇到"代码没动却突然崩"的情况,别急着怀疑自己,先看看脚底下的运行环境是不是被人换了地板。
