Nginx 配置WSS 解析與實(shí)戰(zhàn)

Nginx 配置WSS 解析與實(shí)戰(zhàn)

1. 幾個(gè)關(guān)鍵概念

1.1 WebSocket

WebSocket 協(xié)議是 html5 的一種通信協(xié)議,該協(xié)議兼容我們常用的瀏覽器。例如:Chrome、Firefox、IE 等。它可以使客戶(hù)端和服務(wù)端雙向數(shù)據(jù)傳輸更加簡(jiǎn)單快捷,并且在TCP連接進(jìn)行一次握手后,就可以持久性連接,同時(shí)允許服務(wù)端對(duì)客戶(hù)端推送數(shù)據(jù)。外加傳統(tǒng)模式的協(xié)議一般HTTP輕輕可能會(huì)包含較長(zhǎng)的頭部,但真正有效的可能只有小部分,從而占用了很多資源和寬帶。因此WebSocket協(xié)議不僅可以實(shí)時(shí)通訊,支持?jǐn)U展;也可以壓縮節(jié)省服務(wù)資源和寬帶。 關(guān)于WebSocket的深入理解,可以看這篇文章:Html5 WebSocket 。

WS 協(xié)議 和 WSS 協(xié)議兩個(gè)均是 WebSocket 協(xié)議的 SCHEM,兩者一個(gè)是非安全的,一個(gè)是安全的。也是統(tǒng)一的資源標(biāo)志符。就好比 HTTP協(xié)議 和 HTTPS協(xié)議的差別。非安全的沒(méi)有證書(shū),安全的需要 SSL 證書(shū)。其中 WSS 表示在 TLS 之上的 WebSocket。

WS 一般默認(rèn)是 80 端口,而 WSS 默認(rèn)是 443 端口,大多數(shù)網(wǎng)站用的就是 80 和 443 端口。

image
image

1.2 SSL

SSL(Secure Socket Layer,安全套接層) 簡(jiǎn)單來(lái)說(shuō)是一種加密技術(shù), 通過(guò)它, 我們可以在通信的雙方上建立一個(gè)安全的通信鏈路, 因此數(shù)據(jù)交互的雙方可以安全地通信, 而不需要擔(dān)心數(shù)據(jù)被竊取. 關(guān)于 SSL 的深入知識(shí), 可以看這篇文章: SSL/TLS協(xié)議運(yùn)行機(jī)制的概述。

1.3 WSS

WSS 是 Web Socket Secure 的簡(jiǎn)稱(chēng), 它是 WebSocket 的加密版本. 我們知道 WebSocket 中的數(shù)據(jù)是不加密的, 但是不加密的數(shù)據(jù)很容易被別有用心的人竊取, 因此為了保護(hù)數(shù)據(jù)安全, 人們將 WebSocket 與 SSL 結(jié)合, 實(shí)現(xiàn)了安全的 WebSocket 通信, 即 WebSocket Secure。所以說(shuō) WSS 是使用 SSL 進(jìn)行加密了的 WebSocket 通信技術(shù)。

1.3 HTTPS

其實(shí) HTTPS 和 WSS 類(lèi)似, HTTP 之于 HTTPS 就像 WebSocket 之于 WebSocket Secure。
HTTP 協(xié)議本身也是明文傳輸, 因此為了數(shù)據(jù)的安全性, 人們利用 SSL 作為加密通道, 在 SSL 之上傳遞 HTTP 數(shù)據(jù), 因此 SSL 加密通道上運(yùn)行的 HTTP 協(xié)議就被稱(chēng)為 HTTPS 了。

1.4 總結(jié)

SSL 是基礎(chǔ), 在 SSL 上運(yùn)行 WebSocket 協(xié)議就是 WSS; 在 SSL 上運(yùn)行 HTTP 協(xié)議就是 HTTPS。

2. SSL單向/雙向認(rèn)證

