HarmonyOS 鸿蒙Next网络编程系列8-TLS安全数据传输双向认证示例

发布于 1周前 作者 songsunli 来自 鸿蒙OS

HarmonyOS 鸿蒙Next网络编程系列8-TLS安全数据传输双向认证示例

1.TLS双向认证

在上一篇文章鸿蒙网络编程系列7-TLS安全数据传输单向认证示例中,演示了单向认证的方式,也就是客户端需要验证服务端的数字证书,而服务端不需要验证客户端的数字证书,这能满足大部分业务场景,但是,在一些高安全性需求的场景下,服务端也需要确保客户端是可信的,这就要求双向认证,也就是除了客户端验证服务端证书外,服务端也需要验证客户端的数字证书。

要实现双向的认证,就需要服务端在握手时提出客户端的数字证书认证需求,以ECDHE算法的握手过程为例,在第二次握手时,需要服务端发送Certificate Request消息给客户端,表明是双向认证的,在第三次握手时,客户端发送Certificate消息给服务端,其中就包含证书信息。

2.TLS双向认证通讯示例

本文将实现一个双向认证的示例,应用运行后的界面如图所示:

双向认证示例界面

在这个应用里,用户可以选择服务端的CA文件,作为验证服务端证书有效性的依据;还需要选择客户端证书文件和客户端私钥文件,用来执行服务端对客户端的认证。这三个文件都选择并加载后,就可以单击“连接”按钮连接服务端了,连接成功后可以发送消息给服务端。

下面详细介绍创建该应用的步骤。

步骤1:创建Empty Ability项目。

步骤2:在module.json5配置文件加上对权限的声明:

{
  "requestPermissions": [
    {
      "name": "ohos.permission.INTERNET"
    },
    {
      "name": "ohos.permission.GET_WIFI_INFO"
    }
  ]
}

这里分别添加了访问互联网和访问WIFI信息的权限。

步骤3:在Index.ets文件里添加如下的代码:

import socket from '@ohos.net.socket';
import wifiManager from '@ohos.wifiManager';
import systemDateTime from '@ohos.systemDateTime';
import util from '@ohos.util';
import picker from '@ohos.file.picker';
import fs from '@ohos.file.fs';
import common from '@ohos.app.ability.common';

//执行TLS通讯的对象
let tlsSocket = socket.constructTLSSocketInstance()

//说明:本地的IP地址不是必须知道的,绑定时绑定到IP:0.0.0.0即可,显示本地IP地址的目的是方便对方发送信息过来
//本地IP的数值形式
let ipNum = wifiManager.getIpInfo().ipAddress
//本地IP的字符串形式
let localIp = (ipNum >>> 24) + '.' + (ipNum >> 16 & 0xFF) + '.' + (ipNum >> 8 & 0xFF) + '.' + (ipNum & 0xFF);

//服务端ca证书文件地址
let caFileUri = ''

//客户端证书文件地址
let certFileUri = ''

//客户端私钥文件地址
let keyFileUri = ''

@Entry
@Component
struct Index {
  //连接、通讯历史记录
  @State msgHistory: string = ''
  //要发送的信息
  @State sendMsg: string = ''
  //服务端IP地址
  @State serverIp: string = "0.0.0.0"
  //服务端端口
  @State serverPort: number = 9999
  //是否可以加载CA
  @State caCanLoad: boolean = false
  //是否已加载CA
  @State caLoaded: boolean = false
  //是否可以加载证书
  @State certCanLoad: boolean = false
  //是否已加载证书
  @State certLoaded: boolean = false
  //是否可以加载私钥
  @State keyCanLoad: boolean = false
  //是否已加载私钥
  @State keyLoaded: boolean = false
  //是否可以发送消息
  @State canSend: boolean = false
  //服务端证书
  @State ca: string = ``
  //客户端证书
  @State cert: string = ``
  //客户端私钥
  @State privateKey: string = ``
  scroller: Scroller = new Scroller()

