netty系列之:让TLS支持http2

简介

我们知道虽然HTTP2协议并不强制使用HTTPS,但是对大多数浏览器来说,如果要使用HTTP2的话,则必须使用HTTPS,所以我们需要了解如何在netty的TLS中支持http2。

TLS的扩展协议NPN和ALPN

HTTP2协议是从spdy协议发展而来的,无论是spdy还是http2都为了能在HTTPS的环境下工作,发展出来了TLS协议的扩展。

他们分别叫做NPN(Next Protocol Negotiation) 和 ALPN (Application Layer Protocol Negotiation) 。

他们规定了在TLS协议握手之后,客户端和服务器端进行应用数据通信的协议。其中ALPN可以在客户端首次和服务器端进行握手的时候,就列出客户端支持的应用层数据协议,服务器端直接选择即可,因此可以比NPN少一个交互流程,更加优秀。

那么spdy和http2分别支持的协议都有哪些呢?

netty提供了一个ApplicationProtocolNames类,在其中定义了各自对应的协议,其中ALPN对应了http2和http1.1,而sydy对应了spdy/1,spdy/2,spdy/3:

代码语言:javascript
复制
    /**
     * HTTP version 2
     */
    public static final String HTTP_2 = "h2";
/**
 * {@code "http/1.1"}: HTTP version 1.1
 */
public static final String HTTP_1_1 = "http/1.1";

/**
 * {@code "spdy/3.1"}: SPDY version 3.1
 */
public static final String SPDY_3_1 = "spdy/3.1";

/**
 * {@code "spdy/3"}: SPDY version 3
 */
public static final String SPDY_3 = "spdy/3";

/**
 * {@code "spdy/2"}: SPDY version 2
 */
public static final String SPDY_2 = "spdy/2";

/**
 * {@code "spdy/1"}: SPDY version 1
 */
public static final String SPDY_1 = &#34;spdy/1&#34;;</code></pre></div></div><h2 id="5d37n" name="SslProvider">SslProvider</h2><p>目前来说,netty中有两种SSL的实现方式,一种是JDK,一种是OPENSSL,不同的实现方式对TLS协议扩展的支持也不一样。它提供了一个isAlpnSupported方法,根据传入provider的不同来判断,是否支持ALPN。</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">    public static boolean isAlpnSupported(final SslProvider provider) {
    switch (provider) {
        case JDK:
            return JdkAlpnApplicationProtocolNegotiator.isAlpnSupported();
        case OPENSSL:
        case OPENSSL_REFCNT:
            return OpenSsl.isAlpnSupported();
        default:
            throw new Error(&#34;Unknown SslProvider: &#34; + provider);
    }
}</code></pre></div></div><p>如果你使用的是JDK8,那么运行之后,可能会得到下面的错误提示:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">ALPN is only supported in Java9 or if you use conscrypt as your provider or have the jetty alpn stuff on the class path.</code></pre></div></div><p>也就是说如果是用JDK作为默认的SSL provider的话,它是不支持ALPN的。必须升级到java9.</p><p>根据提示如果添加conscrypt到classpath中:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">        &lt;dependency&gt;
        &lt;groupId&gt;org.conscrypt&lt;/groupId&gt;
        &lt;artifactId&gt;conscrypt-openjdk-uber&lt;/artifactId&gt;
        &lt;version&gt;2.5.2&lt;/version&gt;
    &lt;/dependency&gt;</code></pre></div></div><p>运行之后会得到下面的错误:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">Unable to wrap SSLEngine of type &#39;sun.security.ssl.SSLEngineImpl&#39;</code></pre></div></div><p>怎么办呢?答案就是使用Open SSL,还需要添加:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">        &lt;dependency&gt;
        &lt;groupId&gt;io.netty&lt;/groupId&gt;
        &lt;artifactId&gt;netty-tcnative-boringssl-static&lt;/artifactId&gt;
        &lt;version&gt;2.0.40.Final&lt;/version&gt;
    &lt;/dependency&gt;</code></pre></div></div><p>经过测试,完美执行。</p><h2 id="cj7gl" name="ApplicationProtocolConfig">ApplicationProtocolConfig</h2><p>ApplicationProtocolConfig是netty提供了传递给SSLEngine的协议配置类,它主要有四个属性:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">    private final List&lt;String&gt; supportedProtocols;