2.1 單向認(rèn)證 SSL 協(xié)議的具體過(guò)程

  1. 客戶(hù)端的瀏覽器向服務(wù)器傳送客戶(hù)端 SSL 協(xié)議的版本號(hào),加密算法的種類(lèi),產(chǎn)生的隨機(jī)數(shù),以及其他服務(wù)器和客戶(hù)端之間通訊所需要的各種信息。
  2. 服務(wù)器向客戶(hù)端傳送 SSL 協(xié)議的版本號(hào),加密算法的種類(lèi),隨機(jī)數(shù)以及其他相關(guān)信息,同時(shí)服務(wù)器還將向客戶(hù)端傳送自己的證書(shū)。
  3. 客戶(hù)利用服務(wù)器傳過(guò)來(lái)的信息驗(yàn)證服務(wù)器的合法性,服務(wù)器的合法性包括:證書(shū)是否過(guò)期,發(fā)行服務(wù)器證書(shū)的 CA 是否可靠,發(fā)行者證書(shū)的公鑰能否正確解開(kāi)服務(wù)器證書(shū)的“發(fā)行者的數(shù)字簽名”,服務(wù)器證書(shū)上的域名是否和服務(wù)器的實(shí)際域名相匹配。如果合法性驗(yàn)證沒(méi)有通過(guò), 通訊將斷開(kāi);如果合法性驗(yàn)證通過(guò),將繼續(xù)進(jìn)行第四步。
  4. 用戶(hù)端隨機(jī)產(chǎn)生一個(gè)用于后面通訊的“對(duì)稱(chēng)密碼”,然后用服務(wù)器的公鑰(服務(wù)器的公鑰從步驟②中的服務(wù)器的證書(shū)中獲得)對(duì)其加密,然后將加密后的“預(yù)主密碼”傳給服務(wù)器。
  5. 如果服務(wù)器要求客戶(hù)的身份認(rèn)證(在握手過(guò)程中為可選),用戶(hù)可以建立一個(gè)隨機(jī)數(shù)然后對(duì)其進(jìn)行數(shù)據(jù)簽名,將這個(gè)含有簽名的隨機(jī)數(shù)和客戶(hù)自己的證書(shū)以及加密過(guò)的“預(yù)主密碼”一起傳給服務(wù)器。
  6. 如果服務(wù)器要求客戶(hù)的身份認(rèn)證,服務(wù)器必須檢驗(yàn)客戶(hù)證書(shū)和簽名隨機(jī)數(shù)的合法性,具體的合法性驗(yàn)證過(guò)程包括:客戶(hù)的證書(shū)使用日期是否有效,為客戶(hù)提供證書(shū)的 CA 是否可靠,發(fā)行CA 的公鑰能否正確解開(kāi)客戶(hù)證書(shū)的發(fā)行 CA 的數(shù)字簽名,檢查客戶(hù)的證書(shū)是否在證書(shū)廢止列表(CRL)中。檢驗(yàn)如果沒(méi)有通過(guò),通訊立刻中斷;如果驗(yàn)證通過(guò),服務(wù)器將用自己的私鑰解開(kāi)加密的“預(yù)主密碼 ”,然后執(zhí)行一系列步驟來(lái)產(chǎn)生主通訊密碼(客戶(hù)端也將通過(guò)同樣的方法產(chǎn)生相同的主通訊密碼)。
  7. 服務(wù)器和客戶(hù)端用相同的主密碼即“通話(huà)密碼”,一個(gè)對(duì)稱(chēng)密鑰用于 SSL 協(xié)議的安全數(shù)據(jù)通訊的加解密通訊。同時(shí)在 SSL 通訊過(guò)程中還要完成數(shù)據(jù)通訊的完整性,防止數(shù)據(jù)通訊中的任何變化。
  8. 客戶(hù)端向服務(wù)器端發(fā)出信息,指明后面的數(shù)據(jù)通訊將使用的步驟⑦中的主密碼為對(duì)稱(chēng)密鑰,同時(shí)通知服務(wù)器客戶(hù)端的握手過(guò)程結(jié)束。
  9. 服務(wù)器向客戶(hù)端發(fā)出信息,指明后面的數(shù)據(jù)通訊將使用的步驟⑦中的主密碼為對(duì)稱(chēng)密鑰,同時(shí)通知客戶(hù)端服務(wù)器端的握手過(guò)程結(jié)束。
  10. SSL 的握手部分結(jié)束,SSL 安全通道的數(shù)據(jù)通訊開(kāi)始,客戶(hù)和服務(wù)器開(kāi)始使用相同的對(duì)稱(chēng)密鑰進(jìn)行數(shù)據(jù)通訊,同時(shí)進(jìn)行通訊完整性的檢驗(yàn)。

