綦霖
好记性不如烂笔头~ 持续写bug中...

项目背景:

  项目版本SpringBoot 3.4.2,jdk17,本地开发环境win11,测试环境Linux红帽+docker运行

以上为基础开发环境,另外就是开发处于内网开发限制较多,导致问题解决繁琐,首先就是项目原来使用RestTemplate方法,网上找了好多有用httpclient5的有用httpclient4.5的我都验证不太好使,历时三天将本地开发环境以及测试环境调试通了,可能不是最优解,但幸运的是问题算是解决了,话不用多说上代码

解决方案一:跳过证书认证,亲测好用但根据要求不能用在生产环境上,测试验证接口通了以后只能含泪放弃此方案。

解决方案二:

Pom

使用的是jdk原生自带的HttpClient

<properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <!-- 添加Spring Boot父POM -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <!-- 添加Spring Boot常规依赖 -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
        </dependency>
        <!-- oracle -->
        <dependency>
            <groupId>com.hynnet</groupId>
            <artifactId>oracle-driver-ojdbc_g</artifactId>
            <version>12.1.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.34</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.9</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.7</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>33.3.1-jre</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>10.1.39</version>
        </dependency>
        <!--knife4j api工具-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
            <version>4.3.0</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.4</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-jsqlparser</artifactId>
            <version>3.5.10</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.2.5</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring</artifactId>
            <version>3.5.10</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.19</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.19</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.113.Final</version>
        </dependency>

增加配置类

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import javax.net.ssl.SSLContext;
import java.io.InputStream;
import java.net.http.HttpClient;
import java.security.KeyStore;
import java.time.Duration;

@Configuration
@EnableConfigurationProperties(SslProperties.class)
public class JavaHttpClientSSLConfig {

    private final SslProperties sslProperties;
    public JavaHttpClientSSLConfig(SslProperties sslProperties) {
        this.sslProperties = sslProperties;
    }
    @Bean
    public RestTemplate sslRestTemplate() throws Exception {
        // 创建自定义的SSL上下文
        SSLContext sslContext = createSSLContext();

        // 使用Java内置的HttpClient
        HttpClient httpClient = HttpClient.newBuilder()
                .sslContext(sslContext)
                .connectTimeout(Duration.ofSeconds(5))
                .build();

        // 创建RequestFactory
        JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory(httpClient);
        factory.setReadTimeout(Duration.ofSeconds(10));

        return new RestTemplate(factory);
    }

    private SSLContext createSSLContext() throws Exception {
        if (sslProperties == null) {
            throw new IllegalStateException("SSL properties not configured properly");
        }
        // 加载密钥库
        KeyStore keyStore = KeyStore.getInstance(sslProperties.getTrustStoreType());
        ClassPathResource resource = new ClassPathResource(sslProperties.getCertFile());
        try (InputStream inputStream = resource.getInputStream()) {
            keyStore.load(inputStream, sslProperties.getKeyPassword().toCharArray());
        }

        // 创建KeyManagerFactory
        javax.net.ssl.KeyManagerFactory keyManagerFactory = 
            javax.net.ssl.KeyManagerFactory.getInstance(
                javax.net.ssl.KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore,  sslProperties.getKeyPassword().toCharArray());

        // 创建SSL上下文
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(keyManagerFactory.getKeyManagers(), null, null);

        return sslContext;
    }
}

配置类

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "client.ssl")
@Data
public class SslProperties {
    private String certFile;
    private String keyFile;
    private String keyPassword;
    private String caCertFile;
    private String trustStoreType;

    // Getters and Setters
}

yml文件配置

#####################################证书-配置######################################
client:
  ssl:
    # PEM 证书配置
    cert-file: certs/dev/app-xxx.p12
    key-file: certs/dev/app-xxx.key
    key-password: XXX@2025  # 私钥密码(如果没有密码则留空)
    trust-store-type: PKCS12
  # 可选:CA 证书(如果服务器使用自签名证书)

  #    ca-cert-file: certs/dev/accc.pem

  # API 配置
  api:
    base-url: https://app-xxx
    cti-endpoint: /api/online-xxx/xxc
    timeout: 30000
    max-connections: 20

