多方合作時,系統(tǒng)間的交互是怎么做的?
作者:不若為止
我們最近做了很多項目,有些我們是總負責的,有些是合作的。這些項目涉及的系統(tǒng)各種各樣,但基本上沒有一家公司會主動去做『開放平臺』。這是因為投入產(chǎn)出比較低,項目一旦完成就結(jié)束了,而且標書里也沒有要求做開放平臺。雖然這些項目都是業(yè)務系統(tǒng),沒有通用能力好開放的,但在同一個項目中,總是有些東西需要打通,還是需要一種輕量、安全的交互方式。

場景分類
(一)單點登錄
單點登錄是一種方便的登錄方式,它可以應用在各種場景中,比如門戶網(wǎng)站和小程序跳轉(zhuǎn)。用戶只需要在登錄門戶網(wǎng)站時輸入用戶名和密碼,就可以輕松訪問其他相關(guān)子系統(tǒng),無需反復輸入登錄信息。這不僅方便了用戶,還幫助了IT管理人員更好地管理系統(tǒng)。
以百度為例:

?這就是一個典型的單點登錄案例,那么我們怎么實現(xiàn)單點登錄功能呢?
思路分析
從『系統(tǒng)A門戶頁』點擊導航進入『系統(tǒng)B』,用戶信息是怎么同步的呢?把信息放在跳轉(zhuǎn)鏈接上傳給系統(tǒng)A肯定不合適,這相當于泄漏了用戶信息,方案不可行。我們的做法是:
用戶輸入賬號密碼進入系統(tǒng)A的門戶頁;
用戶點擊跳轉(zhuǎn)系統(tǒng)B導航后,系統(tǒng)A會生成一個當前用戶唯一標識,一般是一串唯一的字符串,我們將它成為臨時授權(quán)碼,取名為userToken;
這個標識會當成一個參數(shù)拼接在系統(tǒng)B的跳轉(zhuǎn)鏈接上,比如:https://systemB.com/index?userToken=xxxx;
系統(tǒng)A提供一個根據(jù)userToken查詢當前用戶的接口,比如:https://systemA.com/queryUserByToken?userToken=xxx;
進入系統(tǒng)B的首頁之后,系統(tǒng)B調(diào)用系統(tǒng)A的queryUserByToken接口獲取信息。
為了安全起見,這個userToken一般都是有時限性的,過了1個小時就不能用了,而且只能用一次,用完就廢棄掉。
我畫個時序圖解釋一下這個邏輯

?
(二)接口調(diào)用
接口調(diào)用方式一般有兩種:http接口和rpc接口。
1. http接口
我們都知道http接口是什么,也能夠輕易地使用Java調(diào)用Get、Post請求。然而,我們需要考慮http接口的數(shù)據(jù)安全問題。當我們在瀏覽器或者postman工具中調(diào)用接口時,數(shù)據(jù)會以明文形式返回,不需要認證也不需要解密,這顯然是不太安全的。我在開發(fā)過程中,經(jīng)常會遇到合作方提供的接口直接以明文返回數(shù)據(jù),甚至包括敏感信息如手機號碼等。雖然這種方式方便快捷,但總體來說并不太安全和可靠。
想要實現(xiàn)一個相對安全的http接口一般有兩種辦法:
(1)調(diào)用方需要進行認證并獲取token,調(diào)用接口時需將token放置于請求頭或Cookie中。處理方通過過濾器檢查token的合法性;

?
(2)處理方應生成并提供給調(diào)用方一個唯一的appId和對應的appSecret。調(diào)用方使用這個appId去調(diào)用接口,處理方使用appId和appSecret對數(shù)據(jù)進行加密。調(diào)用方獲取到數(shù)據(jù)后,使用同樣的appId和appSecret進行解密。
?