2.2 雙向認(rèn)證 SSL 協(xié)議的具體過(guò)程

  1. 瀏覽器發(fā)送一個(gè)連接請(qǐng)求給安全服務(wù)器。
  2. 服務(wù)器將自己的證書(shū),以及同證書(shū)相關(guān)的信息發(fā)送給客戶(hù)瀏覽器。
  3. 客戶(hù)瀏覽器檢查服務(wù)器送過(guò)來(lái)的證書(shū)是否是由自己信賴(lài)的 CA 中心所簽發(fā)的。如果是,就繼續(xù)執(zhí)行協(xié)議;如果不是,客戶(hù)瀏覽器就給客戶(hù)一個(gè)警告消息:警告客戶(hù)這個(gè)證書(shū)不是可以信賴(lài)的,詢(xún)問(wèn)客戶(hù)是否需要繼續(xù)。
  4. 接著客戶(hù)瀏覽器比較證書(shū)里的消息,例如域名和公鑰,與服務(wù)器剛剛發(fā)送的相關(guān)消息是否一致,如果是一致的,客戶(hù)瀏覽器認(rèn)可這個(gè)服務(wù)器的合法身份。
  5. 服務(wù)器要求客戶(hù)發(fā)送客戶(hù)自己的證書(shū)。收到后,服務(wù)器驗(yàn)證客戶(hù)的證書(shū),如果沒(méi)有通過(guò)驗(yàn)證,拒絕連接;如果通過(guò)驗(yàn)證,服務(wù)器獲得用戶(hù)的公鑰。
  6. 客戶(hù)瀏覽器告訴服務(wù)器自己所能夠支持的通訊對(duì)稱(chēng)密碼方案。
  7. 服務(wù)器從客戶(hù)發(fā)送過(guò)來(lái)的密碼方案中,選擇一種加密程度最高的密碼方案,用客戶(hù)的公鑰加過(guò)密后通知瀏覽器。
  8. 瀏覽器針對(duì)這個(gè)密碼方案,選擇一個(gè)通話(huà)密鑰,接著用服務(wù)器的公鑰加過(guò)密后發(fā)送給服務(wù)器。
  9. 服務(wù)器接收到瀏覽器送過(guò)來(lái)的消息,用自己的私鑰解密,獲得通話(huà)密鑰。
  10. 服務(wù)器、瀏覽器接下來(lái)的通訊都是用對(duì)稱(chēng)密碼方案,對(duì)稱(chēng)密鑰是加過(guò)密的。

上面所述的是雙向認(rèn)證 SSL 協(xié)議的具體通訊過(guò)程,這種情況要求服務(wù)器和用戶(hù)雙方都有證書(shū)。
單向認(rèn)證 SSL 協(xié)議不需要客戶(hù)擁有 CA 證書(shū),具體的過(guò)程相對(duì)于上面的步驟,只需將服務(wù)器端驗(yàn)證客戶(hù)證書(shū)的過(guò)程去掉,以及在協(xié)商對(duì)稱(chēng)密碼方案,對(duì)稱(chēng)通話(huà)密鑰時(shí),服務(wù)器發(fā)送給客戶(hù)的是沒(méi)有加過(guò)密的 (這并不影響 SSL 過(guò)程的安全性)密碼方案。這樣,雙方具體的通訊內(nèi)容,就是加過(guò)密的數(shù)據(jù),如果有第三方攻擊,獲得的只是加密的數(shù)據(jù),第三方要獲得有用的信息,就需要對(duì)加密 的數(shù)據(jù)進(jìn)行解密,這時(shí)候的安全就依賴(lài)于密碼方案的安全。而幸運(yùn)的是,目前所用的密碼方案,只要通訊密鑰長(zhǎng)度足夠的長(zhǎng),就足夠的安全。這也是我們強(qiáng)調(diào)要求使 用 128 位加密通訊的原因。

3. 證書(shū)格式說(shuō)明

在使用openssl自己生成證書(shū)的時(shí)候,會(huì)發(fā)現(xiàn)網(wǎng)上很多例子生成的證書(shū)格式都不同,同一篇文章里也會(huì)有很多種格式.
所以就需要了解下 不同格式有什么區(qū)別和聯(lián)系

