簡介
我們知道雖然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/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!