(注意:另请参阅此答案末尾的最新更新)
说明
原因是默认 SecureRandom 提供者。
在 Windows 上,有 2 个SecureRandom 提供程序可用:
- provider=SUN, type=SecureRandom, algorithm=SHA1PRNG
- provider=SunMSCAPI, type=SecureRandom, algorithm=Windows-PRNG
在 Linux 上(使用 Oracle JDK 8u162 在 Alpine docker 中测试):
- provider=SUN, type=SecureRandom, algorithm=NativePRNG
- provider=SUN, type=SecureRandom, algorithm=SHA1PRNG
- provider=SUN, type=SecureRandom, algorithm=NativePRNGBlocking
- provider=SUN, type=SecureRandom, algorithm=NativePRNGNonBlocking
这些在jre/lib/security/java.security 文件中指定。
security.provider.1=sun.security.provider.Sun
...
security.provider.10=sun.security.mscapi.SunMSCAPI
默认情况下,使用第一个 SecureRandom 提供程序。在 Windows 上,默认是 sun.security.provider.Sun,当 JVM 使用 -Djava.security.debug="provider,engine=SecureRandom" 运行时,此实现报告如下:
Provider: SecureRandom.SHA1PRNG algorithm from: SUN
provider: Failed to use operating system seed generator: java.io.IOException: Required native CryptoAPI features not available on this machine
provider: Using default threaded seed generator
而且默认的线程种子生成器非常慢。
你需要使用SunMSCAPI提供者。
解决方案 1:配置
在配置中重新排序提供程序:
编辑jre/lib/security/java.security:
security.provider.1=sun.security.mscapi.SunMSCAPI
...
security.provider.10=sun.security.provider.Sun
我不知道这可以通过系统属性来完成。
或者可能是的,使用-Djava.security.properties(未经测试,see this)
解决方案 2:程序化
以编程方式重新排序提供程序:
Optional.ofNullable(Security.getProvider("SunMSCAPI")).ifPresent(p->{
Security.removeProvider(p.getName());
Security.insertProviderAt(p, 1);
});
JVM 现在报告以下 (-Djava.security.debug="provider,engine=SecureRandom"):
Provider: SecureRandom.Windows-PRNG algorithm from: SunMSCAPI
解决方案 3:程序化 v2
受this idea 的启发,下面的代码只插入一个SecureRandom 服务,从现有的SunMSCAPI 提供程序动态配置,而不显式依赖sun.* 类。这也避免了与SunMSCAPI 提供者的所有服务不分青红皂白地优先排序相关的潜在风险。
public interface WindowsPRNG {
static void init() {
String provider = "SunMSCAPI"; // original provider
String type = "SecureRandom"; // service type
String alg = "Windows-PRNG"; // algorithm
String name = String.format("%s.%s", provider, type); // our provider name
if (Security.getProvider(name) != null) return; // already registered
Optional.ofNullable(Security.getProvider(provider)) // only on Windows
.ifPresent(p-> Optional.ofNullable(p.getService(type, alg)) // should exist but who knows?
.ifPresent(svc-> Security.insertProviderAt( // insert our provider with single SecureRandom service
new Provider(name, p.getVersion(), null) {{
setProperty(String.format("%s.%s", type, alg), svc.getClassName());
}}, 1)));
}
}
性能
<140 msec(而不是5000+ msec)
详情
当您使用 URL.openConnection("https://...") 时,调用堆栈下方的某处有对 new SecureRandom() 的调用
它调用getPrngAlgorithm()(见SecureRandom:880)
这会返回它找到的第一个 SecureRandom 提供者。
出于测试目的,对URL.openConnection() 的调用可以替换为:
new SecureRandom().generateSeed(20);
免责声明
我不知道供应商重新订购会造成任何负面影响。但是,可能会有一些,特别是考虑到默认的提供者选择算法。
无论如何,至少在理论上,从功能的角度来看,这对应用程序应该是透明的。
2019-01-08 更新
Windows 10(版本 1803):无法再在任何最新 JDK 上重现此问题(从旧的 oracle 1.7.0_72 到 openjdk "12-ea" 2019-03-19 均已测试)。
看起来是 Windows 问题,已在最新的操作系统更新中修复。最近的 JRE 版本中也可能发生或未发生相关更新。
但是,即使我最旧的 JDK 7 update 72 安装肯定会受到影响,并且肯定不会以任何方式修补,我也无法重现原始问题。
使用此解决方案时,性能仍有小幅提升(平均 cca 350 毫秒),但默认行为不再遭受无法忍受的 5+ 秒惩罚。