  build() {
    Row() {
      Column() {
        Text("TLS通讯示例")
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding(10)

        Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
          Text("本地IP地址:")
            .fontSize(14)
            .width(100)
            .flexGrow(0)
          Text(localIp)
            .fontSize(12)
            .flexGrow(1)
        }
        .width('100%')
        .padding(10)

        Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
          Text("服务端地址:")
            .fontSize(14)
            .width(90)
            .flexGrow(1)

          TextInput({ text: this.serverIp })
            .onChange(value => {
              this.serverIp = value
            })
            .width(110)
            .fontSize(12)
            .flexGrow(4)

          Text(":")
            .width(5)
            .flexGrow(0)

          TextInput({ text: this.serverPort.toString() })
            .type(InputType.Number)
            .onChange(value => {
              this.serverPort = parseInt(value)
            })
            .fontSize(12)
            .flexGrow(2)
            .width(50)
        }
        .width('100%')
        .padding(10)

        Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
          Text("服务端CA:")
            .fontSize(14)
            .width(90)
            .flexGrow(1)

          Button("选择")
            .onClick(() => {
              this.selectCA()
            })
            .width(70)
            .fontSize(14)

          Button("加载")
            .onClick(() => {
              this.loadCA()
            })
            .enabled(this.caCanLoad)
            .width(70)
            .fontSize(14)
        }
        .width('100%')
        .padding(10)

        Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
          Text("客户端证书:")
            .fontSize(14)
            .width(90)
            .flexGrow(1)

          Button("选择")
            .onClick(() => {
              this.selectCert()
            })
            .width(70)
            .fontSize(14)

