服务器 HTTPS 支持

Server HTTPS Support

Akka HTTP supports TLS encryption on the server-side as well as on the client-side.

Akka HTTP 支持在服务器端以及 客户端 上的 TLS 加密。

The central vehicle for configuring encryption is the HttpsConnectionContextHttpsConnectionContext, which can be created using the static method ConnectionContext.https which is defined like this:

配置加密的主要工具是 HttpsConnectionContextHttpsConnectionContext ,它通过使用静态方法 ConnectionContext.https 创建,定义看起来像这样:

Scala
def https(
  sslContext:          SSLContext,
  sslConfig:           Option[AkkaSSLConfig]         = None,
  enabledCipherSuites: Option[immutable.Seq[String]] = None,
  enabledProtocols:    Option[immutable.Seq[String]] = None,
  clientAuth:          Option[TLSClientAuth]         = None,
  sslParameters:       Option[SSLParameters]         = None) =
  new HttpsConnectionContext(sslContext, sslConfig, enabledCipherSuites, enabledProtocols, clientAuth, sslParameters)
Java
// ConnectionContext
/** Used to serve HTTPS traffic. */
def https(sslContext: SSLContext): HttpsConnectionContext = // ...

/** Used to serve HTTPS traffic. */
def https(
  sslContext:          SSLContext,
  sslConfig:           Optional[AkkaSSLConfig],
  enabledCipherSuites: Optional[JCollection[String]],
  enabledProtocols:    Optional[JCollection[String]],
  clientAuth:          Optional[TLSClientAuth],
  sslParameters:       Optional[SSLParameters]) = // ...

On the server-side the bind, and bindAndHandleXXX methods of the HttpHttp extension define an optional httpsContext parameter, which can receive the HTTPS configuration in the form of an HttpsContext instance. If defined encryption is enabled on all accepted connections. Otherwise it is disabled (which is the default).

HttpHttp 扩展上的 bindbindAndHandleXXX 方法定义了一个可选 httpsContext 参数,它可以以一个 HttpsContext 实例的形式接收 HTTPS 配置。

For detailed documentation for client-side HTTPS support refer to Client-Side HTTPS Support.

对于客户端 HTTPS 支持的详细文件参考 客户端 HTTPS 支持

Obtaining SSL/TLS Certificates

获得 SSL/TLS 证书

In order to run an HTTPS server a certificate has to be provided, which usually is either obtained from a signing authority or created by yourself for local or staging environment purposes.

为了运行 HTTPS 服务器,必须提供证书。证书通常从签名机构获得,或者用于本地或临时环境目的自己创建。

Signing authorities often provide instructions on how to create a Java keystore (typically with reference to Tomcat configuration). If you want to generate your own certificates, the official Oracle documentation on how to generate keystores using the JDK keytool utility can be found here.

签名机构通常提供怎样创建一个 Java 密钥库的说明(通常参考 Tomcat 配置)。 如果你想生成自己的证书,怎样使用 JDK keytool 工具生成密钥库可以在 这里 找到 Oracle 官方文档。

SSL-Config provides a more targeted guide on generating certificates, so we recommend you start with the guide titled Generating X.509 Certificates.

因为 SSL-Config 提供生成证书的更多目录指南,所以我们推荐从指南 生成 X.509 证书 开始。

Using HTTPS

使用 HTTPS

Once you have obtained the server certificate, using it is as simple as preparing an HttpsConnectionContextHttpsConnectionContext and either setting it as the default one to be used by all servers started by the given HttpHttp extension or passing it in explicitly when binding the server.

一旦你获得了服务器证书,使用它就像 HttpsConnectionContextHttpsConnectionContext 一样简单。设置它作为默认 HTTPS,被用于通过 HttpHttp 扩展启动的所有服务器,或者在绑定服务器时显示传递。

The below example shows how setting up HTTPS works. First, you create and configure an instance of HttpsConnectionContextHttpsConnectionContext :

下面示例演示怎样设置 HTTPS 。首先,创建并配置一个 HttpsConnectionContextHttpsConnectionContext 的实例:

Scala
import java.io.InputStream
import java.security.{ SecureRandom, KeyStore }
import javax.net.ssl.{ SSLContext, TrustManagerFactory, KeyManagerFactory }

