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

JXBrowser 7.37.0证书验证异常致DMS WEB客户端无法启动Outlook

JxBrowser 7.37.0 适配HTTPS后证书验证及外部应用启动问题

环境说明

前同事将JXBrowser 7.37.0集成到Java软件中,用于连接客户内网的文档管理系统(DMS)。用户通过Windows账号登录DMS,客户端已安装DMS的WEB客户端及证书,WEB客户端原本可启动Outlook发送文档。

问题现象

DMS从HTTP切换到HTTPS后,页面仅显示浏览器需重载的提示,WEB客户端功能失效,无法启动Outlook。

测试情况

编写了测试程序,实现了VerifyCertificateCallback、CertificateErrorCallback、SelectClientCertificateCallback等多个回调及相关浏览器设置,但测试时VerifyCertificateCallback始终触发,DMS服务器证书无法完成验证。

测试程序源码:

package demon;

import com.teamdev.jxbrowser.browser.Browser;
import com.teamdev.jxbrowser.engine.Engine;
import com.teamdev.jxbrowser.engine.EngineOptions;
import com.teamdev.jxbrowser.engine.event.EngineClosed;
import com.teamdev.jxbrowser.engine.event.EngineCrashed;
import com.teamdev.jxbrowser.view.swing.BrowserView;
import com.teamdev.jxbrowser.net.callback.AuthenticateCallback;
import com.teamdev.jxbrowser.net.callback.VerifyCertificateCallback;
import com.teamdev.jxbrowser.net.callback.VerifyCertificateCallback.Response;
import com.teamdev.jxbrowser.net.tls.CertVerificationError;
import com.teamdev.jxbrowser.net.tls.Certificate;
import com.teamdev.jxbrowser.browser.callback.CertificateErrorCallback;
import com.teamdev.jxbrowser.browser.callback.OpenExternalAppCallback;
import com.teamdev.jxbrowser.browser.callback.SelectClientCertificateCallback;

import static com.teamdev.jxbrowser.engine.RenderingMode.HARDWARE_ACCELERATED;

import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.Properties;

public class JxBrowserApp {