          Button("加载")
            .onClick(() => {
              this.loadCert()
            })
            .enabled(this.caCanLoad)
            .width(70)
            .fontSize(14)
        }
        .width('100%')
        .padding(10)

        Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
          Text("客户端私钥:")
            .fontSize(14)
            .width(90)
            .flexGrow(1)

          Button("选择")
            .onClick(() => {
              this.selectKey()
            })
            .width(70)
            .fontSize(14)

          Button("加载")
            .onClick(() => {
              this.loadKey()
            })
            .enabled(this.caCanLoad)
            .width(70)
            .fontSize(14)

          Button("连接")
            .onClick(() => {
              this.connect2Server()
            })
            .enabled(this.caLoaded && this.keyLoaded && this.certLoaded)
            .width(70)
            .fontSize(14)
        }
        .width('100%')
        .padding(10)

        Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
          TextInput({ placeholder: "输入要发送的消息" }).onChange(value => {
            this.sendMsg = value
          })
            .width(200)
            .flexGrow(1)

          Button("发送")
            .enabled(this.canSend)
            .width(70)
            .fontSize(14)
            .flexGrow(0)
            .onClick(() => {
              this.sendMsg2Server()
            })
        }
        .width('100%')
        .padding(10)

        Scroll(this.scroller) {
          Text(this.msgHistory)
            .textAlign(TextAlign.Start)
            .padding(10)
            .width('100%')
            .backgroundColor(0xeeeeee)
        }
        .align(Alignment.Top)
        .backgroundColor(0xeeeeee)
        .height(300)
        .flexGrow(1)
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.On)
        .scrollBarWidth(20)
      }
      .width('100%')
      .justifyContent(FlexAlign.Start)
      .height('100%')
    }
    .height('100%')
  }

  //发送消息到服务端
  sendMsg2Server() {
    tlsSocket.send(this.sendMsg + "\r\n")
      .then(async () => {
        this.msgHistory += "我:" + this.sendMsg + await getCurrentTimeString() + "\r\n"
      })
      .catch(e => {
        this.msgHistory += '发送失败' + e.message + "\r\n";
      })
  }

  //绑定本地地址
  async bind2LocalAddress() {
    //本地地址
    let localAddress = { address: "0.0.0.0", family: 1 }

    await tlsSocket.bind(localAddress)
      .then(() => {
        this.msgHistory += 'bind success' + "\r\n";
      })
      .catch(e => {
        this.msgHistory += 'bind fail ' + e.message + "\r\n";
      })

    //收到消息时的处理
    tlsSocket.on("message", async (value) => {
      let msg = buf2String(value.message)
      let time = await getCurrentTimeString()
      this.msgHistory += "服务端:" + msg + time + "\r\n"
      this.scroller.scrollEdge(Edge.Bottom)
    })
  }

  //选择CA证书文件
  selectCA() {
    let documentPicker = new picker.DocumentViewPicker();
    documentPicker.select().then(result => {
      if (result.length > 0) {
        caFileUri = result[0]
        this.msgHistory += "select file: " + caFileUri + "\r\n";
        this.caCanLoad = true
      }
    }).catch(e => {
      this.msgHistory += 'DocumentViewPicker.select failed ' + e.message + "\r\n";
    });
  }

  //选择客户端证书文件
  selectCert() {
    let documentPicker = new picker.DocumentViewPicker();
    documentPicker.select().then(result => {
      if (result.length > 0) {
        certFileUri = result[0]
        this.msgHistory += "select file: " + certFileUri + "\r\n";
        this.certCanLoad = true
      }
    }).catch(e => {
      this.msgHistory += 'DocumentViewPicker.select failed ' + e.message + "\r\n";
    });
  }

  //选择私钥文件
  selectKey() {
    let documentPicker = new picker.DocumentViewPicker();
    documentPicker.select().then(result => {
      if (result.length > 0) {
        keyFileUri = result[0]
        this.msgHistory += "select file: " + keyFileUri + "\r\n";
        this.keyCanLoad = true
      }
    }).catch(e => {
      this.msgHistory += 'DocumentViewPicker.select failed ' + e.message + "\r\n";
    });
  }

  //加载CA文件内容
  loadCA() {
    try {
      this.ca = this.readStringFromFile(caFileUri)
      this.caLoaded = true
    }
    catch (e) {
      this.msgHistory += 'readText failed ' + e.message + "\r\n";
    }
  }

  //加载私钥文件内容
  loadKey() {
    try {
      this.privateKey = this.readStringFromFile(keyFileUri)
      this.keyLoaded = true
    }
    catch (e) {
      this.msgHistory += 'readText failed ' + e.message + "\r\n";
    }
  }

  //加载客户端证书文件内容
  loadCert() {
    try {
      this.cert = this.readStringFromFile(certFileUri)
      this.certLoaded = true
    }
    catch (e) {
      this.msgHistory += 'readText failed ' + e.message + "\r\n";
    }
  }

  //从文件读取字符串内容
  readStringFromFile(fileUri: string): string {
    let buf = new ArrayBuffer(1024 * 4);
    let file = fs.openSync(fileUri, fs.OpenMode.READ_ONLY);
    let readLen = fs.readSync(file.fd, buf, { offset: 0 });
    let result = buf2String(buf.slice(0, readLen))
    fs.closeSync(file);
    return result
  }

  //连接服务端
  async connect2Server() {
    //绑定本地地址
    await this.bind2LocalAddress()

    //服务端地址
    let serverAddress = { address: this.serverIp, port: this.serverPort, family: 1 }
    //tls选项
    let opt: socket.TLSSecureOptions = {
      ca: [this.ca],
      cert: this.cert,
      key: this.privateKey
    }

    await tlsSocket.connect({ address: serverAddress, secureOptions: opt })
      .then(() => {
        this.msgHistory = 'connect success ' + "\r\n";
        this.canSend = true
      })
      .catch(e => {
        this.msgHistory = 'connect fail ' + e.message + "\r\n";
      })
  }
}