import akka.actor.ActorSystem
import akka.http.scaladsl.server.{ Route, Directives }
import akka.http.scaladsl.{ ConnectionContext, HttpsConnectionContext, Http }
import akka.stream.ActorMaterializer
import com.typesafe.sslconfig.akka.AkkaSSLConfig
implicit val system = ActorSystem()
implicit val mat = ActorMaterializer()
implicit val dispatcher = system.dispatcher

// Manual HTTPS configuration

val password: Array[Char] = "change me".toCharArray // do not store passwords in code, read them from somewhere safe!

val ks: KeyStore = KeyStore.getInstance("PKCS12")
val keystore: InputStream = getClass.getClassLoader.getResourceAsStream("server.p12")

require(keystore != null, "Keystore required!")
ks.load(keystore, password)

val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("SunX509")
keyManagerFactory.init(ks, password)

val tmf: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509")
tmf.init(ks)

val sslContext: SSLContext = SSLContext.getInstance("TLS")
sslContext.init(keyManagerFactory.getKeyManagers, tmf.getTrustManagers, new SecureRandom)
val https: HttpsConnectionContext = ConnectionContext.https(sslContext)
Java
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.security.cert.CertificateException;

import akka.http.javadsl.HttpsConnectionContext;

// ** CONFIGURING ADDITIONAL SETTINGS ** //

public static HttpsConnectionContext useHttps(ActorSystem system) {
    HttpsConnectionContext https = null;
    try {
      // initialise the keystore
      // !!! never put passwords into code !!!
      final char[] password = new char[]{'a', 'b', 'c', 'd', 'e', 'f'};

      final KeyStore ks = KeyStore.getInstance("PKCS12");
      final InputStream keystore = SimpleServerApp.class.getClassLoader().getResourceAsStream("httpsDemoKeys/keys/server.p12");
      if (keystore == null) {
        throw new RuntimeException("Keystore required!");
      }
      ks.load(keystore, password);

      final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
      keyManagerFactory.init(ks, password);

      final TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
      tmf.init(ks);

      final SSLContext sslContext = SSLContext.getInstance("TLS");
      sslContext.init(keyManagerFactory.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());

      https = ConnectionContext.https(sslContext);

    } catch (NoSuchAlgorithmException | KeyManagementException e) {
      system.log().error("Exception while configuring HTTPS.", e);
    } catch (CertificateException | KeyStoreException | UnrecoverableKeyException | IOException e) {
      system.log().error("Exception while ", e);
    }

    return https;
}

Once you configured the HTTPS context, you can set it as default: Then pass it to the akka.http.javadsl.Http class’s setDefaultServerHttpContext method, like in the below main method.

一旦你配置了 HTTPS 上下文,你可以设置它为默认的: 把它传到 akka.http.javadsl.Http 类的 setDefaultServerHttpContext 方法,像下面 main 方法里面做的一样。

Scala
// sets default context to HTTPS – all Http() bound servers for this ActorSystem will use HTTPS from now on
Http().setDefaultServerHttpContext(https)
Http().bindAndHandle(routes, "127.0.0.1", 9090, connectionContext = https)
Java
public Route multiply(int x, int y) {
  int result = x * y;
  return complete(String.format("%d * %d = %d", x, y, result));
}

public CompletionStage<Route> multiplyAsync(Executor ctx, int x, int y) {
  return CompletableFuture.supplyAsync(() -> multiply(x, y), ctx);
}