?
使用請求頭或Cookie的方式將token放置于請求中的優(yōu)點是安全性高,因為token不易被竊取或篡改。而使用appId和appSecret進行加密和解密的方式的優(yōu)點是方便性高,因為appId和appSecret可以在接口文檔或其他途徑中公開,調(diào)用方只需要使用這些信息即可進行加解密操作,無需每次都進行認證獲取token。
因此,兩種方式的選擇應根據(jù)具體情況而定,通常安全性較為重要的場景可以選擇使用token方式,而方便性較為重要的場景可以選擇使用appId和appSecret方式。
這里我給大家提供一份可用的代碼工具類,親測可用。
EncryptUtil.java

import java.io.IOException;
import java.security.Security;
import java.text.ParseException;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
/**
* 需要依賴
*
* <dependency>
* <groupId>com.alibaba</groupId>
* <artifactId>fastjson</artifactId>
* <version>1.2.83</version>
* </dependency>
*
* <dependency>
* <groupId>org.bouncycastle</groupId>
* <artifactId>bcprov-jdk15on</artifactId>
* <version>1.56</version>
* </dependency>
*
* <dependency>
* <groupId>commons-codec</groupId>
* <artifactId>commons-codec</artifactId>
* <version>1.14</version>
* </dependency>
*/
@Slf4j
public class EncryptUtil {
? ?static {
? ? ? ?Security.addProvider(new BouncyCastleProvider());
? ?}
? ?private static final String CipherMode = "AES/CBC/PKCS7Padding";
? ?private static final String EncryptAlg = "AES";
? ?private static final String Encode = "UTF-8";
? ?/**
? ? * 加密隨機鹽
? ? */
? ?private static final String AESIV = "ff465fdecc764337";
? ?/**
? ? * 加密:有向量16位,結(jié)果轉(zhuǎn)base64
? ? *
? ? * @param context
? ? * @return
? ? */
? ?public static String encrypt(String context, String sk) {
? ? ? ?try {
? ? ? ? ? ?// 下面這行在進行PKCS7Padding加密時必須加上,否則報錯
? ? ? ? ? ?Security.addProvider(new BouncyCastleProvider());
? ? ? ? ? ?byte[] content = context.getBytes(Encode);
? ? ? ? ? ?Cipher cipher = Cipher.getInstance(CipherMode);
? ? ? ? ? ?cipher.init(
? ? ? ? ? ? ? ?Cipher.ENCRYPT_MODE,
? ? ? ? ? ? ? ?new SecretKeySpec(sk.getBytes(Encode), EncryptAlg),
? ? ? ? ? ? ? ?new IvParameterSpec(AESIV.getBytes(Encode)));
? ? ? ? ? ?byte[] data = cipher.doFinal(content);
? ? ? ? ? ?String result = Base64.encodeBase64String(data);
? ? ? ? ? ?return result;
? ? ? ?} catch (Exception e) {
? ? ? ? ? ?e.printStackTrace();
? ? ? ?}
? ? ? ?return null;
? ?}
? ?/**
? ? * 解密
? ? *
? ? * @param context
? ? * @return
? ? */
? ?public static String decrypt(String context, String sk) {
? ? ? ?try {
? ? ? ? ? ?byte[] data = Base64.decodeBase64(context);
? ? ? ? ? ?Cipher cipher = Cipher.getInstance(CipherMode);
? ? ? ? ? ?cipher.init(
? ? ? ? ? ? ? ?Cipher.DECRYPT_MODE,
? ? ? ? ? ? ? ?new SecretKeySpec(sk.getBytes(Encode), EncryptAlg),
? ? ? ? ? ? ? ?new IvParameterSpec(AESIV.getBytes(Encode)));
? ? ? ? ? ?byte[] content = cipher.doFinal(data);
? ? ? ? ? ?String result = new String(content, Encode);
? ? ? ? ? ?return result;
? ? ? ?} catch (Exception e) {
? ? ? ? ? ?e.printStackTrace();
? ? ? ?}
? ? ? ?return null;
? ?}
? ?public static String sendPost(String url, JSONObject jsonObject, String encoding)
? ? ? ?throws ParseException, IOException {
? ? ? ?String body = "";
? ? ? ?//創(chuàng)建httpclient對象
? ? ? ?CloseableHttpClient client = HttpClients.createDefault();
? ? ? ?//創(chuàng)建post方式請求對象
? ? ? ?HttpPost httpPost = new HttpPost(url);
? ? ? ?//裝填參數(shù)
? ? ? ?StringEntity s = new StringEntity(jsonObject.toString(), "utf-8");
? ? ? ?s.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE,
? ? ? ? ? ?"application/json"));
? ? ? ?//設置參數(shù)到請求對象中
? ? ? ?httpPost.setEntity(s);
? ? ? ?log.info("請求地址:" + url);
? ? ? ?// ? ? ? ?System.out.println("請求參數(shù):"+nvps.toString());
? ? ? ?//設置header信息
? ? ? ?//指定報文頭【Content-type】、【User-Agent】
? ? ? ?// ? ? ? ?httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
? ? ? ?httpPost.setHeader("Content-type", "application/json");
? ? ? ?httpPost.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
? ? ? ?//執(zhí)行請求操作,并拿到結(jié)果(同步阻塞)
? ? ? ?CloseableHttpResponse response = client.execute(httpPost);
? ? ? ?//獲取結(jié)果實體
? ? ? ?HttpEntity entity = response.getEntity();
? ? ? ?if (entity != null) {
? ? ? ? ? ?//按指定編碼轉(zhuǎn)換結(jié)果實體為String類型
? ? ? ? ? ?body = EntityUtils.toString(entity, encoding);
? ? ? ?}
? ? ? ?EntityUtils.consume(entity);
? ? ? ?//釋放鏈接
? ? ? ?response.close();
? ? ? ?return body;
? ?}
? ?public static void main(String[] args) {
? ? ? ?String appId = "appId";
? ? ? ?//AES算法支持的密鑰長度有128位、192位和256位,其中128位密鑰是最常用的。
? ? ? ?//因此,如果使用AES算法進行加密和解密,必須確保密鑰長度是128位、192位或256位。
? ? ? ?//如果使用的是AES-128算法,則密鑰長度應該是128位,也就是16個字節(jié);
? ? ? ?//如果使用的是AES-192算法,則密鑰長度應該是192位,也就是24個字節(jié);
? ? ? ?//如果使用的是AES-256算法,則密鑰長度應該是256位,也就是32個字節(jié)
? ? ? ?String appKey = UUIDUtil.generateString(32);
? ? ? ?//參數(shù)加密
? ? ? ?JSONObject jsonObject = new JSONObject();
? ? ? ?jsonObject.put("appId", appId);
? ? ? ?jsonObject.put("appKey", appKey);
? ? ? ?jsonObject.put("data", "我是內(nèi)容");
? ? ? ?String encrypt = EncryptUtil.encrypt(jsonObject.toJSONString(), appKey);
? ? ? ?System.out.println("加密后內(nèi)容=" + encrypt);
? ? ? ?//參數(shù)界面
? ? ? ?System.out.println("解密后內(nèi)容=" + EncryptUtil.decrypt(encrypt, appKey));
? ?}
}