在Appliction 里注入RestTemplate

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@MapperScan("com.xxx.xxxx.mapper")
public class BmwCtiServerAppliction {
    public static void main(String[] args) {
        org.springframework.boot.SpringApplication.run(BmwCtiServerAppliction.class, args);
    }
    public ResponseEntity<?> respionseEntity(){
        return ResponseEntity.ok().build();
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate(); // 直接注册到容器
    }
}

 

这个时候调用接口会提示证书未被信任,调用接口仍旧失败,提示

2025-08-27 17:45:45.019 |-ERROR [http-nio-8080-exec-1] org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/].[dispatcherServlet] [175] -| Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.web.client.ResourceAccessException: I/O error on POST request for "https://app-xxx.cctrsa-int.china.ali.cloud.xxx/api/xxx-online-xxx/cti": PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target] with root cause
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

image

 

此时需要

在服务器上将域名证书下载下来,另存为pem文件

openssl s_client -connect app-icbm.cctrsa-dev.china.ali.cloud.bmw:443 -showcerts </dev/null 2>/dev/null | openssl x509 -outform PEM > server-dev-cert.pem
openssl s_client -connect app-icbm.cctrsa-int.china.ali.cloud.bmw:443 -showcerts </dev/null 2>/dev/null | openssl x509 -outform PEM > server-int-cert.pem

找到你的jdk目录用管理员CMD执行下方指令
keytool -importcert -alias bmw-icbm-dev-cert -keystore D:\YC\java7\jdk-17.0.12\lib\security\cacerts -file C:\certs\server-dev-cert.pem -storepass changeit
keytool -importcert -alias bmw-icbm-int-cert -keystore D:\YC\java7\jdk-17.0.12\lib\security\cacerts -file C:\certs\server-int-cert.pem -storepass changeit

最后本地测试可以调通

 如上为本地开发环境调用成功,下面开始调试服务器上的docker应用,其实原理都一样,就是代码不变的情况下在jdk里增加Java信任库,由于docker镜像启动,想要修改jdk的话就得在镜像上面研究,本来最优解就是重新打一个包含这次调用域名的jdk专用镜像,奈何内网开发有诸多限制,尝试了几次只能无奈作罢,新的思路是在docker run的时候引入信任库,这样改动最小,docker run指令如下:

docker run --name xxxServer --restart=always \
  --add-host="app-xxx.cctrsa-int.china.ali.cloud.xx:10.86.161.xxx" \
  --add-host="app-xxx.cctrsa-dev.china.ali.cloud.xx:10.86.158.xx" \
  -v /data/wwwlogs/xxxServer:/data/wwwlogs/xxxServer \
  -v /data/app/xxxServer/server-int-cert.pem:/tmp/xx-cert.pem:ro \
  -p 8090:8080 \
  -e ENVO=int \
  -d registry.xxx-xx.com.cn/microservice/xxxserver-uat:0.0.2 \
  sh -c "keytool -importcert -noprompt -alias xxx-icbm-dev -keystore \$JAVA_HOME/lib/security/cacerts -file /tmp/xx-cert.pem -storepass changeit && exec java -jar /app/app.jar"
  

大概意思就是在docker启动时将信任证书加入jdk,想法没错思路也对,不出意外还是出了意外,翻车了,启动之后进入镜像检查证书最开始查不到后来修改docker run再之后能查到但是还是不行,灵机一动想是不是证书没有加到用的jdk里面去,先放一下检查相关指令留作备用:

# 进入镜像
docker exec -it {appname} bash
echo "=== Java环境检查 ==="
java -version
ls -l $(which java)
echo "=== keytool检查 ==="
which keytool
keytool -help 2>&1 | head -5
 echo "=== 证书文件检查 ==="
 ls -la /tmp/xxx-cert.pem
 file /tmp/xxx-cert.pem 2>/dev/null || echo "file命令不可用,使用替代方法检查证书"
head -3 /tmp/bmw-cert.pem
echo "=== 尝试读取证书信息 ==="
 keytool -printcert -file /tmp/bmw-cert.pem
 echo ""echo "=== cacerts文件检查 ==="
 echo "=== 检查其他cacerts位置 ==="
 find / -name "cacerts" 2>/dev/null | head -5
 echo "=== 验证导入结果 ==="
 keytool -list -keystore /usr/local/jdk1.8.0_191/jre/lib/security/cacerts -storepass changeit | grep -i bmw
