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

Micronaut客户端使用无CA自签名证书的mTLS通信返回401未授权,但Postman与curl可正常工作

Micronaut客户端使用无CA自签名证书的mTLS通信返回401未授权,但Postman与curl可正常工作

我最近在配置Micronaut HTTP客户端,要和一个使用自签名证书的HTTPS服务器做双向TLS(mTLS)通信——这个服务器没有使用CA证书。我按照Micronaut的文档设置了客户端的密钥库和信任库,跳过了根CA证书,但请求一直返回401 Unauthorized错误,实在搞不懂哪里出问题了。

问题现象

  • mTLS通信在Postman和终端curl中完全正常,但用Micronaut HTTP客户端调用就返回401 Unauthorized

环境信息

  • Micronaut版本:3.9.6

客户端application.yml配置

micronaut:
  http:
    client:
      ssl:
        enabled: true
        key-store:
          path: classpath:certs/client.p12
          password: changeit
          type: PKCS12
        trust-store:
          path: classpath:certs/truststore.jks  # 直接用服务器证书作为信任库
          password: changeit
          type: JKS

预期行为

  • Micronaut HTTP客户端应该正确发送客户端证书
  • 请求不应该被401拒绝
  • 既然Postman和curl都能正常工作,Micronaut也该有同样的表现

补充排查信息

抓包发现的异常

我用Wireshark做了网络抓包,发现客户端证书的长度存在异常,这可能是服务器拒绝请求的原因。

可正常运行的Express.js参考示例

require("dotenv").config();
const fs = require("fs");
const https = require("https");
const express = require("express");
const axios = require("axios");
const qs = require("qs");

const app = express();
const PORT = process.env.PORT || 3000;

// 加载TLS证书和密钥
const options = {
    rejectUnauthorized: false,
    key: fs.readFileSync("./certs/transport.key"),
    cert: fs.readFileSync("./certs/transport.pem")
  };
  
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const TOKEN_URL = process.env.TOKEN_URL || "https://as1.lab.openbanking.sa/token";

app.use(express.json());
app.use((req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
  next();
});

// 获取访问令牌的函数
const getAccessToken = async () => {
  try {
    const authHeader = `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64")}`;

    const requestBody = qs.stringify({
      grant_type: "client_credentials",
      scope: "accounts openid",
    });

    console.log("Requesting token from API...");
    console.log(`Authorization: ${authHeader}`);
    console.log(`Request Body: ${requestBody}`);

    const response = await axios.post(TOKEN_URL, requestBody, {
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        "Authorization": authHeader,
        "Accept": "application/json"
      },
      httpsAgent: new https.Agent(options),
    });

    console.log("Token Response:", response.data);
    return response.data;
  } catch (error) {
    console.error(" API Error:", error.response ? error.response.data : error.message);
    throw error;
  }
};

app.post("/public", async (req, res) => {
  try {
    const tokenData = await getAccessToken();
    res.json(tokenData);
  } catch (error) {
    res.status(500).json({ error: "Failed to retrieve token" });
  }
});

app.listen(PORT, (error) =>{
    if(!error)
        console.log("Server is Successfully Running, and App is listening on port "+ PORT)
    else 
        console.log("Error occurred, server can't start", error);
    }
);

尝试原生方式配置SSL上下文,但问题依旧

我还试过用Java原生方式手动构建SSL上下文并配置到HttpClient,不过还是返回401错误:

public HttpResponse<String> postRequest() throws Exception {
        // 加载PKCS12格式的密钥库
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        try (FileInputStream keyFile = new FileInputStream("src/main/resources/certs/client.p12")) {
            keyStore.load(keyFile, "123456".toCharArray());
        }

        // 初始化密钥管理器工厂
        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
        kmf.init(keyStore, "123456".toCharArray());

        // 信任所有证书(绕过SSL验证)
        TrustManager[] trustAllCertificates = new TrustManager[]{
                new X509TrustManager() {
                    public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {}
                    public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {}
                    public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; }
                }
        };

        // 禁用主机名验证
        HostnameVerifier allHostsValid = (hostname, session) -> true;

        // 创建TLS 1.2的SSL上下文
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        sslContext.init(kmf.getKeyManagers(), trustAllCertificates, new SecureRandom());

        // 用自定义SSL上下文创建HTTP客户端
        HttpClient httpClient = HttpClient.newBuilder()
                .sslContext(sslContext)
                .version(HttpClient.Version.HTTP_2)
                .build();

        String formData = "grant_type=client_credentials&scope=accounts%20openid";
        String basicToken = Base64.getEncoder().encodeToString(("clientid" + ":" + "clientsecret").getBytes(StandardCharsets.UTF_8));

        // 构建POST请求
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://as1.lab.openbanking.sa/token"))
                .header("Authorization", "Basic " + basicToken)
                .header("Content-Type", "application/x-www-form-urlencoded")
                .POST(HttpRequest.BodyPublishers.ofString(formData))
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println("Response Code: " + response.statusCode());
        System.out.println("Response Body: " + response.body());

        return response;
    }

备注:内容来源于stack exchange,提问作者Vibhanshu Shekhar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 11:54:50