private final Protocol protocol;
private final SelectorFailureBehavior selectorBehavior;
private final SelectedListenerFailureBehavior selectedBehavior;</code></pre></div></div><p>supportedProtocols是支持的数据传输协议,像上面的HTTP2,HTTP1.1或者spdy/1,spdy/2,spdy/3等。</p><p>protocol是TLS的扩展协议,像ALPN或者NPN等。</p><p>selectorBehavior是在选择协议的时候的表现方式,有3种方式:</p><p>FATAL_ALERT: 如果选择应用程序协议的节点没有找到匹配项,那么握手将会失败。

NO_ADVERTISE: 如果选择应用程序协议的节点没有找到匹配项,它将通过在握手中假装不支持 TLS 扩展。
CHOOSE_MY_LAST_PROTOCOL: 如果选择应用程序协议的节点没有找到匹配项,将会使用上一次建议使用的协议。

selectedBehavior是通知被选择的协议之后的表现方式,也有3种方式:

ACCEPT: 如果节点不支持对方节点选择的应用程序协议,则该节点默认不支持该TLS扩展,然后继续握手。
FATAL_ALERT: 如果节点不支持对方节点选择的应用程序协议,则握手失败。
CHOOSE_MY_LAST_PROTOCOL: 如果节点不支持对方节点选择的应用程序协议,将会使用上一次建议使用的协议。

构建SslContext

有了provider,ApplicationProtocolConfig 之后,就可以构建SslContext了。首先创建SSL provider:

代码语言:javascript
复制
 SslProvider provider =  SslProvider.isAlpnSupported(SslProvider.OPENSSL)  ? SslProvider.OPENSSL : SslProvider.JDK;

默认情况下使用JDK作为ssl provider,如果你使用的是OpenSSL的话,就使用OpenSSL。

我们使用SslContextBuilder.forServer来创建SslContext,这个方法需要传入certificate和privateKey,为了简单起见,我们使用自签名的SelfSignedCertificate:

代码语言:javascript
复制
 SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();

还可以为其设置sslProvider,ciphers和applicationProtocolConfig等信息:

代码语言:javascript
复制
sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
.sslProvider(provider)
//支持的cipher
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
.applicationProtocolConfig(new ApplicationProtocolConfig(
Protocol.ALPN,
// 目前 OpenSsl 和 JDK providers只支持NO_ADVERTISE
SelectorFailureBehavior.NO_ADVERTISE,
// 目前 OpenSsl 和 JDK providers只支持ACCEPT
SelectedListenerFailureBehavior.ACCEPT,
ApplicationProtocolNames.HTTP_2,
ApplicationProtocolNames.HTTP_1_1))
.build();

ProtocolNegotiationHandler

最后,我们需要根据协商使用的不同协议,进行不同的处理。netty提供了一个ApplicationProtocolNegotiationHandler,自定义的话,只需要继承该类即可,比如,我们根据protocol的名称不同,来分别处理HTTP1和HTTP2请求:

代码语言:javascript
复制
   public class MyNegotiationHandler extends ApplicationProtocolNegotiationHandler {
public MyNegotiationHandler() {
super(ApplicationProtocolNames.HTTP_1_1);
}

   protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
       if (ApplicationProtocolNames.HTTP_2.equals(protocol) {
           configureHttp2(ctx);
       } else if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) {
           configureHttp1(ctx);
       } else {
           throw new IllegalStateException(&#34;unknown protocol: &#34; + protocol);
       }
   }

}

然后将其加入到ChannelPipeline中即可:

代码语言:javascript
复制
   public class MyInitializer extends ChannelInitializer<Channel> {
private final SslContext sslCtx;

   public MyInitializer(SslContext sslCtx) {
       this.sslCtx = sslCtx;
   }

   protected void initChannel(Channel ch) {
       ChannelPipeline p = ch.pipeline();
       p.addLast(sslCtx.newHandler(...)); // Adds SslHandler
       p.addLast(new MyNegotiationHandler());
   }

}

总结

以上就是在netty中配置TLS支持HTTP2的完整流程了。

本文的例子可以参考:learn-netty4

本文已收录于 http://www.flydean.com/26-netty-secure-http2/
最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!