#其实查到这儿的时候大概就是知道原因了,原因就是这个jdk镜像是由1.8jdk后期修改的17,所以镜像里的javaHome有点乱并且镜像里面为1.8的版本和位置,简而言之一句话就是jdk信任证书加错位置了,下面是搜的指令

5. 检查证书文件本身是否有问题
bash
# 在宿主机上检查证书文件
cat /data/wwwlogs/xxxServer/server-int-cert.pem


# 确保证书格式正确(应该包含)
# -----BEGIN CERTIFICATE-----
# ...证书内容...
# -----END CERTIFICATE-----


# 如果证书格式不对,重新获取
openssl s_client -connect app-x.cctrsa-int.china.ali.cloud.x:443 -showcerts </dev/null | openssl x509 -outform PEM > /data/app/xxxServer/server-int-cert.pem
验证步骤
运行后检查:


bash
# 查看容器日志
docker logs xxServer


# 进入容器验证
docker exec -it xxServer bash


# 检查证书是否导入
keytool -list -keystore /usr/local/jdk1.8.0_191/jre/lib/security/cacerts -storepass changeit | grep -i bmw


# 或者检查自定义信任库
keytool -list -keystore /app/security/custom-cacerts -storepass changeit | grep -i bmw
推荐先运行带调试信息的命令,这样可以清楚地看到哪里出了问题。如果证书导入一直失败,建议使用方法B创建自定义信任库。

 

这个时候还没死心,想着docker run能解决的事情干毛要搞镜像,毕竟时间紧任务重(md天天加班到八九点),以下为突发奇想(胡思乱想)的docker run,希望有个有缘人能用到吧

#第一版
docker run --name xxxServer --restart=always \
  --add-host="app-x.cctrsa-int.china.ali.cloud.x:10.86.161.x" \
  --add-host="app-x.cctrsa-dev.china.ali.cloud.x:10.86.158.x" \
  -v /data/wwwlogs/xxxServer:/data/wwwlogs/xxxServer \
  -v /data/app/xxxServer/server-dev-cert.pem:/tmp/xxx-cert.pem:ro \
  -p 8090:8080 \
  -e ENVO=test \
  -d registry.x-assistance.com.cn/microservice/xxxserver-uat:0.0.3 \
  sh -c "keytool -importcert -noprompt -alias bmw-icbm-dev -keystore /date/jdk-17.0.13/lib/security/cacerts -file /tmp/bmw-cert.pem -storepass changeit && exec java -jar /app/app.jar"