public Route createRoute() {
  Route addHandler = parameter(StringUnmarshallers.INTEGER, "x", x ->
    parameter(StringUnmarshallers.INTEGER, "y", y -> {
      int result = x + y;
      return complete(String.format("%d + %d = %d", x, y, result));
    })
  );

  BiFunction<Integer, Integer, Route> subtractHandler = (x, y) -> {
    int result = x - y;
    return complete(String.format("%d - %d = %d", x, y, result));
  };

  return
    concat(
      // matches the empty path
      pathSingleSlash(() ->
        getFromResource("web/calculator.html")
      ),
      // matches paths like this: /add?x=42&y=23
      path("add", () -> addHandler),
      path("subtract", () ->
        parameter(StringUnmarshallers.INTEGER, "x", x ->
          parameter(StringUnmarshallers.INTEGER, "y", y ->
            subtractHandler.apply(x, y)
          )
        )
      ),
      // matches paths like this: /multiply/{x}/{y}
      path(PathMatchers.segment("multiply").slash(integerSegment()).slash(integerSegment()),
        this::multiply
      ),
      path(PathMatchers.segment("multiplyAsync").slash(integerSegment()).slash(integerSegment()), (x, y) ->
        extractExecutionContext(ctx ->
          onSuccess(multiplyAsync(ctx, x, y), Function.identity())
        )
      ),
      post(() ->
        path("hello", () ->
          entity(entityToString(), body ->
            complete("Hello " + body + "!")
          )
        )
      )
    );
}

// ** STARTING THE SERVER ** //

public static void main(String[] args) throws IOException {
  final ActorSystem system = ActorSystem.create("SimpleServerApp");
  final ActorMaterializer materializer = ActorMaterializer.create(system);
  final Http http = Http.get(system);

  boolean useHttps = false; // pick value from anywhere
  if ( useHttps ) {
    HttpsConnectionContext https = useHttps(system);
    http.setDefaultServerHttpContext(https);
  }

  final SimpleServerApp app = new SimpleServerApp();
  final Flow<HttpRequest, HttpResponse, NotUsed> flow = app.createRoute().flow(system, materializer);

  Http.get(system).bindAndHandle(flow, ConnectHttp.toHost("localhost", 8080), materializer);

  System.out.println("Type RETURN to exit");
  System.in.read();
  system.terminate();
}

It is also possible to pass in the context to specific bind... (or client) calls, like displayed below:

也可以在上下文中传递到特定的 bind... (或客户端)调用,像下面显示的一样:

Http().bind("127.0.0.1", connectionContext = https)

// or using the high level routing DSL:
val routes: Route = get { complete("Hello world!") }
Http().bindAndHandle(routes, "127.0.0.1", 8080, connectionContext = https)

Running both HTTP and HTTPS

同时运行 HTTP 和 HTTPS

If you want to run HTTP and HTTPS servers in a single application, you can call bind... methods twice, one for HTTPS, and the other for HTTP.

如果你想在单个程序里同时运行 HTTP 和 HTTPS 服务器,你可以调用 bind... 方法两次,一个用于 HTTPS,另一个用于 HTTP。

When configuring HTTPS, you can do it up like explained in the above Using HTTPS section,

配置 HTTPS 时,你可以像上面 使用 HTTPS 部分描述的那样进行配置,

Scala
implicit val system = ActorSystem()
implicit val mat = ActorMaterializer()
implicit val dispatcher = system.dispatcher

// Manual HTTPS configuration

val password: Array[Char] = "change me".toCharArray // do not store passwords in code, read them from somewhere safe!

val ks: KeyStore = KeyStore.getInstance("PKCS12")
val keystore: InputStream = getClass.getClassLoader.getResourceAsStream("server.p12")

require(keystore != null, "Keystore required!")
ks.load(keystore, password)

val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("SunX509")
keyManagerFactory.init(ks, password)

val tmf: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509")
tmf.init(ks)

val sslContext: SSLContext = SSLContext.getInstance("TLS")
sslContext.init(keyManagerFactory.getKeyManagers, tmf.getTrustManagers, new SecureRandom)
val https: HttpsConnectionContext = ConnectionContext.https(sslContext)
Java
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.security.cert.CertificateException;

import akka.http.javadsl.HttpsConnectionContext;

// ** CONFIGURING ADDITIONAL SETTINGS ** //

