前言
接上版,本次版本做了如下優化:
1、新增同意、拒絕添加好友后做線上提示;
2、新增好友分組,使用工具生成后台API,新增好友分組功能,主要功能有:添加分組、重命名分組名稱、刪除分組
3、新增好友管理,主要功能:刪除好友(下個版本再實現功能)、移動好友至其他分組
4、添加好友時有驗證信息、好友備注、好友分組
5、AIP接口、WebSocket通道的前后端交互采用AES與RSA混合加密,防抓包監聽,加、解密操作后並不影響業務,AIP接口、WebSocket通道的前后端交互正常
優化細節
1、新增同意、拒絕添加好友后做線上提示;
2、新增好友分組,使用工具生成后台API,新增好友分組功能,主要功能有:添加分組、重命名分組名稱、刪除分組
沒有分組的默認在列表前面追加,分組名稱后面展示對應好友數以及在線好友數量
添加分組、重命名分組名稱、刪除分組
3、新增好友管理,主要功能:刪除好友(下個版本再實現功能)、移動好友至其他分組
移動好友至其他分組
4、添加好友時有驗證信息、好友備注、好友分組
先登錄兩個還不是好友的人,各種新增一個好友分組
A向B發起好友申請
同意好友申請
拒絕好友申請
5、AIP接口、WebSocket通道的前后端交互采用AES與RSA混合加密,防抓包監聽,加、解密操作后並不影響業務,AIP接口、WebSocket通道的前后端交互正常
API交互,關於前后端API安全交互,我前段時間實現了一套AES與RSA混合加密,詳情請戳:前后端API交互數據加密——AES與RSA混合加密完整實例
WebSocket聊天,webSocket的加、解密與AIP的加、解密原理一樣,發送前加密、收到數據后解密再交給業務處理,有個地方要注意的是,我們在進行消息轉發時,要用的是接收方的前端公鑰進行加密
建立WebSocket連接時,將當前用戶的前端公鑰發送到后端,后端進行Map保存(只貼出關鍵代碼)
//因為是url的方式傳值,公鑰中的/需要進行轉換一下,傳到后端再轉回來(PS:因為生成的公鑰里是不存在","的,所以這里轉成逗號) websocket = new WebSocket("ws://localhost:10086/websocket/" + userId + "/" + window.jsPublicKey.replace(/\//g,","));
/** * 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的AES與RSA混合加密,單獨寫一篇博客