參考博客http://blog.csdn.net/justinjing0612/article/details/7770301

參考博客http://www.cnblogs.com/lzjsky/archive/2010/11/14/1877143.html

  • der,cer文件一般是二進(jìn)制格式的,只放證書(shū),不含私鑰
  • crt文件可能是二進(jìn)制的,也可能是文本格式的,應(yīng)該以文本格式居多,功能同der/cer
  • pem文件一般是文本格式的,可以放證書(shū)或者私鑰,或者兩者都有
  • pem如果只含私鑰的話(huà),一般用.key擴(kuò)展名,而且可以有密碼保護(hù)
  • pfx,p12文件是二進(jìn)制格式,同時(shí)含私鑰和證書(shū),通常有保護(hù)密碼

4. 使用openssl生成證書(shū)

因?yàn)槲业南到y(tǒng)是Mac,這里記錄的是Mac申請(qǐng)自簽名的SSL證書(shū),其他系統(tǒng)也類(lèi)似,這里不再贅述。

  1. 桌面創(chuàng)建"SSL"文件夾,用來(lái)存放申請(qǐng)證書(shū)過(guò)程中的所有文件。
  1. 打開(kāi)"終端"并進(jìn)入到"桌面->SSL"。
  1. 管理員權(quán)限(命令:sudo su)。

這個(gè)命令超級(jí)重要!重要!重要!沒(méi)有在管理員權(quán)限下執(zhí)行下面的操作,即使你可以完成前面幾步,也會(huì)在最后一步卡住報(bào)錯(cuò)!

sudo su
  1. 創(chuàng)建rootCA.key(命令:openssl genrsa -des3 -out rootCA.key 2048)。

創(chuàng)建時(shí)需要輸入一個(gè)密碼,這個(gè)密碼要記住,下一步有用。

openssl genrsa -des3 -out rootCA.key 2048
image
image
  1. 使用rootCA.key創(chuàng)建rootCA.pem(命令:openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 -out rootCA.pem)

創(chuàng)建時(shí)需要輸入rootCA.key的密碼。
其他信息可以隨意填寫(xiě)。

openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 -out rootCA.pem
image
image
  1. 雙擊rootCA.pem證書(shū)。在鑰匙串中雙擊,并修改為"始終信任"該證書(shū)
image
image
  1. 創(chuàng)建v3.ext文件(命令:touch v3.ext)
touch v3.ext

8、編輯v3.ext文件(命令:sudo vim v3.ext)

sudo vim v3.ext

將下列文字復(fù)制粘貼進(jìn)去
注意最后一行,(DNS.1 = xxxxxxx)
這一行可以輸入域名或IP。

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage=digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName=@alt_names
[alt_names]
DNS.1 = 192.168.0.3
image
image
  1. 創(chuàng)建server.csr和server.key(命令:openssl req -new -sha256 -nodes -out server.csr -newkey rsa:2048 -keyout server.key)

這里要注意倒數(shù)第二項(xiàng),必須填自己的IP地址或域名,如果IP為動(dòng)態(tài)獲取的,建議先改成固定IP。

openssl req -new -sha256 -nodes -out server.csr -newkey rsa:2048 -keyout server.key
image
image
  1. 創(chuàng)建server.crt(命令:openssl x509 -req -in server.csr -CA [rootCA.pem路徑] -CAkey [rootCA.key路徑] -CAcreateserial -out server.crt -days 500 -sha256 -extfile v3.ext)

去掉中括號(hào),把對(duì)應(yīng)文件路徑寫(xiě)進(jìn)去。
如果你報(bào)了“Error opening CA Certificate”這個(gè)錯(cuò)誤,請(qǐng)先檢查路徑是否正確,如果正確請(qǐng)檢查是否執(zhí)行了第三步,我就卡在這卡了一晚上~
如果一切正常會(huì)讓你輸入密碼,輸入完成后就會(huì)生成server.crt??!

openssl x509 -req -in server.csr -CA [rootCA.pem路徑] -CAkey [rootCA.key路徑] -CAcreateserial -out server.crt -days 500 -sha256 -extfile v3.ext
image
image
  1. 最后查看SSL文件夾