#第二版
docker run --name xServer --restart=always \
  --add-host="app-x.cctrsa-int.china.ali.cloud.x:10.86.161.1x" \
  --add-host="app-x.cctrsa-dev.china.ali.cloud.x:10.86.158.1x" \
  -v /data/wwwlogs/xServer:/data/wwwlogs/xServer \
  -v /data/wwwlogs/xServer/server-int-cert.pem:/tmp/x-cert.pem:ro \
  -p 8090:8080 \
  -e ENVO=int \
  -d registry.x-assistance.com.cn/microservice/xserver-uat:0.0.3 \
  sh -c "
    # 查找Java home和cacerts文件
    JAVA_HOME=\$(dirname \$(dirname \$(readlink -f \$(which java || command -v java))))
    echo \"Java Home: \$JAVA_HOME\"
    
    # 尝试多个可能的cacerts路径
    CACERTS_PATH=\"\"
    for path in \"\$JAVA_HOME/lib/security/cacerts\" \"\$JAVA_HOME/jre/lib/security/cacerts\" \"/etc/ssl/certs/java/cacerts\"; do
        if [ -f \"\$path\" ]; then
            CACERTS_PATH=\"\$path\"
            break
        fi
    done
    
    if [ -z \"\$CACERTS_PATH\" ]; then
        echo \"Creating custom truststore...\"
        mkdir -p /app/security
        CACERTS_PATH=\"/app/security/custom-cacerts\"
        # 创建新的信任库
        keytool -genkeypair -keyalg RSA -alias temp -keystore \"\$CACERTS_PATH\" -storepass changeit -keypass changeit -dname \"CN=Temp\" -validity 1
        keytool -delete -alias temp -keystore \"\$CACERTS_PATH\" -storepass changeit
    fi
    
    echo \"Using truststore: \$CACERTS_PATH\"
    
    # 导入证书
    keytool -importcert -noprompt -alias bmw-icbm-int -keystore \"\$CACERTS_PATH\" -file /tmp/bmw-cert.pem -storepass changeit
    
    # 验证证书导入
    echo \"Checking imported certificates:\"
    keytool -list -keystore \"\$CACERTS_PATH\" -storepass changeit | grep bmw-icbm-int
    
    # 设置Java选项使用正确的信任库
    export JAVA_OPTS=\"\$JAVA_OPTS -Djavax.net.ssl.trustStore=\$CACERTS_PATH -Djavax.net.ssl.trustStorePassword=changeit\"
    
    echo \"Starting application with JAVA_OPTS: \$JAVA_OPTS\"
    exec java \$JAVA_OPTS -jar /app/app.jar
  "

#第三版
docker run --name xServer --restart=always \
  --add-host="app-x.cctrsa-int.china.ali.cloud.x:10.86.161.1x" \
  --add-host="app-x.cctrsa-dev.china.ali.cloud.bmw:10.86.158.1x" \
  -v /data/wwwlogs/xServer:/data/wwwlogs/xServer \
  -v /data/app/xServer/server-int-cert.pem:/tmp/x-cert.pem:ro \
  -p 8090:8080 \
  -e ENVO=int \
  -d registry.x-assistance.com.cn/microservice/xserver-uat:0.0.3 \
  sh -c "
    echo '=== 开始导入证书 ==='
    
    # 导入到所有找到的cacerts文件
    for cacerts in \
      /usr/local/jdk1.8.0_191/jre/lib/security/cacerts \
      /date/jdk-17.0.13/lib/security/cacerts \
      /etc/pki/java/cacerts \
      /etc/pki/ca-trust/extracted/java/cacerts; do
      
      if [ -f \"\$cacerts\" ]; then
        echo \"导入证书到: \$cacerts\"
        keytool -importcert -noprompt -alias x-icbm-int -keystore \"\$cacerts\" -file /tmp/x-cert.pem -storepass changeit
        # 验证导入
        keytool -list -keystore \"\$cacerts\" -storepass changeit | grep -i bmw && echo \"导入成功\" || echo \"导入可能失败\"
        echo ''
      fi
    done
    
    echo '=== 证书导入完成 ==='
    echo '检查当前Java版本:'
    java -version
    echo ''
    
    # 确定主Java路径并设置正确的信任库
    JAVA_HOME=\$(dirname \$(dirname \$(readlink -f \$(which java))))
    echo \"检测到JAVA_HOME: \$JAVA_HOME\"
    
    MAIN_CACERTS=\"\$JAVA_HOME/jre/lib/security/cacerts\"
    if [ -f \"\$MAIN_CACERTS\" ]; then
        export JAVA_OPTS=\"\$JAVA_OPTS -Djavax.net.ssl.trustStore=\$MAIN_CACERTS -Djavax.net.ssl.trustStorePassword=changeit\"
        echo \"设置主信任库: \$MAIN_CACERTS\"
    fi
    
    echo '启动应用...'
    exec java \$JAVA_OPTS -jar /app/app.jar
  "
#第四版
docker run --name xServer --restart=always \
  --add-host="app-x.cctrsa-int.china.ali.cloud.x:10.86.161.x" \
  --add-host="app-x.cctrsa-dev.china.ali.cloud.x:10.86.158.1x" \
  -v /data/wwwlogs/xServer:/data/wwwlogs/xServer \
  -v /data/app/xServer/server-int-cert.pem:/tmp/x-cert.pem \
  -p 8090:8080 \
  -e ENVO=int \
  -d registry.x-assistance.com.cn/microservice/xserver-uat:0.0.3 \
  sh -c "
    set -x  # 开启详细调试输出
    
    echo '=== 调试信息开始 ==='
    
    # 设置Java环境
    export JAVA_HOME=/usr/local/jdk1.8.0_191
    export PATH=\$JAVA_HOME/bin:\$PATH
    
    echo 'Java版本:'
    java -version
    echo ''
    
    echo 'keytool位置:'
    which keytool
    echo ''
    
    echo '证书文件信息:'
    ls -la /tmp/bmw-cert.pem
    echo ''
    
    echo '证书文件内容开头:'
    head -3 /tmp/bmw-cert.pem
    echo ''
    
    echo '证书文件内容结尾:'
    tail -3 /tmp/bmw-cert.pem
    echo ''
    
    # 先尝试打印证书信息
    echo '尝试读取证书信息:'
    keytool -printcert -file /tmp/bmw-cert.pem || echo '打印证书失败'
    echo ''
    
    # 导入证书
    CACERTS=\$JAVA_HOME/jre/lib/security/cacerts
    echo '导入证书到: '\$CACERTS
    
    # 检查cacerts文件是否存在
    if [ -f \"\$CACERTS\" ]; then
        echo 'cacerts文件存在,开始导入...'
        keytool -importcert -v -noprompt -alias bmw-icbm-int -keystore \"\$CACERTS\" -file /tmp/bmw-cert.pem -storepass changeit
        echo '导入完成,退出代码: '\$?
        
        # 验证导入
        echo '验证导入结果:'
        keytool -list -keystore \"\$CACERTS\" -storepass changeit | grep -i bmw
        echo '验证完成'
    else
        echo '错误: cacerts文件不存在于 '\$CACERTS
        echo '找到的cacerts文件:'
        find / -name cacerts 2>/dev/null
    fi
    
    echo '=== 调试信息结束 ==='
    
    # 启动应用
    export JAVA_OPTS=\"-Djavax.net.ssl.trustStore=\$CACERTS -Djavax.net.ssl.trustStorePassword=changeit\"
    exec java \$JAVA_OPTS -jar /app/app.jar
  "

以上个人觉得没问题然后就是和我实际情况不一样(如果jdk镜像没问题可以尝试这个方法)

故此,开始思索打jdk镜像的主意但是网络不支持,启动的时候又因为基础镜像不行,询问了一下同事我俩一致觉得可以在打服务镜像的时候修正jdkJavaHome。于是乎,能解决问题的方法信息出炉了

就是先在项目中放Dockerfile的位置把证书放好一会导入的时候使用项目结构大致为

image

 然后修改Dockerfile:

FROM registry.xxx-assistance.com.cn/java/jdk:17.0.13
WORKDIR /

# 设置正确的JDK路径(根据实际查找结果)
ENV JAVA_HOME=/date/jdk-17.0.13
ENV PATH=$JAVA_HOME/bin:$PATH

# 验证JDK版本(构建时确认正确性)
RUN echo "使用的JDK路径: $JAVA_HOME" && \
    java -version && \
    javac -version

# 添加证书文件到临时目录
ADD certs/server-int-cert.pem /tmp/server-int-cert.pem
ADD certs/server-dev-cert.pem /tmp/server-dev-cert.pem

# 导入证书到正确的Java信任库并清理
RUN keytool -importcert -trustcacerts -file /tmp/server-int-cert.pem \
    -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit -noprompt -alias server-int-cert && \
    keytool -importcert -trustcacerts -file /tmp/server-dev-cert.pem \
    -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit -noprompt -alias server-dev-cert && \
    rm -f /tmp/server-int-cert.pem /tmp/server-dev-cert.pem

# 添加应用程序JAR包
ADD xxxServer-0.0.1-SNAPSHOT.jar /xxxServer-0.0.1-SNAPSHOT.jar

# 环境配置
ENV ENVO="dev"
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo "$TZ" > /etc/timezone
ENV LANG en_US.UTF-8

# 启动命令(保留source /etc/profile确保环境变量生效)
ENTRYPOINT [ "sh", "-c", "source /etc/profile && java -Djava.security.egd=file:/dev/./urandom -jar /xxxServer-0.0.1-SNAPSHOT.jar --spring.profiles.active=$ENVO" ]

如此,问题解决。。。。。。。。

继续快(ku)乐(bi)的调试接口去了┭┮﹏┭┮

 

posted on 2025-08-28 09:52  綦霖  阅读(38)  评论(0)    收藏  举报