1. 程式人生 > >WebSocket資料加密——AES與RSA混合加密

WebSocket資料加密——AES與RSA混合加密

  前言

  之前在寫“一套簡單的web即時通訊”,寫到第三版的時候沒什麼思路,正好微信公眾號看到一篇講API互動加密,於是就自己搞了一套AES與RSA混合加密,無意中產生應用在WebSocket想法,好在思路都差不多,稍微改動一下就能實現,特意寫這篇部落格記錄下來

  WebSocket是HTML5 開始提供的一種瀏覽器與伺服器進行全雙工通訊的網路技術,屬於應用層協議。它基於 TCP 傳輸協議,並複用 HTTP 的握手通道。

  1、建立連線,客戶端通過 HTTP 請求與服務端協商升級協議。協議升級完成後,後續的資料交換則遵照 WebSocket 的協議。

  2、資料交換,建立連線後,後續的操作通過TCP協議以資料幀的格式傳輸交換。

  通過TCP進行資料交換,不像http請求,websocket資料交換在瀏覽器上使用開發者工具(F12)是看不到資料,但使用抓包軟體仍然可以獲取到這些TCP傳輸資料,例如Charles和Fiddler等,以明文的方式直接傳輸很容易被第三方監聽,因此,我覺得是有必要的

  前面我們實現了一套AES與RSA混合加密(詳情請戳:前後端API互動資料加密——AES與RSA混合加密完整例項),我們現在用它實現一下WebSocket資料加密

 

  思路、程式碼

  工具類我們直接用之前API加密那一套就行,操作也與之前的API加密類似,傳送前加密、收到資料後解密再交給業務處理,有個地方要注意的是,我們在進行訊息轉發時,要用的是接收方的前端公鑰進行加密

  按照我們目前的規則,專案啟動後生成後端金鑰對,訪問登入頁面時響應後端公鑰給前端,前端儲存到後端RSA公鑰並存到sessionStorage,每個頁面都引入頭部head.html(使用thymeleaf的<script th:replace="head::static"></script>),在head裡面獲取前端RSA公鑰密碼、AES的key,並放到window,這些都跟之前的一樣,不需要改變,需要改動的是下面這些:

  建立WebSocket連線時,將當前使用者的前端公鑰傳送到後端,後端進行Map儲存

//判斷當前瀏覽器是否支援WebSocket
if ('WebSocket' in window) {
    //因為是url的方式傳值,公鑰中的/需要進行轉換一下,傳到後端再轉回來(PS:因為生成的公鑰裡是不存在","的,所以這裡轉成逗號)
    websocket = new WebSocket("ws://localhost:10086/websocket/" + userId + "/" + window.jsPublicKey.replace(/\//g,","));
} else {
    console.error("不支援WebSocket");
}
/**
 * WebSocket服務
 */
@Component
@ServerEndpoint(value = "/websocket/{userId}/{publicKey}", configurator = MyEndpointConfigure.class)
public class WebSocketServer {
    //省略其他程式碼  

    /**
     * 登入使用者的前端公鑰Map集合(其實應該放在Redis)
     */
    private static Map<Session, String> loginPublicKeyList = new HashMap<Session, String>();

    /**
     * 連線建立成功呼叫的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId, @PathParam("publicKey") String publicKey) {
        //省略其他程式碼

        //設定前端公鑰,因為是url的方式傳值,公鑰中的/需要進行轉換一下,傳到後端再轉回來,然後將每個使用者的前端公鑰儲存起來
        loginPublicKeyList.put(session,publicKey.replaceAll(",", "/"));

    }
}

  前端傳送前加密

//傳送訊息
function send(but) {
    //業務操作不變,省略程式碼

    //先加密
    let aesKey = aesUtil.genKey();
    let data = {
        data: aesUtil.encrypt(JSON.stringify({
            "type": "1",
            "toUser": {"userId": toUserId},
            "fromUser": {"userId": fromUserId},
            "message": message,
            "date": nowTime
        }), aesKey),//AES加密後的資料
        aesKey: rsaUtil.encrypt(aesKey, sessionStorage.getItem('javaPublicKey')),//後端RSA公鑰加密後的AES的key
        publicKey: window.jsPublicKey//前端公鑰
    };
    websocket.send(JSON.stringify(data));

    //業務操作不變,省略程式碼
}

  後端收到後先解密

/**
     * 伺服器接收到客戶端訊息時呼叫的方法
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            //jackson
            ObjectMapper mapper = new ObjectMapper();
            //jackson 序列化和反序列化 date處理
            mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            //JSON字串轉 HashMap
            HashMap map = mapper.readValue(message, HashMap.class);

            //先解密
            String data = (String) map.get("data");
            String aesKey = (String) map.get("aesKey");

            //後端私鑰解密的到AES的key
            byte[] plaintext = RsaUtil.decryptByPrivateKey(Base64.decodeBase64(aesKey), RsaUtil.getPrivateKey());
            aesKey = new String(plaintext);

            //RSA解密出來字串多一對雙引號
            aesKey = aesKey.substring(1, aesKey.length() - 1);

            //AES解密得到明文data資料
            String decrypt = AesUtil.decrypt(data, aesKey);

            //JSON字串轉 HashMap
            HashMap hashMap = mapper.readValue(decrypt, HashMap.class);

            //得到hashMap,下面的業務操作跟前面的一樣,這裡就不貼出來了

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

  後端傳送之前先加密,這裡要用訊息接收方的前端公鑰進行加密

/**
     * 封裝一個send方法,傳送訊息到前端
     */
    private void send(Session session, String message) {
        try {
            //傳送前加密
            //每次響應之前隨機獲取AES的key,加密data資料
            String key = AesUtil.getKey();
            String data = AesUtil.encrypt(message, key);
            //用前端的公鑰來解密AES的key,並轉成Base64,注意:這裡需要用接收方的前端公鑰進行加密,從loginPublicKeyList集合獲取
            String aesKey = Base64.encodeBase64String(RsaUtil.encryptByPublicKey(key.getBytes(), loginPublicKeyList.get(session)));
            //傳送過去的是AES加密後的data,跟RSA加密後的aesKey
            session.getBasicRemote().sendText("{\"data\":\"" + data + "\",\"aesKey\":\"" + aesKey + "\"}");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

  前端收到訊息後先解密

//接收到訊息的回撥方法
websocket.onmessage = function (event) {
    let data = eval("(" + event.data + ")");
    //先解密
    let msgObj = aesUtil.decrypt(data.data, rsaUtil.decrypt(data.aesKey, window.jsPrivateKey));
    
    //業務操作不變,省略程式碼

};

 

  效果演示  

  按照程式流程:

  前端加密前、加密後

 

  後端解密前、解密後

  後端加密前、加密後

  前端解密前、解密後

 

 

 

 

   完整頁面演示:

  好友上線線上系統通知沒有問題

  聊天沒有問題

 

  後記

  WebSocket的加密就先記錄到這裡,其他的後面再