image
image

5. Nginx配置 WSS

修改 nginx.conf配置文件

map $http_upgrade $connection_upgrade {  
    default upgrade;  
    '' close;  
}  
upstream websocket {  
    server 192.168.0.3:8888;  
}  
server {  
    listen 8888;  
    server_name test.com;
    ssl on;
    ssl_certificate /usr/local/etc/nginx/ZC_chain.crt;
    ssl_certificate_key /usr/local/etc/nginx/ZC_key.key;   
    #ssl_client_certificate /usr/local/etc/nginx/ZC_chain.crt;
    ssl_session_timeout 20m;
    ssl_verify_client off;
    location / {  
        proxy_pass http://websocket;  
        proxy_http_version 1.1;  
        proxy_set_header Upgrade $http_upgrade;  
        proxy_set_header Connection "Upgrade";  
    }  
}

128.190.0.3:8888是真正的服務(wù)端地址,nginx所在域名是test.com,代理的端口號(hào)是8888,所以前端訪(fǎng)問(wèn)的時(shí)候這樣配置:

WEBSOCKET_URL: 'wss://test.com:8888',

檢查nginx.conf正確性:

nginx -t

重新加載配置文件:

nginx -s reload

6. WebSocketServer(基于SpringBoot)

基于SpringBoot整合WebSocket實(shí)現(xiàn)前后端互推消息,該代碼可以用于ws協(xié)議以及wss協(xié)議進(jìn)行測(cè)試。

  1. 首先創(chuàng)建一個(gè)springboot項(xiàng)目,網(wǎng)上教程很多,很簡(jiǎn)單,最終的目錄結(jié)構(gòu)如下:
image
image
  1. 項(xiàng)目的pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>webSocket</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>webSocket</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

  1. application.properties中配置端口號(hào)。
