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:


    /**
     * 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 = "spdy/1";

SslProvider

目前來說,netty中有兩種SSL的實現方式,一種是JDK,一種是OPENSSL,不同的實現方式對TLS協議擴展的支持也不一樣。它提供了一個isAlpnSupported方法,根據傳入provider的不同來判斷,是否支持ALPN。

    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("Unknown SslProvider: " + provider);
        }
    }

如果你使用的是JDK8,那么運行之后,可能會得到下面的錯誤提示:

ALPN is only supported in Java9 or if you use conscrypt as your provider or have the jetty alpn stuff on the class path.

也就是說如果是用JDK作為默認的SSL provider的話,它是不支持ALPN的。必須升級到java9.

根據提示如果添加conscrypt到classpath中:

        <dependency>
            <groupId>org.conscrypt</groupId>
            <artifactId>conscrypt-openjdk-uber</artifactId>
            <version>2.5.2</version>
        </dependency>

運行之后會得到下面的錯誤:

Unable to wrap SSLEngine of type 'sun.security.ssl.SSLEngineImpl'

怎么辦呢?答案就是使用Open SSL,還需要添加:

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-tcnative-boringssl-static</artifactId>
            <version>2.0.40.Final</version>
        </dependency>

經過測試,完美執行。

ApplicationProtocolConfig

ApplicationProtocolConfig是netty提供了傳遞給SSLEngine的協議配置類,它主要有四個屬性:

    private final List<String> supportedProtocols;
    private final Protocol protocol;
    private final SelectorFailureBehavior selectorBehavior;
    private final SelectedListenerFailureBehavior selectedBehavior;

supportedProtocols是支持的數據傳輸協議,像上面的HTTP2,HTTP1.1或者spdy/1,spdy/2,spdy/3等。

protocol是TLS的擴展協議,像ALPN或者NPN等。

selectorBehavior是在選擇協議的時候的表現方式,有3種方式:

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:

 SslProvider provider =  SslProvider.isAlpnSupported(SslProvider.OPENSSL)  ? SslProvider.OPENSSL : SslProvider.JDK;
           

默認情況下使用JDK作為ssl provider,如果你使用的是OpenSSL的話,就使用OpenSSL。

我們使用SslContextBuilder.forServer來創建SslContext,這個方法需要傳入certificate和privateKey,為了簡單起見,我們使用自簽名的SelfSignedCertificate:

 SelfSignedCertificate ssc = new SelfSignedCertificate();
            sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();

還可以為其設置sslProvider,ciphers和applicationProtocolConfig等信息:

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請求:

   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("unknown protocol: " + protocol);
           }
       }
   }

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

   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/

最通俗的解讀,最深刻的干貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程序那些事」,懂技術,更懂你!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM