带有 ssl-conf 的 Akka HTTPS (SSL) 服务器

Posted

技术标签:

【中文标题】带有 ssl-conf 的 Akka HTTPS (SSL) 服务器【英文标题】:Akka HTTPS (SSL) Server with ssl-conf 【发布时间】:2016-10-12 03:42:33 【问题描述】:

Akka 版本

Akka 2.4.7

Akka 功能

HTTPS Server Support

Typesafe's ssl-config

语言:Scala

我正在使用 Akka 2.4.7 的 Http Server 功能在不同的端口上提供多个 HTTPS 服务连接。 在这个阶段,要求参与者系统的这个组件在一个 JVM 中托管多个 HTTPS 服务 - 它是一个连接和集成其他服务的后端。

问题

我想使用Typesafe's ssl-config 库来配置每个HTTPS 服务器。我该怎么做(我的尝试没有成功)

我尝试过的:

对于每个服务,我在 application.conf 中定义了 ssl-config 配置块。下一个配置的示例是:

my-service 
  ssl-config = 
    debug 
      all = true
    
    sslParameters 
      clientAuth : "none"
    
    ssl = 
      keyManager = 
        stores = [
          path: tmp/certs/autumn/devhost.jks, password: "not-real-password", type: "JKS"
        ]
      
    
  

我使用 application.conf 中定义的 my-service 的 HOCON 路径获取配置的这一部分,并将其与参考默认配置合并以创建 SSLConfigSettings .

  def parseSslConfig(config: Config): SSLConfigSettings = 
    val cfg = config.withFallback(ConfigFactory.defaultReference().getConfig("ssl-config"))
    val parser = new SSLConfigParser(EnrichedConfig(cfg), getClass.getClassLoader)
    parser.parse()
  

现在有了 SSLConfigSettings,我现在可以创建一个 AkkaSSLConfig 对象,而在 Akka 2.4.7 中,可以使用该函数创建一个 HttpsConnectionContext原型:

//#https-context-creation // 连接上下文 定义 https( sslContext:SSLContext, sslConfig:选项 [AkkaSSLConfig] = 无, enabledCipherSuites: Option[immutable.Seq[String]] = None, enabledProtocols: Option[immutable.Seq[String]] = None, 客户端身份验证:选项 [TLSClientAuth] = 无, sslParameters:选项 [SSLParameters] = 无)= 新的 HttpsConnectionContext(sslContext, sslConfig, enabledCipherSuites, enabledProtocols, clientAuth, sslParameters) //#https-context-creation