server.port=8888
  1. 配置類(lèi)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {
    /**
     * ServerEndpointExporter 作用
     *
     * 這個(gè)Bean會(huì)自動(dòng)注冊(cè)使用@ServerEndpoint注解聲明的websocket endpoint
     *
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
  1. 核心類(lèi)
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@ServerEndpoint("/webSocket/{sid}")
@Component
public class WebSocketServer {
    //靜態(tài)變量,用來(lái)記錄當(dāng)前在線(xiàn)連接數(shù)。應(yīng)該把它設(shè)計(jì)成線(xiàn)程安全的。
    private static AtomicInteger onlineNum = new AtomicInteger();

    //concurrent包的線(xiàn)程安全Set,用來(lái)存放每個(gè)客戶(hù)端對(duì)應(yīng)的WebSocketServer對(duì)象。
    private static ConcurrentHashMap<String, Session> sessionPools = new ConcurrentHashMap<>();

    //發(fā)送消息
    public void sendMessage(Session session, String message) throws IOException {
        if(session != null){
            synchronized (session) {
//                System.out.println("發(fā)送數(shù)據(jù):" + message);
                session.getBasicRemote().sendText(message);
            }
        }
    }
    //給指定用戶(hù)發(fā)送信息
    public void sendInfo(String userName, String message){
        Session session = sessionPools.get(userName);
        try {
            sendMessage(session, message);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    //建立連接成功調(diào)用
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "sid") String userName){
        sessionPools.put(userName, session);
        addOnlineCount();
        System.out.println(userName + "加入webSocket!當(dāng)前人數(shù)為" + onlineNum);
        try {
            sendMessage(session, "歡迎" + userName + "加入連接!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //關(guān)閉連接時(shí)調(diào)用
    @OnClose
    public void onClose(@PathParam(value = "sid") String userName){
        sessionPools.remove(userName);
        subOnlineCount();
        System.out.println(userName + "斷開(kāi)webSocket連接!當(dāng)前人數(shù)為" + onlineNum);
    }

    //收到客戶(hù)端信息
    @OnMessage
    public void onMessage(String message) throws IOException{
        message = "客戶(hù)端:" + message + ",已收到";
        System.out.println(message);
        for (Session session: sessionPools.values()) {
            try {
                sendMessage(session, message);
            } catch(Exception e){
                e.printStackTrace();
                continue;
            }
        }
    }

    //錯(cuò)誤時(shí)調(diào)用
    @OnError
    public void onError(Session session, Throwable throwable){
        System.out.println("發(fā)生錯(cuò)誤");
        throwable.printStackTrace();
    }

    public static void addOnlineCount(){
        onlineNum.incrementAndGet();
    }

    public static void subOnlineCount() {
        onlineNum.decrementAndGet();
    }

}
  1. 在Controller中跳轉(zhuǎn)頁(yè)面
package com.example.controller;

import com.example.service.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;


@Controller
public class SocketController {

    @Autowired
    private WebSocketServer webSocketServer;

    @RequestMapping("/index")
    public String index() {
        return "index";
    }

    @GetMapping("/webSocket")
    public ModelAndView socket() {
        ModelAndView mav = new ModelAndView("/webSocket");
        //mav.addObject("userId", userId);
        return mav;
    }


}
  1. 前端代碼在webSocket.html中:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket</title>

</head>
<body>
<h3>hello socket</h3>
<p>【userId】:<div><input id="userId" name="userId" type="text" value="10"></div>
<p>【toUserId】:<div><input id="toUserId" name="toUserId" type="text" value="20"></div>
<p>【toUserId】:<div><input id="contentText" name="contentText" type="text" value="hello websocket"></div>
<p>操作:<div><a onclick="openSocket()">開(kāi)啟socket</a></div>
<p>【操作】:<div><a onclick="sendMessage()">發(fā)送消息</a></div>
</body>
<script>
    

    var socket;
    function openSocket() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的瀏覽器不支持WebSocket");
        }else{
            console.log("您的瀏覽器支持WebSocket");
            //實(shí)現(xiàn)化WebSocket對(duì)象,指定要連接的服務(wù)器地址與端口  建立連接
            var userId = document.getElementById('userId').value;
            var socketUrl="wss://192.168.0.3:8888/webSocket/"+userId;
            console.log(socketUrl);
            if(socket!=null){
                socket.close();
                socket=null;
            }
            socket = new WebSocket(socketUrl);
            //打開(kāi)事件
            socket.onopen = function() {
                console.log("websocket已打開(kāi)");
                //socket.send("這是來(lái)自客戶(hù)端的消息" + location.href + new Date());
            };
            //獲得消息事件
            socket.onmessage = function(msg) {
                var serverMsg = "收到服務(wù)端信息:" + msg.data;
                console.log(serverMsg);
                //發(fā)現(xiàn)消息進(jìn)入    開(kāi)始處理前端觸發(fā)邏輯
            };
            //關(guān)閉事件
            socket.onclose = function() {
                console.log("websocket已關(guān)閉");
            };
            //發(fā)生了錯(cuò)誤事件
            socket.onerror = function() {
                console.log("websocket發(fā)生了錯(cuò)誤");
            }
        }
    }
    function sendMessage() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的瀏覽器不支持WebSocket");
        }else {
            // console.log("您的瀏覽器支持WebSocket");
            var toUserId = document.getElementById('toUserId').value;
            var contentText = document.getElementById('contentText').value;
            var msg = '{"toUserId":"'+toUserId+'","contentText":"'+contentText+'"}';
            console.log(msg);
            socket.send(msg);
        }
    }

    </script>
</html>

7. WSS + WebSocketClient

  1. maven依賴(lài)

備注:maven倉(cāng)庫(kù)官方(https://mvnrepository.com/

<!--        WebSocket start-->
        <dependency>
            <groupId>org.java-websocket</groupId>
            <artifactId>Java-WebSocket</artifactId>
            <version>1.3.0</version>
        </dependency>
        <!--        WebSocket end-->

這個(gè)ws客戶(hù)端對(duì)wss支持不好,源碼中的wss client 還要使用證書(shū)。可以修改為不驗(yàn)證證書(shū),具體看工具類(lèi)代碼。

  1. 工具類(lèi)
package com.example.wss;
import java.net.URI;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Map;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.java_websocket.client.DefaultSSLWebSocketClientFactory;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft;
/**
 * <p>
 * WSS工具類(lèi)
 * </p>
 *
 * @author: hejianhui
 * @create: 2020-08-23 00:27
 * @see MyWssUtil
 * @since JDK1.8
 */
abstract class MyWssUtil extends WebSocketClient {

    public MyWssUtil(URI serverURI) {
        super(serverURI);
        if (serverURI.toString().contains("wss://"))
            trustAllHosts(this);
    }

    public MyWssUtil(URI serverURI, Draft draft) {
        super(serverURI, draft);
        if (serverURI.toString().contains("wss://"))
            trustAllHosts(this);
    }

    public MyWssUtil(URI serverURI, Draft draft, Map<String, String> headers, int connecttimeout) {
        super(serverURI, draft, headers, connecttimeout);
        if (serverURI.toString().contains("wss://"))
            trustAllHosts(this);
    }

    final static HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() {
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    };


    static void trustAllHosts(MyWssUtil appClient) {
        System.out.println("start...");
        TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                return new java.security.cert.X509Certificate[]{};
            }

            @Override
            public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
                // TODO Auto-generated method stub

            }

            @Override
            public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
                // TODO Auto-generated method stub

            }
        }};

        try {
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            appClient.setWebSocketFactory(new DefaultSSLWebSocketClientFactory(sc));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  1. 使用工具進(jìn)行wss協(xié)議的接口請(qǐng)求
package com.example.wss;

import org.java_websocket.WebSocket;
import org.java_websocket.drafts.Draft;
import org.java_websocket.drafts.Draft_17;
import org.java_websocket.handshake.ServerHandshake;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * 進(jìn)行wss協(xié)議的接口請(qǐng)求測(cè)試
 * </p>
 *
 * @author: hejianhui
 * @create: 2020-08-23 00:30
 * @see WssTest
 * @since JDK1.8
 */
public class WssTest {
    public static List<String> result = new ArrayList<>();

    public static String initmsg = "{'lat':'118.817891','lng':'31.931724','speed':'0','distance':'0'}";

    public static void main(String[] args) throws URISyntaxException {
        Map<String, String> headers = new HashMap<>();
        // 根據(jù)服務(wù)端具體進(jìn)行配置
        // headers.put("Sec-WebSocket-Extensions", "permessage-deflate; client_max_window_bits");
        // headers.put("Sec-WebSocket-Key", "");
        // headers.put("Sec-WebSocket-Protocol", "x-access-token, ");
        // headers.put("Sec-WebSocket-Version", "13");
        // 以下請(qǐng)求頭可以不用傳
        // headers.put("Connection", "Upgrade");
        // headers.put("Upgrade", "websocket");
        // headers.put("Accept-Encoding", "gzip, deflate, br");
        // headers.put("Accept-Language", "zh-CN,zh;q=0.9");
        // headers.put("Cache-Control", "no-cache");
        // headers.put("Host", "test.com");
        // headers.put("Origin", "https://test.com");
        // headers.put("Pragma", "no-cache");
        // headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36");

        Draft draft = new Draft_17();
        draft.setParseMode(WebSocket.Role.CLIENT);
        new MyWssUtil(new URI("wss://****************填寫(xiě)自己的url******************")
                , draft
                , headers
                , 10
        ) {

            @Override
            public void onClose(int arg0, String arg1, boolean arg2) {
                System.out.println(String.format("onClose:【%s】【%s】【%s】", arg0, arg1, arg2));
            }

            @Override
            public void onError(Exception arg0) {
                System.out.println(String.format("onError:%s", arg0));

            }

            @Override
            public void onMessage(String arg0) {
                if (!arg0.equals("pong")) {
                    result.add(arg0);
                    System.out.println(String.format("onMessage:%s", arg0));
                }
                this.send(arg0);
            }

            @Override
            public void onOpen(ServerHandshake arg0) {
                System.out.println(String.format("onOpen:%s", arg0));
                this.send(initmsg);
            }
        }.connect();
    }
}

完整代碼地址:https://github.com/org-hejianhui/websocket

部分圖片來(lái)源于網(wǎng)絡(luò),版權(quán)歸原作者,侵刪。

以上謝謝大家,求贊求贊求贊!

?? 大佬們隨手關(guān)注下我的wx公眾號(hào)【一角錢(qián)小助手】和 掘金專(zhuān)欄【一角錢(qián)】 更多干貨等你來(lái)~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容