自定义Trust Manager与Hostname Verifier合规实现求助(SonarCloud检测不通过)
问题背景
向第三方服务发送POST请求时,使用了全信任的DummyTrustManager和DummyHostnameVerifier,导致无法通过SonarCloud检测,检测抛出以下安全违规:
- S4423:信任管理器不应盲目信任所有SSL证书,会暴露在中间人攻击风险下
- S5527:主机名验证器不应接受任意主机名,绕过SSL主机名校验存在安全隐患
原代码如下:
public static class DummyTrustManager implements X509TrustManager { public DummyTrustManager() { } public boolean isClientTrusted(X509Certificate cert[]) { return true; } public boolean isServerTrusted(X509Certificate cert[]) { return true; } public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { } public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { } } public static class DummyHostnameVerifier implements HostnameVerifier { public boolean verify(String urlHostname, String certHostname) { return true; } public boolean verify(String arg0, SSLSession arg1) { return true; } } public String nsdlResponseLine(String data, String signature){ String line = null; try { String urlOfNsdl = nsdlKycVerificationUrl; final String version = nsdlKycVerificationVersion; SSLContext sslcontext = SSLContext.getInstance("TLSv1.2"); sslcontext.init(new KeyManager[0], new TrustManager[]{new DummyTrustManager()}, new SecureRandom()); SSLSocketFactory factory = sslcontext.getSocketFactory(); String urlParameters = getUrlParameters(data, signature, version); URL url = new URL(urlOfNsdl); connection = (HttpsURLConnection) url.openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); connection.setRequestProperty("Content-Length", "" + Integer.toString(urlParameters.getBytes().length)); connection.setRequestProperty("Content-Language", "en-US"); connection.setUseCaches(false); connection.setDoInput(true); connection.setDoOutput(true); connection.setSSLSocketFactory(factory); connection.setHostnameVerifier(new DummyHostnameVerifier()); OutputStream os = connection.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(os); osw.write(urlParameters); osw.flush(); osw.close(); InputStream is = connection.getInputStream(); BufferedReader in = new BufferedReader(new InputStreamReader(is)); line = in.readLine(); is.close(); in.close(); } catch (Exception e) { log.debug("::Exception: {}",e.getMessage()); } return line; } private String getUrlParameters(String data, String signature, String version) throws UnsupportedEncodingException { return "data=" + URLEncoder.encode(data, "UTF-8") + "&signature=" + URLEncoder.encode(signature, "UTF-8") + "&version=" + URLEncoder.encode(version, "UTF-8"); }
合规实现方案
1. 自定义TrustManager:仅信任指定第三方证书
不再盲目信任所有证书,仅校验并信任第三方服务的合法证书,可通过加载本地证书文件实现。
示例代码:
public static class TrustedCertTrustManager implements X509TrustManager { private final X509Certificate trustedCert; public TrustedCertTrustManager(X509Certificate trustedCert) { this.trustedCert = trustedCert; } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { // 若无需验证客户端证书,保持空实现即可 } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { boolean isTrusted = false; // 遍历服务端证书链,校验是否包含信任的证书 for (X509Certificate cert : chain) { // 校验证书有效期 cert.checkValidity(); try { // 验证证书签名是否匹配信任证书的公钥 cert.verify(trustedCert.getPublicKey()); isTrusted = true; break; } catch (Exception e) { // 单个证书验证失败,继续检查下一个 } } if (!isTrusted) { throw new CertificateException("服务端证书未通过信任校验"); } } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[]{trustedCert}; } // 加载本地X.509证书的工具方法 public static X509Certificate loadCertificate(InputStream certStream) throws CertificateException { CertificateFactory cf = CertificateFactory.getInstance("X.509"); return (X509Certificate) cf.generateCertificate(certStream); } }
2. 自定义HostnameVerifier:严格校验主机名
仅允许指定的合法主机名通过校验,或直接复用JDK默认的标准主机名验证逻辑。
示例代码:
public static class StrictHostnameVerifier implements HostnameVerifier { private final Set<String> allowedHostnames; public StrictHostnameVerifier(Set<String> allowedHostnames) { this.allowedHostnames = allowedHostnames; } @Override public boolean verify(String hostname, SSLSession session) { // 优先校验是否在允许列表中,再调用默认逻辑处理通配符等场景 if (allowedHostnames.contains(hostname)) { return true; } return HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session); } @Override public boolean verify(String urlHostname, String certHostname) { return HttpsURLConnection.getDefaultHostnameVerifier().verify(urlHostname, certHostname); } }
3. 改造后的请求发送代码
public String nsdlResponseLine(String data, String signature){ String line = null; try { String urlOfNsdl = nsdlKycVerificationUrl; final String version = nsdlKycVerificationVersion; // 从本地资源加载第三方信任证书(替换为实际证书路径) InputStream certInputStream = getClass().getResourceAsStream("/nsdl-trust-cert.crt"); X509Certificate trustedCert = TrustedCertTrustManager.loadCertificate(certInputStream); // 初始化安全的SSL上下文 SSLContext sslcontext = SSLContext.getInstance("TLSv1.2"); sslcontext.init(new KeyManager[0], new TrustManager[]{new TrustedCertTrustManager(trustedCert)}, new SecureRandom()); SSLSocketFactory factory = sslcontext.getSocketFactory(); String urlParameters = getUrlParameters(data, signature, version); URL url = new URL(urlOfNsdl); connection = (HttpsURLConnection) url.openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); connection.setRequestProperty("Content-Length", "" + Integer.toString(urlParameters.getBytes().length)); connection.setRequestProperty("Content-Language", "en-US"); connection.setUseCaches(false); connection.setDoInput(true); connection.setDoOutput(true); connection.setSSLSocketFactory(factory); // 设置允许的合法主机名(替换为第三方服务实际域名) Set<String> allowedHosts = new HashSet<>(Arrays.asList("official.nsdl-service.com")); connection.setHostnameVerifier(new StrictHostnameVerifier(allowedHosts)); OutputStream os = connection.getOutputStream(); OutputStreamWriter osw = new OutputStreamWriter(os); osw.write(urlParameters); osw.flush(); osw.close(); InputStream is = connection.getInputStream(); BufferedReader in = new BufferedReader(new InputStreamReader(is)); line = in.readLine(); is.close(); in.close(); } catch (Exception e) { log.debug("::Exception: {}",e.getMessage()); } return line; }
替代简化方案
如果第三方服务使用的是受全球信任CA签发的证书,无需自定义TrustManager和HostnameVerifier,直接删除原代码中自定义信任管理器、SSL上下文初始化、主机名验证器设置的逻辑,使用JDK默认的HTTPS信任机制即可。
内容的提问来源于stack exchange,提问作者Vedant Dixit
相关产品推荐
相关产品推荐