所以我可以使用如下代码启动 HTTPS 服务器(注意:请求处理程序在其他地方定义,提供 Future[HttpResponse]

val akkaSSLConfig: AkkaSSLConfig = AkkaSSLConfig().withSettings(sslConfigSettings)
val serverConnectionContext = ConnectionContext.https(SSLContext.getDefault, Some(akkaSSLConfig))

  val httpServer = httpServerSystem.bind(interface = "127.0.0.1",
    port = 8991,
    connectionContext = serverConnectionContext)

  val bindingFuture: Future[Http.ServerBinding] = httpServer.to(Sink.foreach 
    connection =>
      system.log.info(s"Accepted HTTP connection " +
        s"[Local: address=$connection.localAddress.getAddress.getHostAddress, port=$connection.localAddress.getPort;" +
        s" Remote: address=$connection.remoteAddress.getAddress.getHostAddress port=$connection.remoteAddress.getPort]" + connection.remoteAddress)
      connection.handleWithAsyncHandler(httpRequest => requestHandler(httpRequest, connection.localAddress, connection.remoteAddress))
  ).run()

服务器启动无异常或错误,并绑定到定义端口8991上的127.0.0.1

2016-06-11 14:07:51,403 DEBUG [autumn-backend-akka.actor.default-dispatcher-7] TcpListener - Successfully bound to /127.0.0.1:8991
2016-06-11 14:07:51,404 DEBUG [autumn-backend-akka.actor.default-dispatcher-7] TcpListener - started (akka.io.TcpListener@3d1d819f)
2016-06-11 14:07:51,404 DEBUG [autumn-backend-akka.actor.default-dispatcher-7] TcpListener - now watched by Actor[akka://autumn-backend/system/IO-TCP/selectors/$a#-745039521]
2016-06-11 14:07:51,407 DEBUG [autumn-backend-akka.actor.default-dispatcher-5] TcpListener - now watched by Actor[akka://autumn-backend/user/StreamSupervisor-0/$$a#-672917867]

我使用浏览器或 curl 访问服务器,结果不好。它要求提供一个我知道是错误的客户端证书,因为我在 ssl-conf 中明确配置它们是不需要的,并且 JDK8 中的 ssl-conf 设置为默认不需要。

curl -v https://localhost:8991
* Rebuilt URL to: https://localhost:8991/
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8991 (#0)
* SSL peer handshake failed, the server most likely requires a client certificate to connect
* Closing connection 0
curl: (35) SSL peer handshake failed, the server most likely requires a client certificate to connect

使用带有 _s_client_ 选项的 openssl 进一步调查表明,没有发生 SSL 握手,也没有返回证书,尽管知道密钥库很好并且可以在其他地方使用。

 openssl s_client -showcerts -connect localhost:8991
CONNECTED(00000003)
140735299473488:error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure:s23_clnt.c:769:
---
no peer certificate available

No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 317 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
---

调试模式下的 Akka 日志未显示异常,并且已建立 TCP 连接,TLS Actor 启动然后立即停止。

2016-06-11 14:09:26,378 DEBUG [autumn-backend-akka.actor.default-dispatcher-6] TcpListener - New connection accepted
2016-06-11 14:09:26,378 DEBUG [autumn-backend-akka.actor.default-dispatcher-9] SelectionHandler - now supervising Actor[akka://autumn-backend/system/IO-TCP/selectors/$a/9#1252313265]
2016-06-11 14:09:26,378 DEBUG [autumn-backend-akka.actor.default-dispatcher-5] TcpIncomingConnection - started (akka.io.TcpIncomingConnection@6f12f120)
2016-06-11 14:09:26,378 DEBUG [autumn-backend-akka.actor.default-dispatcher-5] TcpIncomingConnection - now watched by Actor[akka://autumn-backend/system/IO-TCP/selectors/$a#-745039521]
2016-06-11 14:09:26,381 INFO  [autumn-backend-akka.actor.default-dispatcher-7] ActorSystemImpl - Accepted HTTP connection [Local: address=127.0.0.1, port=8991; Remote: address=127.0.0.1 port=58726]/127.0.0.1:58726
2016-06-11 14:09:26,384 DEBUG [autumn-backend-akka.actor.default-dispatcher-9] StreamSupervisor - now supervising Actor[akka://autumn-backend/user/StreamSupervisor-0/flow-9-0-unknown-operation#149184815]
2016-06-11 14:09:26,385 DEBUG [autumn-backend-akka.actor.default-dispatcher-7] TcpIncomingConnection - now watched by Actor[akka://autumn-backend/user/StreamSupervisor-0/$$j#-1999211380]
2016-06-11 14:09:26,385 DEBUG [autumn-backend-akka.actor.default-dispatcher-9] ActorGraphInterpreter - started (akka.stream.impl.fusing.ActorGraphInterpreter@57451dc8)
2016-06-11 14:09:26,385 DEBUG [autumn-backend-akka.actor.default-dispatcher-5] StreamSupervisor - now supervising Actor[akka://autumn-backend/user/StreamSupervisor-0/flow-9-1-unknown-operation#1511230856]
sslConfig.config.loose.disableSNI = false
2016-06-11 14:09:26,387 DEBUG [autumn-backend-akka.actor.default-dispatcher-5] TLSActor - started (akka.stream.impl.io.TLSActor@50f220e8)
2016-06-11 14:09:26,389 DEBUG [autumn-backend-akka.actor.default-dispatcher-5] TLSActor - stopped

在运行时调试显示正在获取密钥库:

akkaSSLConfig = com.typesafe.sslconfig.akka.AkkaSSLConfig@7851 
 system = akka.actor.ActorSystemImpl@7850 "akka://autumn-backend"
 config = com.typesafe.sslconfig.ssl.SSLConfigSettings@7849 "SSLConfig(None,SSLDebugConfig(false,false,false,None,false,false,false,false,None,false,false,false,false,false),false,Vector(RSA keySize < 2048, DSA keySize < 2048, EC keySize < 224),Vector(MD2, MD4, MD5),None,Some(Vector(TLSv1.2, TLSv1.1, TLSv1)),class com.typesafe.sslconfig.ssl.DefaultHostnameVerifier,KeyManagerConfig(SunX509,List(KeyStoreConfig(None,Some(config/certs/autumn/devhost.jks),Some(A8C7B78Ymb),JKS))),SSLLooseConfig(false,None,None,false,false,false,false),TLSv1.2,None,None,SSLParametersConfig(Default,Vector()),TrustManagerConfig(PKIX,List()))"
  default = false
  protocol = "TLSv1.2"
  checkRevocation = scala.None$@7905 "None"
  revocationLists = scala.None$@7905 "None"
  enabledCipherSuites = scala.None$@7905 "None"
  enabledProtocols = scala.Some@7906 "Some(Vector(TLSv1.2, TLSv1.1, TLSv1))"
  disabledSignatureAlgorithms = scala.collection.immutable.Vector@7907 "Vector" size = 3
  disabledKeyAlgorithms = scala.collection.immutable.Vector@7911 "Vector" size = 3
  sslParametersConfig = com.typesafe.sslconfig.ssl.SSLParametersConfig@7917 "SSLParametersConfig(Default,Vector())"
  keyManagerConfig = com.typesafe.sslconfig.ssl.KeyManagerConfig@7918 "KeyManagerConfig(SunX509,List(KeyStoreConfig(None,Some(config/certs/autumn/devhost.jks),Some(A8C7B78Ymb),JKS)))"
   algorithm = "SunX509"
   keyStoreConfigs = scala.collection.immutable.$colon$colon@7942 "::" size = 1
    0 = com.typesafe.sslconfig.ssl.KeyStoreConfig@9390 "KeyStoreConfig(None,Some(config/certs/autumn/devhost.jks),Some(not-real-password),JKS)"

如果我手动创建一个 HttpsConnectionContext 并且不使用 ssl-conf 或 AkkaSSLConfig,那么可行的方法是——但这不是目标。如何使用 AkkaSSLconf 对象和 Typesafe ssl-config 库配置和创建 HTTPS 配置连接?

更新 1:

如果我特别要求 TLS 上下文的 TLS 实例,如下所示:

val sslCtx = SSLContext.getInstance("TLS")

我收到一个异常,即 sslContext 未初始化。但是对于init SSLContext,我需要创建密钥库、信任库,这一切都很好,但感觉就像我忽略了已经定义了所有这些东西的 ssl-conf 库的所有优点。

更新 2:

我发现您可以使用以下方法创建 HTTPS 连接上下文:

Http().createServerHttpsContext(akkaSSLConfig)

您可以使用 AkkaSSLConfig 创建 HTTPS 服务器上下文,这是我所追求的好东西。问题是在测试 HTTPS 服务器不起作用,它只是挂起 1 分钟,但异常:

2016-06-12 11:14:53,222 DEBUG [autumn-backend-akka.actor.default-dispatcher-12] RepointableActorRef - Aborting tcp connection because of upstream failure: No elements passed in the last 1 minute.
akka.stream.impl.Timers$IdleTimeoutBidi$$anon$7.onTimer(Timers.scala:160)
akka.stream.stage.TimerGraphStageLogic.akka$stream$stage$TimerGraphStageLogic$$onInternalTimer(GraphStage.scala:1125)
akka.stream.stage.TimerGraphStageLogic$$anonfun$akka$stream$stage$TimerGraphStageLogic$$getTimerAsyncCallback$1.apply(GraphStage.scala:1114)
akka.stream.stage.TimerGraphStageLogic$$anonfun$akka$stream$stage$TimerGraphStageLogic$$getTimerAsyncCallback$1.apply(GraphStage.scala:1114)
akka.stream.impl.fusing.GraphInterpreter.runAsyncInput(GraphInterpreter.scala:572)
akka.stream.impl.fusing.GraphInterpreterShell.receive(ActorGraphInterpreter.scala:420)
akka.stream.impl.fusing.ActorGraphInterpreter.akka$stream$impl$fusing$ActorGraphInterpreter$$processEvent(ActorGraphInterpreter.scala:604)
akka.stream.impl.fusing.ActorGraphInterpreter$$anonfun$receive$1.applyOrElse(ActorGraphInterpreter.scala:619)
akka.actor.Actor$class.aroundReceive(Actor.scala:484)

我在 GitHub here 上的 Akka repo 上查看了 createServerHttpsContext 的源代码,发现:

  // currently the same configuration as client by default, however we should tune this for server-side apropriately (!)
  def createServerHttpsContext(sslConfig: AkkaSSLConfig): HttpsConnectionContext = 
    log.warning("Automatic server-side configuration is not supported yet, will attempt to use client-side settings. " +
      "Instead it is recommended to construct the Servers HttpsConnectionContext manually (via SSLContext).")
    createClientHttpsContext(sslConfig)
  

为什么 HTTPS 服务器不能与 createServerHttpsContext(..) 一起工作?特别是考虑到您基本上手动设置了 TLS SSLContext、KeyManagerFactory (带有密钥存储)、SecureRandom 的一个实例,然后就可以使用了。

【问题讨论】:

【参考方案1】:

当您的 HTTPS 服务器挂起 1 分钟,然后产生“由于上游故障而中止 tcp 连接:最后 1 分钟内未传递任何元素”。错误,可能是您的随机数生成器没有足够的熵在合理的时间内产生足够的随机数。

在我的例子中,我在初始化SSLContext 时使用new SecureRandom 而不是SecureRandom.getInstanceString 来“修复”这个问题。这对我来说似乎足够随机/安全,但你当然应该自己决定。

【讨论】:

【参考方案2】:

试图回答这个问题:因为它还没有实现:) 有一个open issue at github。

【讨论】:

【参考方案3】:

正如在另一条评论中发布的那样,有一个 git hub 问题指出尚不支持“自动”使用配置。但是,此问题现已关闭;刚刚完成moved。我浏览了未来版本的发行说明,但我没有看到任何与此相关的内容。现在如此强调安全性,我很惊讶 SSL/TSL 的设置不是开箱即用的。

我使用的是 v2.4.4(当前是 2.4.16)并且类似于提问者,我发现虽然 akk-http 文档告诉你使用配置,但实际上你可以从调试中看到配置被读入,实际使用它的实现尚未完成。我在日志中收到了这条消息:

akka.actor.ActorSystemImpl(OtisRestActorSystem)] Automatic server-side configuration is not supported yet, will attempt to use client-side settings. Instead it is recommended to construct the Servers HttpsConnectionContext manually (via SSLContext)

我尝试使用他们的 ssl 配置“手动构建服务器 HttpsConnectionContext”,但我无法让它工作。

还有其他消息,当我最初进行故障排除时,显示它已在配置的密钥存储中读取(它不使用类路径来查找它,因此一开始找不到它)。所以我不确定哪些部分正在工作,哪些部分丢失了。所以我最终完全放弃了 akka-http ssl 配置并自己设置它,因为我的用例非常简单。我只想启用服务器端 SSL/TSL。

在我的配置中,我有:

  ssl 
    keyStoreFileName = "myKeyFile.p12"
    keyStorePassword = "myPassword"
  

为了阅读我的设置,我有:

class Settings(config: Config) extends Extension 
  object Ssl 
    var KeyStoreFileName = config.getString("ssl.keyStoreFileName")
    var KeyStorePassword = config.getString("ssl.keyStorePassword")
  

对于我拥有的“应用程序”:

object RestWebServiceApp extends App with RouteConcatenation 
  import akka.event.Logging, LoggingAdapter
  import akka.http.scaladsl. ConnectionContext, HttpsConnectionContext, Http 
  import akka.http.scaladsl.server.Directives._
  import akka.http.scaladsl.model.MediaTypes._
  import akka.stream.ActorMaterializer, ActorMaterializerSettings
  import java.io.InputStream
  import java.security. SecureRandom, KeyStore 
  import javax.net.ssl. SSLContext, TrustManagerFactory, KeyManagerFactory 
  import JsonSupport._

  implicit val system = ActorSystem("OtisRestActorSystem")
  implicit val materializer: ActorMaterializer = ActorMaterializer(ActorMaterializerSettings(system))
  implicit val ec = system.dispatcher

  ...  //setting up all the routes, etc.

  val settings = Settings(system)
  val fileName = settings.Ssl.KeyStoreFileName
  val keyFile: InputStream = getClass.getClassLoader.getResourceAsStream(fileName)
  require(keyFile != null, s"Failed to load key file: $settings.Ssl.KeyStoreFileName")
  val extension = if(fileName.lastIndexOf('.')>0) fileName.substring(fileName.lastIndexOf('.')+1) else ""
  val keyStore: KeyStore = extension.toLowerCase match 
    case "jks" =>  KeyStore.getInstance("jks") //Java Key Store; Java default and only works with Java; tested
    case "jcek" =>  KeyStore.getInstance("JCEKS") //Java Cryptography Extension KeyStore; Java 1.4+; not tested
    case "pfx" | "p12" =>  KeyStore.getInstance("PKCS12") // PKCS #12, Common and supported by many languages/frameworks; tested
    case _ => throw new IllegalArgumentException(s"Key has an unknown type extension $extension. Support types are jks, jcek, pfx, p12.")
  
  val password: Array[Char] = (if(settings.Ssl.KeyStorePassword==null) "" else settings.Ssl.KeyStorePassword).toCharArray
  keyStore.load(keyFile, password)

  //TODO: looks like the "SunX509", "TLS", are defined in the keystore, should we pull them out rather than hard coding?
  val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("SunX509")
  keyManagerFactory.init(keyStore, password)

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

  val sslContext: SSLContext = SSLContext.getInstance("TLS")
  sslContext.init(keyManagerFactory.getKeyManagers, tmf.getTrustManagers, new SecureRandom)
  val https: HttpsConnectionContext = ConnectionContext.https(sslContext)
  Http().setDefaultServerHttpContext(https)
  Http().bindAndHandle(routes, "localhost", 433, connectionContext = https)

【讨论】:

以上是关于带有 ssl-conf 的 Akka HTTPS (SSL) 服务器的主要内容,如果未能解决你的问题,请参考以下文章

在带有 akka http 服务器的 play 框架中使用更多线程

Java Akka Actor 和流

Akka-HTTP服务器支持https

Akka 经典 - 发送自定义类消息类型

akka-http 发送连续的分块 http 响应(流)

每个数据库实体一个 Akka 演员?