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、验签改用静态方法。

遇到"代码没动却突然崩"的情况,别急着怀疑自己,先看看脚底下的运行环境是不是被人换了地板。

更多推荐

章节目录