    public static void main(String[] args) {

        // Load settings from App.config
        Properties props = new Properties();

        try (FileInputStream fis = new FileInputStream(Paths.get("config", "App.config").toString())) {

            props.load(fis);

        } catch (IOException e) {

            System.err.println("Error loading App.config: " + e.getMessage());
            System.exit(0);

        }

        String dateDir = Paths.get(System.getProperty("user.dir"), "directory").toString();

        File dir = new File(dateDir);

        if (!(dir.exists() && dir.isDirectory())) {

            try {
                Files.createDirectories(Paths.get(dateDir));
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {

            deleteDirectory(dateDir);

        }

        String userDataDir = Paths.get(dateDir, "user").toString() + System.getProperty("file.separator");
        String chromiumDir = Paths.get(dateDir, "chromium").toString() + System.getProperty("file.separator");
        String crashDumpDir = Paths.get(dateDir, "CrashReports").toString() + System.getProperty("file.separator");

        dir = new File(userDataDir);

        if (!(dir.exists() && dir.isDirectory())) {

            try {
                Files.createDirectories(Paths.get(userDataDir));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        dir = new File(chromiumDir);

        if (!(dir.exists() && dir.isDirectory())) {

            try {
                Files.createDirectories(Paths.get(chromiumDir));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        dir = new File(crashDumpDir);

        if (!(dir.exists() && dir.isDirectory())) {

            try {
                Files.createDirectories(Paths.get(crashDumpDir));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        System.setProperty("jxbrowser.crash.dump.dir", crashDumpDir);

        String url = "https://dmssrv";

        boolean devTools = (props.getProperty("devTools", "false").toLowerCase().equals("true") ? true : false);

        String authenticateUsername = props.getProperty("authenticateUsername", "");
        String authenticatePassword = props.getProperty("authenticatePassword", "");

        // Initialize Chromium.
        EngineOptions options = null;

        options = EngineOptions.newBuilder(HARDWARE_ACCELERATED)
                               .treatInsecureOriginAsSecure("https://dmsrv")
                               .userDataDir(Paths.get(userDataDir))
                               .chromiumDir(Paths.get(chromiumDir))
                               .build();

        Engine engine = Engine.newInstance(options);

        engine.network().httpAuthPreferences().serverWhitelist("dmssrv");

        engine.network().httpAuthPreferences().delegateWhitelist("dmssrv");

        engine.on(EngineClosed.class, event -> {
            System.out.println("CLOSE ENGINE\n");
        });

        engine.on(EngineCrashed.class, event -> {
            System.out.println("CRASHED ENGINE: ExitCode = " + event.exitCode() + "\n");
        });

        // Create a Browser instance.
        Browser browser = engine.newBrowser();

        // Allows JavaScript code on the web pages loaded in the browser to
        // access clipboard.
        browser.settings().allowJavaScriptAccessClipboard();

        // Allows JavaScript code on the web pages loaded in the browser to
        // read/write cookies in the cookies storage using the document.cookie
        // property.
        browser.settings().allowJavaScriptAccessCookies();

        // Allows running an insecure content in the browser.
        browser.settings().allowRunningInsecureContent();

        // Enables the local storage in the browser.
        browser.settings().enableLocalStorage();

        // Enables all plugins on the web pages loaded in the browser.
        browser.settings().enablePlugins();

        // Enables JavaScript on the web pages loaded in the browser.
        browser.settings().enableJavaScript();

        browser.set(OpenExternalAppCallback.class, (param, tell) -> {

            System.out.println("ExternalApp.title: " + param.title());
            System.out.println("ExternalApp.message: " + param.message());
            tell.open();
        });

        engine.network().set(AuthenticateCallback.class, (param, tell) -> {

            System.out.println("AuthenticateCallback.browser: " + param.browser());
            System.out.println("AuthenticateCallback.hostPort: " + param.hostPort());
            System.out.println("AuthenticateCallback.isProxy: " + param.isProxy());
            System.out.println("AuthenticateCallback.url: " + param.url());
            System.out.println("AuthenticateCallback.scheme: " + param.scheme());

            if (authenticateUsername.toLowerCase().equals("none") &&
                authenticatePassword.toLowerCase().equals("none")) {
                
            } else if (authenticateUsername.equals("") && authenticatePassword.equals("")) {
                tell.authenticate("<username>", "<password>");
            } else {
                tell.authenticate(authenticateUsername, authenticatePassword);
            }
        });

        browser.set(SelectClientCertificateCallback.class, (params, tell) -> {

            String host = params.hostPort().toString();
            System.out.println("useSystemCertificateStore for " + host + "...");

            // The list of the installed and available client certificates.
            java.util.List<Certificate> certificates = params.certificates();

            certificates.forEach(e -> {
                System.out.println("useSystemCertificateStore:\n" + e);
            });

            // Select the all client certificate in the list of available
            // client certificates.
            tell.select(certificates.size());

        });

        browser.settings().disallowJavaScriptAccessCookies();

        // Load the required web page.
        //browser.navigation().loadUrlAndWait(url);
        browser.navigation().loadUrl(url);

        if (devTools) {
            System.out.println("devTools: eingestellt\n");
            browser.devTools().show();
        } else {
            System.out.println("devTools: nicht eingestellt\n");
        }

        engine.network().set(VerifyCertificateCallback.class, params -> {

            String host = params.host().value();
            System.out.println("VerifyCertificateCallback: Verifying certificate for " + host);
            System.out.println("VerifyCertificateCallback.certificate = " + params.certificate());
            System.out.println("VerifyCertificateCallback.host = " + params.host());
            System.out.println("VerifyCertificateCallback.intermediateCertificates = " + params.intermediateCertificates());

            // SSL Certificate to verify.
            Certificate certificates = params.certificate();

            // The results of the verification performed by default verifier.
            java.util.List<Certificate> certs = params.intermediateCertificates();

            for (Certificate cert : certs) {
                System.out.println("IntermediateCertificates: " + cert);
            }

            // The results of the verification performed by default verifier.
            java.util.List<CertVerificationError> errors = params.verificationErrors();

            for (CertVerificationError error : errors) {
                System.out.println("VerifyCertificateCallback (Error): " + host);
                System.out.println("Status = " + error.status());
                System.out.println("Short Description = " + error.shortDescription());
                System.out.println("Detailed Description = " + error.detailedDescription());
            }

            return Response.valid();
        });

        browser.set(CertificateErrorCallback.class, (params, tell) -> {

            System.out.println("CertificateErrorCallback.url = " + params.url());
            System.out.println("CertificateErrorCallback.error = " + params.error());
            System.out.println("CertificateErrorCallback.isMainFrame = " + params.isMainFrame());
            System.out.println("CertificateErrorCallback.certificate = " + params.certificate());
            tell.allow();
        });

        SwingUtilities.invokeLater(() -> {

            JFrame frame = new JFrame("JxBrowser AWT/Swing");

            frame.addWindowListener(new WindowAdapter() {

                @Override
                public void windowClosing(WindowEvent e) {

                    // Shutdown Chromium and release allocated resources.
                    engine.close();
                    System.exit(0);

                }

            });

            // Create and embed Swing BrowserView component to display web content.
            frame.add(BrowserView.newInstance(browser), BorderLayout.CENTER);
            frame.setSize(1280, 800);
            frame.setExtendedState(JFrame.MAXIMIZED_BOTH); 
            frame.setUndecorated(false);
            frame.setVisible(true);

        });

    }

    public static void deleteDirectory(String directoryPath) {

        Path path = Paths.get(directoryPath);

        try {

            Files.walk(path)
                 .sorted(Comparator.reverseOrder())
                 .map(Path::toFile)
                 .forEach(File::delete);

        } catch (IOException ex) {

            ex.printStackTrace();

        }

    }

}

问题修复方案

一、证书验证问题修复

  1. 明确证书验证错误原因
    从VerifyCertificateCallback的控制台输出中查看具体错误类型(比如根证书未信任、域名不匹配、证书过期等),针对性处理。如果是内网自签证书,优先将根证书导入JXBrowser的信任链,而非强制返回Response.valid()跳过验证。

  2. 添加自定义信任证书
    在初始化EngineOptions时直接添加内网根证书,让Chromium自动信任:

    // 加载本地根证书文件
    Path rootCertPath = Paths.get("path/to/your/root_ca.crt");
    byte[] rootCertBytes = Files.readAllBytes(rootCertPath);
    
    EngineOptions options = EngineOptions.newBuilder(HARDWARE_ACCELERATED)
            .addTrustedCertificate(rootCertBytes)
            .userDataDir(Paths.get(userDataDir))
            .chromiumDir(Paths.get(chromiumDir))
            .build();
    
  3. 修正客户端证书选择逻辑
    当前代码中tell.select(certificates.size())是错误的,select方法需要传入证书在列表中的索引(从0开始)。如果要选择第一个可用证书,修改为:

    browser.set(SelectClientCertificateCallback.class, (params, tell) -> {
        String host = params.hostPort().toString();
        System.out.println("useSystemCertificateStore for " + host + "...");
    
        java.util.List<Certificate> certificates = params.certificates();
        certificates.forEach(e -> System.out.println("useSystemCertificateStore:\n" + e));
    
        if (!certificates.isEmpty()) {
            tell.select(0); // 选择第一个证书
        } else {
            tell.cancel();
        }
    });
    
  4. 调整回调注册顺序
    先注册证书相关回调,再加载URL,确保首次请求就能触发验证逻辑:

    // 创建Engine后立即注册VerifyCertificateCallback
    engine.network().set(VerifyCertificateCallback.class, params -> {
        // ... 原有逻辑
    });
    
    // 再创建Browser并加载URL
    Browser browser = engine.newBrowser();
    browser.navigation().loadUrl(url);
    

二、Outlook启动问题修复

  1. 启用外部协议弹窗
    添加浏览器设置允许外部协议启动:

    browser.settings().allowExternalProtocolDialogs();
    
  2. 保留用户数据目录
    当前代码每次启动都会删除dateDir,导致Chromium的协议关联、证书缓存被清空。修改逻辑,只在首次启动时创建目录,不重复删除:

    String dateDir = Paths.get(System.getProperty("user.dir"), "directory").toString();
    File dir = new File(dateDir);
    if (!dir.exists()) {
        try {
            Files.createDirectories(Paths.get(dateDir));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    // 移除原有的deleteDirectory(dateDir)调用
    
  3. 修复Cookie设置冲突
    代码中先调用allowJavaScriptAccessCookies()又调用disallowJavaScriptAccessCookies(),最终禁用了Cookie访问,可能导致DMS登录状态丢失。删除browser.settings().disallowJavaScriptAccessCookies()这一行。

三、其他优化

  • 移除allowRunningInsecureContent()设置:HTTPS页面加载不安全内容会被浏览器拦截,当前场景不需要该配置。
  • 检查treatInsecureOriginAsSecure参数:如果dmssrv是HTTPS地址,该配置无需添加,可移除。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 05:14:52