慢速SecureRandom初始化

时间:2018-03-16 14:19:28

标签: java windows performance random

假设你做的很简单:

public class Main {
    public static void main(String[] args) {
        long started = System.currentTimeMillis();
        try {
            new URL(args[0]).openConnection();
        } catch (Exception ignore) {
        }
        System.out.println(System.currentTimeMillis() - started);
    }
}

现在使用http://localhost作为args[0]

运行它

完成~100 msec

现在尝试https://localhost

需要5000+ msec

现在在linux或docker中运行相同的东西:

  • http:~100 msec
  • https:~350 msec

这是为什么? 为什么平台之间有这么大的差异? 你能做些什么呢?

对于长时间运行的应用程序服务器和应用程序,它们有很长很重的初始化序列,这5秒可能无关紧要。

然而,有很多应用程序,这个最初的5秒"挂起"事情并且可能变得令人沮丧...

1 个答案:

答案 0 :(得分:15)

(注意:请参阅本答复末尾的最新更新)

<强>解释

原因是默认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

<强>详情

当您使用new SecureRandom()

时,调用堆栈中的某个地方会调用URL.openConnection("https://...")

它会调用getPrngAlgorithm()(请参阅SecureRandom:880

这将返回它找到的第一个SecureRandom提供程序。

出于测试目的,可以用以下内容替换对URL.openConnection()的呼叫:

new SecureRandom().generateSeed(20);

<强>声明

我不知道提供商重新排序造成的任何负面影响。但是,可能有一些,特别是考虑默认提供商选择算法。

无论如何,至少在理论上,从功能的角度来看,这应该对应用是透明的。

更新2019-01-08

Windows 10(版本1803):无法再在任何最新的JDK上重现此问题(从旧的oracle 1.7.0_72到openjdk&#34; 12-ea&#34; 2019-03-19测试)。< / p>

看起来它是Windows问题,已在最新的操作系统更新中修复。相关更新可能也可能没有在最近的JRE版本中发生。 但是,即使我最老的JDK 7更新72安装受到影响,我也无法重现原始问题,并且无法以任何方式修补。

使用此解决方案时仍然会有轻微的性能提升(cca平均为350毫秒),但默认行为不再受到无法忍受的5秒以上的惩罚。