1. 網(wǎng)絡(luò)通訊的安全性問題
在本系列的第1、2、3、25篇文章,分別講解了使用UDP以及TCP進(jìn)行通訊的方式,并且以回聲服務(wù)器的形式分別演示了客戶端與服務(wù)端之間的通訊。這種通訊方式一般來說沒什么問題,但是在需要對內(nèi)容保密的情況下就不可取了,畢竟它們都是通過明文進(jìn)行通訊的,如果數(shù)據(jù)包在傳輸過程中被攔截,攻擊者可以直接讀取其中的信息,這使得用戶的敏感信息(如密碼、個(gè)人資料等)容易遭受竊聽或篡改。要避免這種情況的發(fā)生,可以使用TLS通訊,它通過加密技術(shù)確保數(shù)據(jù)的保密性和完整性,防止數(shù)據(jù)在傳輸過程中被竊聽或篡改。當(dāng)使用TLS進(jìn)行通訊時(shí),客戶端和服務(wù)器會(huì)先進(jìn)行一個(gè)握手過程,在這個(gè)過程中雙方協(xié)商加密算法、交換加密密鑰等,之后所有傳輸?shù)臄?shù)據(jù)都會(huì)被加密,即使數(shù)據(jù)包被第三方截獲,由于沒有解密密鑰,第三方也無法讀取數(shù)據(jù)的真實(shí)內(nèi)容。
在本系列的第7、8篇文章,介紹了TLS客戶端的使用,本篇將介紹TLS服務(wù)端的使用,TLS服務(wù)端在HarmonyOS NEXT的KIT開放能力模型中,歸屬于系統(tǒng)相關(guān)Kit開放能力中的Network Kit(網(wǎng)絡(luò)服務(wù)),對應(yīng)的類名稱為TLSSocketServer,使用如下的代碼導(dǎo)入模塊:
import { socket } from '@kit.NetworkKit';
在使用其方法前需要先通過socket.constructTLSSocketServerInstance方法創(chuàng)建實(shí)例。
本文將演示TLS服務(wù)端的用法,創(chuàng)建一個(gè)TLS回聲服務(wù)器,并通過TLS客戶端和其進(jìn)行通訊。
2. TLS回聲服務(wù)器演示
本示例運(yùn)行后的界面如圖所示:

選擇服務(wù)端數(shù)字證書及數(shù)字證書對應(yīng)的私鑰,輸入要綁定的服務(wù)端端口,然后單擊“啟動(dòng)”按鈕即可啟動(dòng)TLS服務(wù),如圖所示:

然后啟動(dòng)TLS客戶端,可以使用本系列前述文章介紹的客戶端,也可以使用其他客戶端,啟動(dòng)后,再選擇服務(wù)端CA證書,輸入服務(wù)端地址和端口,最后連接服務(wù)端,如圖所示:

可以看到,TLS服務(wù)端連接成功了,并且在日志區(qū)域輸出了服務(wù)端的證書信息。
下面測試TLS通訊,輸入要發(fā)送的信息,然后單擊“發(fā)送”按鈕,就會(huì)收到服務(wù)端自動(dòng)回復(fù)的消息,如圖所示:

此時(shí),查看TLS服務(wù)端界面,可以看到服務(wù)端也收到了客戶端的消息:

