netty系列之:讓TLS支援http2

flydean發表於2021-10-20

簡介

我們知道雖然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/

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

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

相關文章