• Akka-CQRS(13)- SSL/TLS for gRPC and HTTPS:自签名证书产生和使用


      到现在,我们已经完成了POS平台和前端的网络集成。不过,还是那句话:平台系统的网络安全是至关重要的。前一篇博客里我们尝试实现了gRPC ssl/tls网络连接,但测试时用的证书如何产生始终没有搞清楚。现在akka-http开发的ws同样面临HTTPS的设置和使用问题。所以,特别抽出这篇博文讨论一下数字证书的问题。

    在正式的生产环境里数字证书应该是由第三方公证机构CA签发的,我们需要向CA提出申请。数字证书的申请、签发和验证流程如下:

    1) 服务⽅ S 向第三⽅方机构CA提交公钥、组织信息、个⼈信息(域名)等资料提出认证申请 (不需要提供私钥)
    
    2) CA 通过各种手段验证申请者所提供信息的真实性,如组织是否存在、 企业是否合法,是否拥有域名的所有权等
    
    3) 如信息审核通过,CA 会向申请者签发认证文件-证书。 证书包含以下信息:申请者公钥、申请者的组织信息和个⼈信息、签发机构 CA 信息、有效时间、证书序列号等信息的明⽂,同时包含一个签名的产⽣生算法:首先,使用散列函数计算出证书中公开明文信息的信息摘要,然后, 采用 CA 的私钥对信息摘要进⾏加密,这个密⽂就是签名了
    
    4) 客户端 C 向服务器 S 发出请求时,S 返回证书文件
    
    5) 客户端 C 读取证书中的相关的明⽂信息,采⽤相同的散列函数计算得到信息摘要, 然后,利用对应 CA 的公钥解密签名数据,对比证书的信息摘要,如果一致,则可以确认证书的合法性,即公钥合法
    
    6) 客户端 C 然后检验证书相关的域名信息、有效时间等信息
    
    7) 客户端 C 应内置信任 CA 的证书信息(包含公钥),如果 CA 不被信任,则找不到对应 CA 的证书,证书也会被判定非法
    
    8) 内置 CA 对应的证书称为根证书,颁发者和使⽤者相同,用 CA ⾃⼰的私钥签名,即⾃签名证书(此证书中的公钥即为 CA 的公钥,可以使用这个公钥对证书的签名进行校验,⽆需另外⼀份证书)

    服务器端在通信中建立SSL加密渠道过程如下:

    1)客户端 C 发送请求到服务器端 S
    
    2) 服务器端 S 返回证书和公开密钥到 C,公开密钥作为证书的一部分传送
    
    3)客户端 C 检验证书和公开密钥的有效性,如果有效,则⽣成共享密钥并使⽤公开密钥加密发送到服务器端 S
    
    4) 服务器端 S 使⽤私有密钥解密数据,并用收到的共享密钥加密数据,发送到客户端 C 
    
    5) 客户端 C 使⽤用共享密钥解密数据
    
    6) SSL 加密通信渠道建立 ...

    应该说,需要在客户端进行认证的应用场景不多。这种情况需要在客户端存放数字证书。像支付宝和一些银行客户端一般都需要安装证书。

    好了,还是回到如何产生自签名证书示范吧。下面是一个标准的用openssl命令产生自签名证书流程:

    在产生证书和密钥的过程中所有系统提问回答要一致。我们先假设密码统一为:123456

    1、生成根证书私钥: rootCA.key:  openssl genrsa -des3 -out rootCA.key 2048 

    2、根证书申请 rootCA.csr:openssl req -new -key rootCA.key -out rootCA.csr

    3、用申请rootCA.csr生成根证书 rootCA.crt:openssl x509 -req -days 365 -sha256 -extensions v3_ca -signkey rootCA.key -in rootCA.csr -out rootCA.crt

    4、pem根证书 rootCA.pem:openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.pem

    5、创建⼀个v3.ext⽂件,目的是产生X509 v3证书,主要目的是指定subjectAltName选项:

      authorityKeyIdentifier=keyid,issuer
      basicConstraints=CA:FALSE
      keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
      subjectAltName = @alt_names
      [alt_names]
      DNS.1 = localhost
      IP.1 = "192.168.11.189"  
      IP.5 = "192.168.0.189"
      IP.2 = "132.232.229.60"
      IP.3 = "118.24.165.225"
      IP.4 = "129.28.108.238"

    注意subjectAltName,这些都是可以信任的域名或地址。

    6、构建证书密钥 server.key:openssl req -new -sha256 -nodes -out server.csr -newkey rsa:2048 -keyout server.key

    7、用根证书rootCA产生自签证书 server.crt:openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out server.crt -days 500 -sha256 -extfile v3.ext

    上面这个过程需要不断重复回答同样的问题,很烦。可以用配置文件来一次性产生:

    先构建一个ssl.cnf文件:

      [req]
      prompt = no
      default_bits = 4096
      default_md = sha256
      distinguished_name = dn
      x509_extensions = v3_req
      [dn]
      C=CN
      ST=GuangDong
      L=ShenZhen
      O=Bayakala
      OU=POS
      CN=www.bayakala.com
      emailAddress=admin@localhost
      [v3_req]
      keyUsage=keyEncipherment, dataEncipherment
      extendedKeyUsage=serverAuth
      subjectAltName=@alt_names
      [alt_names]
      DNS.1 = localhost
      IP.1 = "192.168.11.189"  
      IP.5 = "192.168.0.189"
      IP.2 = "132.232.229.60"
      IP.3 = "118.24.165.225"
      IP.4 = "129.28.108.238"

    然后:openssl req -new -newkey rsa:2048 -sha1 -days 3650 -nodes -x509 -keyout server.key -out server.crt -config ssl.cnf

    一个指令同时产生需要的server.crt,server.key。

    除aubjectAltName外还要关注CN这个字段,它就是我们经常会遇到系统提问:你确定信任“域名”吗?中这个域名,也就是对外界开放的一个使用了数字证书的域名。

    把crt,key抄写到main/resources目录下,然后在gRPC服务器配置证书:

    trait gRPCServer {
      
      val serverCrtFile = new File(getClass.getClassLoader.getResource("server.crt").getPath)
      val serverKeyFile = new File(getClass.getClassLoader.getResource("server.key").getPath)
    
      def runServer(service: ServerServiceDefinition): Unit = {
        val server = NettyServerBuilder
          .forPort(50051)
          .addService(service)
          .useTransportSecurity(serverCrtFile,serverKeyFile)
          .build
          .start
        // make sure our server is stopped when jvm is shut down
        Runtime.getRuntime.addShutdownHook(new Thread() {
          override def run(): Unit = {
            server.shutdown()
            server.awaitTermination()
          }
        })
      }
    
    }

    启动gRPC服务,运作正常。在看看客户端代码:

        val clientCrtFile = new File(getClass.getClassLoader.getResource("server.crt").getPath)
     //或者   val clientCrtFile = new File(getClass.getClassLoader.getResource("rootCA.pem").getPath)
    
    //这样也行 val clientCrtFile: InputStream = getClass.getClassLoader.getResourceAsStream("rootCA.pem")
    
        val sslContextBuilder = GrpcSslContexts.forClient().trustManager(clientCrtFile)
    
        //build connection channel
        val channel = NettyChannelBuilder
          .forAddress("192.168.11.189",50051)
          .negotiationType(NegotiationType.TLS)
          .sslContext(sslContextBuilder.build())
    //      .overrideAuthority("192.168.1.3")
          .build()

    测试连接,gRPC SSL/TLS成功!

    现在开始了解一下https证书的配置使用方法吧。看了一下akka-http关于server端HTTPS设置的例子,证书是嵌在HttpsConnectionContext类型里面的。还有就是akka-http使用的https证书格式只支持pkcs12,所以需要把上面用openssl产生的自签名证书server.crt转成server.p12。这个转换又需要先产生证书链certificate-chain chain.pem:

    1)产生certificate-chain:  cat server.crt rootCA.crt > chain.pem

    2) server.crt转换成server.p12: openssl pkcs12 -export -name servercrt -in chain.pem -inkey server.key -out server.p12

    https server 测试代码:

    //#imports
    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 akka.http.scaladsl.Http
    import akka.http.scaladsl.server.Directives._
    
    //#imports
    
    
    object HttpsDemo extends App {
    
      implicit val httpSys = ActorSystem("httpSystem")
      implicit val httpMat = ActorMaterializer()
      implicit val httpEC = httpSys.dispatcher
      
    
        val password: Array[Char] = "123456".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")
        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)
    
    
      val route = get { complete("Hello world!") }
    
      val (port, host) = (50081,"192.168.11.189")
    
      val bindingFuture = Http().bindAndHandle(route,host,port,connectionContext = https)
    
      println(s"Https Server running at $host $port. Press any key to exit ...")
    
      scala.io.StdIn.readLine()
    
    
      bindingFuture.flatMap(_.unbind())
        .onComplete(_ => httpSys.terminate())
    
    }

    用safari连接https://192.168.11.189:50081/, 弹出窗口一堆废话后还是成功连接上了。

  • 相关阅读:
    js 闭包
    jQuery——表单异步提交
    jaxFileUpload插件异步上传图片
    phpstorm设置断点调试
    mysql函数,语法
    mysql函数替换域名
    php对输入的检测
    超实用的php代码片段
    js检测到如果是手机端就跳转到手机端的网址代码
    定时运行某个php文件的bat文件
  • 原文地址:https://www.cnblogs.com/tiger-xc/p/11075240.html
Copyright © 2020-2023  润新知