public static HttpsConnectionContext useHttps(ActorSystem system) {
    HttpsConnectionContext https = null;
    try {
      // initialise the keystore
      // !!! never put passwords into code !!!
      final char[] password = new char[]{'a', 'b', 'c', 'd', 'e', 'f'};

      final KeyStore ks = KeyStore.getInstance("PKCS12");
      final InputStream keystore = SimpleServerApp.class.getClassLoader().getResourceAsStream("httpsDemoKeys/keys/server.p12");
      if (keystore == null) {
        throw new RuntimeException("Keystore required!");
      }
      ks.load(keystore, password);

      final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
      keyManagerFactory.init(ks, password);

      final TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
      tmf.init(ks);

      final SSLContext sslContext = SSLContext.getInstance("TLS");
      sslContext.init(keyManagerFactory.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());

      https = ConnectionContext.https(sslContext);

    } catch (NoSuchAlgorithmException | KeyManagementException e) {
      system.log().error("Exception while configuring HTTPS.", e);
    } catch (CertificateException | KeyStoreException | UnrecoverableKeyException | IOException e) {
      system.log().error("Exception while ", e);
    }

    return https;
}

Then, call bind... methods twice like below. The passed https context is from the above code snippet. SimpleServerApp.useHttps(system) is calling the above defined public static HttpsConnectionContext useHttps(ActorSystem system) method.

然后,启用 bind... 方法两次,如下所示。 传递的 https 来自上面的代码片段。 SimpleServerApp.useHttps(system) 调用上面定义的 public static HttpsConnectionContext useHttps(ActorSystem system) 方法。

Scala
// you can run both HTTP and HTTPS in the same application as follows:
val commonRoutes: Route = get { complete("Hello world!") }
Http().bindAndHandle(commonRoutes, "127.0.0.1", 443, connectionContext = https)
Http().bindAndHandle(commonRoutes, "127.0.0.1", 80)
Java
final Http http = Http.get(system);
//Run HTTP server firstly
http.bindAndHandle(flow, ConnectHttp.toHost("localhost", 80), materializer);

//get configured HTTPS context
HttpsConnectionContext https = SimpleServerApp.useHttps(system);

// sets default context to HTTPS – all Http() bound servers for this ActorSystem will use HTTPS from now on
http.setDefaultServerHttpContext(https);

//Then run HTTPS server
http.bindAndHandle(flow, ConnectHttp.toHost("localhost", 443), materializer);

Mutual authentication

相互认证

To require clients to authenticate themselves when connecting, pass in Some(TLSClientAuth.Need)Optional.of(TLSClientAuth.need) as the clientAuth parameter of the HttpsConnectionContextHttpsConnectionContext and make sure the truststore is populated accordingly. For further (custom) certificate checks you can use the `Tls-Session-Info``TlsSessionInfo` synthetic header.

在进行连接时要求客户端认证身份,传递 Some(TLSClientAuth.Need)Optional.of(TLSClientAuth.need) 作为 HttpsConnectionContextHttpsConnectionContextclientAuth 参数,并确保信任库被相应地填充。 为了进一步(自定义)证书检查,你可以使用 `Tls-Session-Info``TlsSessionInfo` 合成头。

At this point dynamic renegotiation of the certificates to be used is not implemented. For details see issue #18351 and some preliminary work in PR #19787.

此时,证书的动态重新协商还没有实现。有关详细信息见 问题 #18351PR #19787 里的一先初步工作。

Further reading

进一步阅读

The topic of properly configuring HTTPS for your web server is an always changing one, thus we recommend staying up to date with various security breach news and of course keep your JVM at the latest version possible, as the default settings are often updated by Oracle in reaction to various security updates and known issues.

为你的 WEB 服务器正确配置 HTTPS 是一个不断变化的主题,因此我们建议随时了解各种安全漏洞的最新消息,当然还应尽可能保持你的 JVM 为最新版本, 作为默认设置,因为默认设置经常被 Oracle 更新,以对大量安全更新和已经问题作出反应。

We also recommend having a look at the Play documentation about securing your app, as well as the techniques described in the Play documentation about setting up a reverse proxy to terminate TLS in front of your application instead of terminating TLS inside the JVM, and therefore Akka HTTP, itself.

我们还建议看一看 关于保护你的应用程序的 Play 文档, 以及在 Play 文档里关于设置 在你的应用程序前面反向代理终止 TLS 描述的技术来替代在 JVM 内部终止 TLS。

译注:意思就是使用专门的 HTTP 服务器(如 Nginx、Apache 等)来管理 TLS(HTTPS)。

Other excellent articles on the subject:

其它优秀文章:

在此文档中发现错误?该页面的源代码可以在 这里 找到。欢迎随时编辑并提交 Pull Request。