UUIDUtil.java

import java.text.SimpleDateFormat;import java.util.Date;import java.util.Random;import java.util.UUID;public class UUIDUtil { ?public static final String allChar = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; ?public static final String letterChar = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; ?public static final String numberChar = "0123456789"; ?public static String[] chars = ? ? ?new String[] { ? ? ? ?"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", ? ? ? ?"I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ? ? ?}; ?/** 用于生成8位唯一標識字符串 */ ?public static String generateShortUuid() { ? ?StringBuffer shortBuffer = new StringBuffer(); ? ?String uuid = UUID.randomUUID().toString().replace("-", ""); ? ?for (int i = 0; i < 8; i++) { ? ? ?String str = uuid.substring(i * 4, i * 4 + 4); ? ? ?int x = Integer.parseInt(str, 16); ? ? ?shortBuffer.append(chars[x % 36]); ? ?} ? ?return shortBuffer.toString(); ?} ?/** ? * 生成指定長度純數(shù)字唯一標識字符串 ? * ? * @param length ? * @return ? */ ?public static String generatePureNumberUuid(int length) { ? ?StringBuffer shortBuffer = new StringBuffer(); ? ?Random random = new Random(); ? ?for (int i = 0; i < length; i++) { ? ? ?shortBuffer.append(numberChar.charAt(random.nextInt(10))); ? ?} ? ?return shortBuffer.toString(); ?} ?/** ? * 由大小寫字母、數(shù)字組成的隨機字符串 ? * ? * @param length ? * @return ? */ ?public static String generateString(int length) { ? ?StringBuffer sb = new StringBuffer(); ? ?Random random = new Random(); ? ?for (int i = 0; i < length; i++) { ? ? ?sb.append(allChar.charAt(random.nextInt(allChar.length()))); ? ?} ? ?return sb.toString(); ?} ?/** ? * 由大小寫字母組成的隨機字符串 ? * ? * @param length ? * @return ? */ ?public static String generateMixString(int length) { ? ?StringBuffer sb = new StringBuffer(); ? ?Random random = new Random(); ? ?for (int i = 0; i < length; i++) { ? ? ?sb.append(letterChar.charAt(random.nextInt(letterChar.length()))); ? ?} ? ?return sb.toString(); ?} ?/** ? * 由小字字母組成的隨機字符串 ? * ? * @param length ? * @return ? */ ?public static String generateLowerString(int length) { ? ?return generateMixString(length).toLowerCase(); ?} ?/** ? * 由大寫字母組成的隨機字符串 ? * ? * @param length ? * @return ? */ ?public static String generateUpperString(int length) { ? ?return generateMixString(length).toUpperCase(); ?} ?/** ? * 產(chǎn)生指字個數(shù)的0組成的字符串 ? * ? * @param length ? * @return ? */ ?public static String generateZeroString(int length) { ? ?StringBuffer sb = new StringBuffer(); ? ?for (int i = 0; i < length; i++) { ? ? ?sb.append('0'); ? ?} ? ?return sb.toString(); ?} ?/** ? * 將數(shù)字轉(zhuǎn)化成指字長度的字符串 ? * ? * @param num ? * @param fixdlenth ? * @return ? */ ?public static String toFixdLengthString(long num, int fixdlenth) { ? ?StringBuffer sb = new StringBuffer(); ? ?String strNum = String.valueOf(num); ? ?if (fixdlenth - strNum.length() >= 0) { ? ? ?sb.append(generateZeroString(fixdlenth - strNum.length())); ? ?} else { ? ? ?throw new RuntimeException("將數(shù)字" + num + "轉(zhuǎn)化為長度為" + fixdlenth + "的字符串發(fā)生異常!"); ? ?} ? ?sb.append(strNum); ? ?return sb.toString(); ?} ?/** ? * 將數(shù)字轉(zhuǎn)化成指字長度的字符串 ? * ? * @param num ? * @param fixdlenth ? * @return ? */ ?public static String toFixdLengthString(int num, int fixdlenth) { ? ?StringBuffer sb = new StringBuffer(); ? ?String strNum = String.valueOf(num); ? ?if (fixdlenth - strNum.length() >= 0) { ? ? ?sb.append(generateZeroString(fixdlenth - strNum.length())); ? ?} else { ? ? ?throw new RuntimeException("將數(shù)字" + num + "轉(zhuǎn)化為長度為" + fixdlenth + "的字符串發(fā)生異常!"); ? ?} ? ?sb.append(strNum); ? ?return sb.toString(); ?} ?// 生成訂單編號,時間戳+后8位隨機字符串 ?public static String getOrderNo() { ? ?String orderNo = ""; ? ?String sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()); ? ?orderNo = sdf + generateShortUuid(); ? ?return orderNo; ?} ?/** ? * 這個方法只支持最大長度為32的隨機字符串,如要支持更大長度的,可以適當修改此方法,如前面補、后面補,或者多個uuid相連接 ? * ? * @param length ? * @return ? */ ?private static String toFixedLengthStringByUUID(int length) { ? ?// 也可以通過UUID來隨機生成 ? ?UUID uuid = UUID.randomUUID(); ? ?return uuid.toString().replace("-", "").substring(0, length); ?} ?// 生成訂單編號,時間戳+后8位隨機字符串 ?public static String getBarCode() { ? ?String barCode = ""; ? ?String sdf = new SimpleDateFormat("yyyyMMdd").format(new Date()); ? ?barCode = sdf + generatePureNumberUuid(4); ? ?return barCode; ?} }

2. RPC接口
RPC(Remote Procedure Call)遠程過程調(diào)用是一種進程間通信的方式,可以讓不同的系統(tǒng)之間通過網(wǎng)絡進行通信和交互。然而,由于RPC接口需要事先定義好接口的參數(shù)、返回值、異常等,并且多方合作的開發(fā)框架要大致一樣,因此其應用場景比較受限制。此外,不同系統(tǒng)之間的RPC接口需要保持兼容性,否則可能會出現(xiàn)接口不匹配、數(shù)據(jù)傳輸錯誤等問題。因此,在使用RPC接口時,需要進行充分考慮和設計,以確保接口的正確性和可靠性。雖然RPC接口的應用場景有限,但在特定的場景下,RPC接口可以提供高效、可靠的通信方式,如分布式架構(gòu)中系統(tǒng)間的服務調(diào)用。
我在工作中只遇到過一次RPC調(diào)用的情況。當時,我與公司的不同部門合作,我們使用了同一套框架,他們提供的是RPC接口,我只需引入他們的jar包就能輕松調(diào)用他們的服務。不過,除了公司內(nèi)部,我很少遇到其他機構(gòu)或公司使用RPC調(diào)用的方式。通常,大多數(shù)外部接口服務都是通過HTTP接口實現(xiàn)的。
(三)中間件交互
這里我引用一下ChatGPT的回答:

?
我遇到的情況:有一次,A方需要主動將數(shù)據(jù)推送給B方,于是提出了用消息隊列的方案,一聽兩方都覺得既解耦又方便,于是開始行動。A方在自己的服務器上部署了消息隊列,但沒想到,各方的服務器環(huán)境是隔離的,網(wǎng)絡不通,B方根本無法連接到A方的消息隊列。他們于是找到了私有云的運維人員,問他們能不能做開放端口、IP加白等一大堆操作,但不知道啥原因就是不行。最后他們只好改為B方提供一個Http接口,A方主動調(diào)用接口把數(shù)據(jù)送過去才得以解決。。。
總結(jié)一下
在多系統(tǒng)合作的場景中,系統(tǒng)間的交互是非常關(guān)鍵的。交互協(xié)議的一致性、數(shù)據(jù)格式的一致性、安全性保障、錯誤處理機制、交互頻率、監(jiān)控和日志記錄等方面,都需要特別注意,以確保系統(tǒng)間的交互穩(wěn)定和可靠。
交互協(xié)議的一致性是系統(tǒng)間進行數(shù)據(jù)傳輸?shù)幕A(chǔ),需要明確定義請求和響應報文格式、數(shù)據(jù)類型、處理規(guī)則等。數(shù)據(jù)格式的一致性也非常重要,需要確定數(shù)據(jù)交換的格式和編碼方式,避免由于格式不一致而導致的數(shù)據(jù)解析異常。
安全性保障是防止系統(tǒng)中出現(xiàn)非法訪問和數(shù)據(jù)泄漏的重要手段,需要采用各種安全措施來保障系統(tǒng)的安全性。
錯誤處理機制需要考慮系統(tǒng)中可能出現(xiàn)的各種異常情況,并對不同的異常情況進行分類處理,確保信息及時反饋給用戶。
交互頻率需要根據(jù)實際情況來制定,避免頻繁的調(diào)用造成系統(tǒng)壓力過大。
監(jiān)控和日志記錄需要對系統(tǒng)進行實時監(jiān)控,及時發(fā)現(xiàn)和處理問題,并記錄日志以便進行排查和分析。
?
綜上所述,在多方合作時,需要全面考慮系統(tǒng)間的交互問題,以確保系統(tǒng)間的交互穩(wěn)定可靠,保障合作的順利進行。
最后提醒大家一下,多方合作少不了開會對齊,在溝通的時候還是要耐心和主動一些,都是干活的應該互幫互助才對,齊心協(xié)力才能少加班嘛!