//同步获取当前时间的字符串形式
async function getCurrentTimeString() {
  let time = ""
  await systemDateTime.getDate().then(
    date => {
      time = date.getHours().toString() + ":" + date.getMinutes().toString()
        + ":" + date.getSeconds().toString()
    }
  )
  return "[" + time + "]"
}

//ArrayBuffer转utf8字符串
function buf2String(buf: ArrayBuffer) {
  let msgArray = new Uint8Array(buf);
  let textDecoder = util.TextDecoder.create("utf-8");
  return textDecoder.decodeWithStream(msgArray)
}

步骤4:编译运行,可以使用模拟器或者真机。

步骤5:加载服务端CA、客户端证书、客户端私钥文件,当然需要确保服务端已经启动了,然后配置服务端地址,再单击“连接”按钮执行连接,如图所示:

加载文件界面

连接成功后,如果在服务端监听,可以看到如下的连接过程信息:

连接过程信息

其中,使用框线标出的即是服务端发送给客户端的认证请求信息以及客户端发送给服务端的证书信息。

步骤6:输入要发送的信息,单击“发送”按钮即可发送信息到服务端,如下图所示:

发送信息界面

这样就完成了TLS双向认证消息发送应用的编写。

(本文作者原创,除非明确授权禁止转载)

其他鸿蒙网络编程文章:


更多关于HarmonyOS 鸿蒙Next网络编程系列8-TLS安全数据传输双向认证示例的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html

2 回复

HarmonyOS鸿蒙Next网络编程系列8-TLS安全数据传输双向认证示例主要展示了如何在鸿蒙系统中实现TLS(Transport Layer Security)安全数据传输的双向认证。双向认证要求客户端和服务器端都验证对方的身份,确保通信双方的可信性。

在鸿蒙系统中,TLS双向认证的实现通常涉及以下几个步骤:

  1. 证书准备:客户端和服务器端都需要准备各自的证书和私钥。客户端证书用于向服务器证明其身份,服务器证书用于向客户端证明其身份。

  2. TLS配置:在鸿蒙系统中,使用HttpsURLConnectionOkHttp等网络库进行TLS配置。需要设置客户端证书、服务器证书以及信任的CA证书。

  3. 双向认证实现:在客户端,配置KeyManagerTrustManager,分别用于管理客户端证书和验证服务器证书。在服务器端,同样需要配置KeyManagerTrustManager,用于管理服务器证书和验证客户端证书。

  4. 建立安全连接:通过配置好的TLS参数,客户端和服务器端建立安全连接,进行加密数据传输。

示例代码可能包括以下关键部分:

  • 客户端证书加载
KeyStore keyStore = KeyStore.getInstance("PKCS12")
keyStore.load(new FileInputStream("client.p12"), "password".toCharArray())
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, "password".toCharArray())
  • 服务器证书验证
KeyStore trustStore = KeyStore.getInstance("JKS")
trustStore.load(new FileInputStream("truststore.jks"), "password".toCharArray())
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(trustStore)
  • TLS上下文配置
SSLContext sslContext = SSLContext.getInstance("TLS")
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom())
  • 建立安全连接
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection()
connection.setSSLSocketFactory(sslContext.getSocketFactory())

更多关于HarmonyOS 鸿蒙Next网络编程系列8-TLS安全数据传输双向认证示例的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS鸿蒙Next中,实现TLS安全数据传输的双向认证,首先需配置客户端和服务器端的证书。使用TLSSocket类创建安全连接,设置客户端证书链和私钥,同时验证服务器证书。服务器端需配置服务器证书和私钥,并验证客户端证书。通过TLSSocket.connect()建立连接,确保数据在加密通道中传输,增强通信安全性。示例代码需包含证书加载、TLS配置及连接建立步骤,确保双向认证流程正确执行。

回到顶部
AI 助手
你好,我是IT营的 AI 助手
您可以尝试点击下方的快捷入口开启体验!