You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Quarkus集成测试与Docker读取密钥文件仅29字节,单元测试正常

Quarkus/Docker环境下二进制密钥文件读取不完整导致JWT解密失败

问题现象

在Quarkus/Docker环境中读取二进制密钥文件时,仅能读取到29字节,但单元测试中可正常读取完整32字节,导致集成测试失败、本地Docker运行时抛出JWT解密异常。生产环境通过Openshift数据卷挂载密钥文件时运行正常,仅本地打包Jar后出现问题。

异常日志

Caused by: com.nimbusds.jose.JOSEException: AES/GCM/NoPadding decryption failed: Tag mismatch!
    at com.nimbusds.jose.crypto.impl.AESGCM.decrypt(AESGCM.java:301)
    at com.nimbusds.jose.crypto.impl.ContentCryptoProvider.decrypt(ContentCryptoProvider.java:279)
    at com.nimbusds.jose.crypto.DirectDecrypter.decrypt(DirectDecrypter.java:271)
    at com.nimbusds.jose.JWEObject.decrypt(JWEObject.java:415)
    at com.org.TokenParser.parseToken(TokenParser.java:38)
    ... 13 more
Caused by: javax.crypto.AEADBadTagException: Tag mismatch!
    at java.base/com.sun.crypto.provider.GaloisCounterMode.decryptFinal(GaloisCounterMode.java:623)
    at java.base/com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:1116)
    at java.base/com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1053)
    at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:853)
    at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446)
    at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2202)
    at com.nimbusds.jose.crypto.impl.AESGCM.decrypt(AESGCM.java:297)

场景与代码细节

密钥读取逻辑

代码优先尝试读取/opt/data/environment路径下的密钥文件,若不存在则 fallback 到类路径资源(src/test/resources或src/main/resources)。

JWT解密代码

import com.nimbusds.jose.JWEDecrypter;
import com.nimbusds.jose.crypto.factories.DefaultJWEDecrypterFactory;
import com.nimbusds.jwt.EncryptedJWT;
import com.nimbusds.jwt.JWTClaimsSet;
...
EncryptedJWT jwt = EncryptedJWT.parse(token);
DefaultJWEDecrypterFactory factory = new DefaultJWEDecrypterFactory();
JWEDecrypter decrypter = factory.createJWEDecrypter(jwt.getHeader(), this.secretKey);
jwt.decrypt(decrypter); // 此处抛出Tag mismatch异常

SecretKey创建代码

SecretKeySpec secretKey = new SecretKeySpec(data,"AES");

密钥读取方法

static byte[] readKeyFromInputStream(final InputStream sharedKeyStream) throws IOException, NoSuchAlgorithmException {
    byte[] keyBytes;
    if (sharedKeyStream == null) {
        keyBytes = autoGenerateKey();
    } else {
        keyBytes = new byte[sharedKeyStream.available()]; // 可用字节为32
        // 使用"read(keyBytes)"仅读取29字节,尝试显式读取32字节仍失败
        int count = sharedKeyStream.read(keyBytes, 0, 32);
        LOG.infof("Activating security token handler with key from input stream (length = %d bytes)", count);
        //if (count != 32) {
        //    throw new IOException("Key read bytes length does not match expected number(32), cannot initialize key!");
        //}
    }
    return keyBytes;
}

通过的单元测试

@Test
void shouldRead32BytesFromInputStream() throws IOException, NoSuchAlgorithmException {
    // given
    InputStream keyFileStream = TokenHandlerTest.class.getResourceAsStream("/key.shared");

    // when
    byte[] keyFileBytes = TokenHandler.readKeyFromInputStream(keyFileStream);

    // then
    assertEquals(32, keyFileBytes.length);
}

关键观察

  • 本地通过Gnome文件管理器和wc -c命令确认密钥文件为32字节
  • 将密钥文件放入src/jib/opt/data/environment由Jib Maven插件打包进Docker镜像后,运行正常
  • 替换为32字节纯文本密钥文件时,集成测试可正常读取32字节并通过
  • 原密钥为二进制文件,通过od查看十六进制内容,推测打包Jar时被压缩导致内容截断

疑问

  1. 原AEADBadTagException异常是否与密钥长度不匹配直接相关?
  2. 如何在集成测试中正确加载本地二进制密钥文件并读取完整32字节?

回答

1. 异常与密钥长度的关联

是的,该异常直接由密钥长度不匹配导致。AES-256要求密钥必须为32字节,当读取到的密钥仅29字节时,SecretKeySpec会将这29字节作为有效密钥使用,导致解密时的密钥与加密方的32字节密钥不一致,最终触发GCM模式的标签校验失败(Tag mismatch)。

2. 解决集成测试中完整读取二进制密钥的问题

问题根源是Maven打包Jar时,默认会对类路径下的文件进行过滤或压缩,二进制文件的特殊字节可能被篡改或截断。以下是三种可行解决方案:

方案1:配置Maven资源插件,排除二进制密钥文件的处理

在pom.xml中修改maven-resources-plugin配置,将密钥文件后缀标记为无需过滤的二进制文件:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-resources-plugin</artifactId>
            <version>3.3.1</version>
            <configuration>
                <nonFilteredFileExtensions>
                    <nonFilteredFileExtension>shared</nonFilteredFileExtension>
                </nonFilteredFileExtensions>
            </configuration>
        </plugin>
    </plugins>
</build>

此配置会让Maven保留.shared文件的原始二进制内容,避免打包时的篡改。

方案2:改进密钥读取逻辑,确保读取完整内容

当前读取逻辑依赖InputStream.available()和单次read()调用,这在流被压缩或缓冲时不可靠。替换为循环读取直到获取完整32字节:

static byte[] readKeyFromInputStream(final InputStream sharedKeyStream) throws IOException, NoSuchAlgorithmException {
    if (sharedKeyStream == null) {
        return autoGenerateKey();
    }
    byte[] keyBytes = new byte[32];
    int totalRead = 0;
    int read;
    while (totalRead < 32 && (read = sharedKeyStream.read(keyBytes, totalRead, 32 - totalRead)) != -1) {
        totalRead += read;
    }
    if (totalRead != 32) {
        throw new IOException("Failed to read complete 32-byte AES key, only read " + totalRead + " bytes");
    }
    LOG.infof("Activating security token handler with key from input stream (length = %d bytes)", totalRead);
    return keyBytes;
}

这种方式确保无论流的类型如何,都会读取到完整的32字节,避免单次读取不完整的问题。

方案3:集成测试中直接读取本地文件系统

在集成测试中绕过类路径,直接读取本地文件系统中的原始密钥文件:

@Test
void shouldRead32BytesFromInputStream() throws IOException, NoSuchAlgorithmException {
    // 直接读取本地src/test/resources下的原始文件
    Path keyPath = Paths.get("src/test/resources/key.shared");
    try (InputStream keyFileStream = Files.newInputStream(keyPath)) {
        byte[] keyFileBytes = TokenHandler.readKeyFromInputStream(keyFileStream);
        assertEquals(32, keyFileBytes.length);
    }
}

这种方式避免了Jar打包过程对文件的影响,确保测试使用的是未被修改的二进制文件。


内容的提问来源于stack exchange,提问作者WesternGun

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 07:25:01