3. TLS回聲服務(wù)器示例編寫
下面詳細(xì)介紹創(chuàng)建該示例的步驟。
步驟1:創(chuàng)建Empty Ability項(xiàng)目。
步驟2:在module.json5配置文件加上對權(quán)限的聲明:
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
這里添加了訪問互聯(lián)網(wǎng)的權(quán)限。
步驟3:在Index.ets文件里添加如下的代碼:
import { socket } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { ArrayList, buffer, util } from '@kit.ArkTS';
import fs from '@ohos.file.fs';
import { picker } from '@kit.CoreFileKit';
//TLS服務(wù)端實(shí)例
let tlsSocketServer: socket.TLSSocketServer = socket.constructTLSSocketServerInstance()
@Entry
@Component
struct Index {
@State title: string = 'TLS回聲服務(wù)器示例';
@State running: boolean = false
//連接、通訊歷史記錄
@State msgHistory: string = ''
//本地端口
@State port: number = 9999
//選擇的證書文件
@State certFileUri: string = ''
//選擇的私鑰文件
@State keyFileUri: string = ''
scroller: Scroller = new Scroller()
//已連接的客戶端列表
clientList = new ArrayList<socket.TLSSocketConnection>()
build() {
Row() {
Column() {
Text(this.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)
.padding(10)
Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
Text("服務(wù)端數(shù)字證書")
.fontSize(14)
.flexGrow(1)
Button("選擇")
.onClick(async () => {
this.certFileUri = await selectSingleDocFile(getContext(this))
})
.width(70)
.fontSize(14)
}
.width('100%')
.padding(5)
Text(this.certFileUri)
.width('100%')
.padding(5)
Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
Text("服務(wù)端數(shù)字證書私鑰:")
.fontSize(14)
.flexGrow(1)
Button("選擇")
.onClick(async () => {
this.keyFileUri = await selectSingleDocFile(getContext(this))
})
.width(70)
.fontSize(14)
}
.width('100%')
.padding(5)
Text(this.keyFileUri)
.width('100%')
.padding(5)
Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
Text("綁定的服務(wù)器端口:")
.fontSize(14)
.width(150)
TextInput({ text: this.port.toString() })
.type(InputType.Number)
.onChange((value) => {
this.port = parseInt(value)
})
.fontSize(12)
.width(100)
.flexGrow(1)
Button(this.running ? "停止" : "啟動(dòng)")
.onClick(() => {
if (!this.running) {
this.startServer()
} else {
this.stopServer()
}
})
.width(70)
.fontSize(14)
}
.width('100%')
.padding(5)
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%')
}
//停止服務(wù)
stopServer() {
tlsSocketServer.off('connect')
for (let client of this.clientList) {
client.off('message')
}
this.running = false
this.msgHistory += "停止服務(wù)\r\n"
}
//獲取tls監(jiān)聽配置信息
getTlsConnOptions(): socket.TLSConnectOptions {
let listenAddress: socket.NetAddress = { address: '0.0.0.0', port: this.port }
let context = getContext(this)
let tlsSecOptions: socket.TLSSecureOptions = {
cert: copy2SandboxAndReadContent(context, this.certFileUri),
key: copy2SandboxAndReadContent(context, this.keyFileUri)
}
return { address: listenAddress, secureOptions: tlsSecOptions }
}
//啟動(dòng)服務(wù)
async startServer() {
//獲取tls監(jiān)聽配置
let tlsConnOptions = this.getTlsConnOptions()
//綁定到指定的端口并啟動(dòng)客戶端連接監(jiān)聽
await tlsSocketServer.listen(tlsConnOptions).then(this.onListenSuccessful)
.catch((err: BusinessError) => {
this.msgHistory += `監(jiān)聽失敗: 錯(cuò)誤碼 ${err.code}, 錯(cuò)誤信息 ${JSON.stringify(err)}\r\n`;
})
//訂閱連接事件消息
tlsSocketServer.on('connect', this.onNewClientConnected);
}
//監(jiān)聽成功的回調(diào)
onListenSuccessful = async () => {
let listenAddr: socket.NetAddress = await tlsSocketServer.getLocalAddress()
this.msgHistory += `監(jiān)聽成功[${listenAddr.address}:${listenAddr.port}]\r\n`
this.running = true
this.msgHistory += "服務(wù)啟動(dòng)\r\n"
}
//接受新的客戶端連接的回調(diào)
onNewClientConnected = async (clientSocket: socket.TLSSocketConnection) => {
this.clientList.add(clientSocket)
//客戶端地址
let clientAddr: socket.NetAddress = await clientSocket.getRemoteAddress()
this.msgHistory += `接受新的客戶端連接[${clientAddr.address}:${clientAddr.port}]\r\n`
clientSocket.on('message', (msgInfo: socket.SocketMessageInfo) => {
//收到的信息轉(zhuǎn)化為字符串
let content = buf2String(msgInfo.message)
//顯示信息日志,最后加上回車換行
this.msgHistory += `[${msgInfo.remoteInfo.address}:${msgInfo.remoteInfo.port}]${content}\r\n`
//把收到的信息發(fā)回客戶端
clientSocket.send(buffer.from(content).buffer)
})
}
}
//選擇一個(gè)文件
async function selectSingleDocFile(context: Context): Promise<string> {
let selectedFilePath: string = ""
let documentPicker = new picker.DocumentViewPicker(context);
await documentPicker.select({ maxSelectNumber: 1 }).then((result) => {
if (result.length > 0) {
selectedFilePath = result[0]
}
})
return selectedFilePath
}
//復(fù)制文件到沙箱并讀取文件內(nèi)容
function copy2SandboxAndReadContent(context: Context, filePath: string): string {
let segments = filePath.split('/')
let fileName = segments[segments.length-1]
let realUri = context.cacheDir + "/" + fileName
let file = fs.openSync(filePath);
fs.copyFileSync(file.fd, realUri)
fs.closeSync(file)
return fs.readTextSync(realUri)
}
//ArrayBuffer轉(zhuǎn)utf8字符串
export function buf2String(buf: ArrayBuffer) {
let msgArray = new Uint8Array(buf);
let textDecoder = util.TextDecoder.create("utf-8");
return textDecoder.decodeToString(msgArray)
}
步驟4:編譯運(yùn)行,可以使用模擬器或者真機(jī)。
步驟5:按照本節(jié)第2部分“TLS回聲服務(wù)器演示”操作即可。
4. 代碼分析
本示例關(guān)鍵點(diǎn)在于TLS服務(wù)器的配置,特別是配置TLS服務(wù)端的證書,因?yàn)槲募?quán)限的關(guān)系,本示例在用戶選擇證書和證書私鑰文件后,把這些文件首選復(fù)制到沙箱,然后再讀取文件內(nèi)容,TLS配置的代碼如下所示:
getTlsConnOptions(): socket.TLSConnectOptions {
let listenAddress: socket.NetAddress = { address: '0.0.0.0', port: this.port }
let context = getContext(this)
let tlsSecOptions: socket.TLSSecureOptions = {
cert: copy2SandboxAndReadContent(context, this.certFileUri),
key: copy2SandboxAndReadContent(context, this.keyFileUri)
}
return { address: listenAddress, secureOptions: tlsSecOptions }
}
復(fù)制文件到沙箱并讀取文件內(nèi)容的代碼如下所示:
function copy2SandboxAndReadContent(context: Context, filePath: string): string {
let segments = filePath.split('/')
let fileName = segments[segments.length-1]
let realUri = context.cacheDir + "/" + fileName
let file = fs.openSync(filePath);
fs.copyFileSync(file.fd, realUri)
fs.closeSync(file)
return fs.readTextSync(realUri)
}
(本文作者原創(chuàng),除非明確授權(quán)禁止轉(zhuǎn)載)
本文源碼地址:
https://gitee.com/zl3624/harmonyos_network_samples/tree/master/code/tls/TLSEchoServer