JXBrowser 7.37.0证书验证异常致DMS WEB客户端无法启动Outlook
环境说明
前同事将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(); } } }
问题修复方案
一、证书验证问题修复
明确证书验证错误原因
从VerifyCertificateCallback的控制台输出中查看具体错误类型(比如根证书未信任、域名不匹配、证书过期等),针对性处理。如果是内网自签证书,优先将根证书导入JXBrowser的信任链,而非强制返回Response.valid()跳过验证。添加自定义信任证书
在初始化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();修正客户端证书选择逻辑
当前代码中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(); } });调整回调注册顺序
先注册证书相关回调,再加载URL,确保首次请求就能触发验证逻辑:// 创建Engine后立即注册VerifyCertificateCallback engine.network().set(VerifyCertificateCallback.class, params -> { // ... 原有逻辑 }); // 再创建Browser并加载URL Browser browser = engine.newBrowser(); browser.navigation().loadUrl(url);
二、Outlook启动问题修复
启用外部协议弹窗
添加浏览器设置允许外部协议启动:browser.settings().allowExternalProtocolDialogs();保留用户数据目录
当前代码每次启动都会删除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)调用修复Cookie设置冲突
代码中先调用allowJavaScriptAccessCookies()又调用disallowJavaScriptAccessCookies(),最终禁用了Cookie访问,可能导致DMS登录状态丢失。删除browser.settings().disallowJavaScriptAccessCookies()这一行。
三、其他优化
- 移除
allowRunningInsecureContent()设置:HTTPS页面加载不安全内容会被浏览器拦截,当前场景不需要该配置。 - 检查
treatInsecureOriginAsSecure参数:如果dmssrv是HTTPS地址,该配置无需添加,可移除。
内容的提问来源于stack exchange,提问作者Chris

