多方合作時,系統間的互動是怎麼做的?

2023-06-08 21:00:24

大家好!我是sum墨,一個一線的底層碼農,平時喜歡研究和思考一些技術相關的問題並整理成文,限於本人水平,如果文章和程式碼有表述不當之處,還請不吝賜教。

以下是正文!

文章背景

我們最近做了很多專案,有些我們是總負責的,有些是合作的。這些專案涉及的系統各種各樣,但基本上沒有一家公司會主動去做『開放平臺』。這是因為投入產出比較低,專案一旦完成就結束了,而且標書裡也沒有要求做開放平臺。雖然這些專案都是業務系統,沒有通用能力好開放的,但在同一個專案中,總是有些東西需要打通,還是需要一種輕量、安全的互動方式。

場景分類

(一)單點登入

單點登入是一種方便的登入方式,它可以應用在各種場景中,比如入口網站和小程式跳轉。使用者只需要在登入入口網站時輸入使用者名稱和密碼,就可以輕鬆存取其他相關子系統,無需反覆輸入登入資訊。這不僅方便了使用者,還幫助了IT管理人員更好地管理系統。

以百度為例:

 這就是一個典型的單點登入案例,那麼我們怎麼實現單點登入功能呢?

思路分析

從『系統A門戶頁』點選導航進入『系統B』,使用者資訊是怎麼同步的呢?把資訊放在跳轉連結上傳給系統A肯定不合適,這相當於洩漏了使用者資訊,方案不可行。我們的做法是:

  1. 使用者輸入賬號密碼進入系統A的門戶頁;
  2. 使用者點選跳轉系統B導航後,系統A會生成一個當前使用者唯一標識,一般是一串唯一的字串,我們將它成為臨時授權碼,取名為userToken;
  3. 這個標識會當成一個引數拼接在系統B的跳轉連結上,比如:https://systemB.com/index?userToken=xxxx;
  4. 系統A提供一個根據userToken查詢當前使用者的介面,比如:https://systemA.com/queryUserByToken?userToken=xxx;
  5. 進入系統B的首頁之後,系統B呼叫系統A的queryUserByToken介面獲取資訊。

為了安全起見,這個userToken一般都是有時限性的,過了1個小時就不能用了,而且只能用一次,用完就廢棄掉。

我畫個時序圖解釋一下這個邏輯

 

(二)介面呼叫

介面呼叫方式一般有兩種:http介面和rpc介面。

1. http介面

我們都知道http介面是什麼,也能夠輕易地使用Java呼叫Get、Post請求。然而,我們需要考慮http介面的資料安全問題。當我們在瀏覽器或者postman工具中呼叫介面時,資料會以明文形式返回,不需要認證也不需要解密,這顯然是不太安全的。我在開發過程中,經常會遇到合作方提供的介面直接以明文返回資料,甚至包括敏感資訊如手機號碼等。雖然這種方式方便快捷,但總體來說並不太安全和可靠。

想要實現一個相對安全的http介面一般有兩種辦法:

  1. 呼叫方需要進行認證並獲取token,呼叫介面時需將token放置於請求頭或Cookie中。處理方通過過濾器檢查token的合法性;
  2. 處理方應生成並提供給呼叫方一個唯一的appId和對應的appSecret。呼叫方使用這個appId去呼叫介面,處理方使用appId和appSecret對資料進行加密。呼叫方獲取到資料後,使用同樣的appId和appSecret進行解密。

 

使用請求頭或Cookie的方式將token放置於請求中的優點是安全性高,因為token不易被竊取或篡改。而使用appId和appSecret進行加密和解密的方式的優點是方便性高,因為appId和appSecret可以在介面檔案或其他途徑中公開,呼叫方只需要使用這些資訊即可進行加解密操作,無需每次都進行認證獲取token。

因此,兩種方式的選擇應根據具體情況而定,通常安全性較為重要的場景可以選擇使用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位元,結果轉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 = "";

//建立httpclient物件
CloseableHttpClient client = HttpClients.createDefault();
//建立post方式請求物件
HttpPost httpPost = new HttpPost(url);
//裝填引數
StringEntity s = new StringEntity(jsonObject.toString(), "utf-8");
s.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE,
"application/json"));
//設定引數到請求物件中
httpPost.setEntity(s);
log.info("請求地址:" + url);
// System.out.println("請求引數:"+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)");
//執行請求操作,並拿到結果(同步阻塞)
CloseableHttpResponse response = client.execute(httpPost);
//獲取結果實體
HttpEntity entity = response.getEntity();
if (entity != null) {
//按指定編碼轉換結果實體為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個位元組;
//如果使用的是AES-192演演算法,則金鑰長度應該是192位,也就是24個位元組;
//如果使用的是AES-256演演算法,則金鑰長度應該是256位,也就是32個位元組
String appKey = UUIDUtil.generateString(32);
//引數加密
JSONObject jsonObject = new JSONObject();
jsonObject.put("appId", appId);
jsonObject.put("appKey", appKey);
jsonObject.put("data", "我是內容");
String encrypt = EncryptUtil.encrypt(jsonObject.toJSONString(), appKey);
System.out.println("加密後內容=" + encrypt);

//引數介面
System.out.println("解密後內容=" + 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();
  }

  /**
   * 生成指定長度純數位唯一標識字串
   *
   * @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();
  }

  /**
   * 由大小寫字母、數位組成的隨機字串
   *
   * @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();
  }

  /**
   * 產生指字個數的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();
  }

  /**
   * 將數位轉化成指字長度的字串
   *
   * @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("將數位" + num + "轉化為長度為" + fixdlenth + "的字串發生異常!");
    }
    sb.append(strNum);
    return sb.toString();
  }

  /**
   * 將數位轉化成指字長度的字串
   *
   * @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("將數位" + num + "轉化為長度為" + fixdlenth + "的字串發生異常!");
    }
    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)遠端過程呼叫是一種程序間通訊的方式,可以讓不同的系統之間通過網路進行通訊和互動。然而,由於RPC介面需要事先定義好介面的引數、返回值、異常等,並且多方合作的開發框架要大致一樣,因此其應用場景比較受限制。此外,不同系統之間的RPC介面需要保持相容性,否則可能會出現介面不匹配、資料傳輸錯誤等問題。因此,在使用RPC介面時,需要進行充分考慮和設計,以確保介面的正確性和可靠性。雖然RPC介面的應用場景有限,但在特定的場景下,RPC介面可以提供高效、可靠的通訊方式,如分散式架構中系統間的服務呼叫。

我在工作中只遇到過一次RPC呼叫的情況。當時,我與公司的不同部門合作,我們使用了同一套框架,他們提供的是RPC介面,我只需引入他們的jar包就能輕鬆呼叫他們的服務。不過,除了公司內部,我很少遇到其他機構或公司使用RPC呼叫的方式。通常,大多數外部介面服務都是通過HTTP介面實現的。

這裡我參照一下ChatGPT的回答:

 

我遇到的情況:有一次,A方需要主動將資料推播給B方,於是提出了用訊息佇列的方案,一聽兩方都覺得既解耦又方便,於是開始行動。A方在自己的伺服器上部署了訊息佇列,但沒想到,各方的伺服器環境是隔離的,網路不通,B方根本無法連線到A方的訊息佇列。他們於是找到了私有云的運維人員,問他們能不能做開放埠、IP加白等一大堆操作,但不知道啥原因就是不行。最後他們只好改為B方提供一個Http介面,A方主動呼叫介面把資料送過去才得以解決。。。

總結一下

在多系統合作的場景中,系統間的互動是非常關鍵的。互動協定的一致性、資料格式的一致性、安全性保障、錯誤處理機制、互動頻率、監控和紀錄檔記錄等方面,都需要特別注意,以確保系統間的互動穩定和可靠。

  1. 互動協定的一致性是系統間進行資料傳輸的基礎,需要明確定義請求和響應報文格式、資料型別、處理規則等。資料格式的一致性也非常重要,需要確定資料交換的格式和編碼方式,避免由於格式不一致而導致的資料解析異常。

  2. 安全性保障是防止系統中出現非法存取和資料洩漏的重要手段,需要採用各種安全措施來保障系統的安全性。

  3. 錯誤處理機制需要考慮系統中可能出現的各種異常情況,並對不同的異常情況進行分類處理,確保資訊及時反饋給使用者。

  4. 互動頻率需要根據實際情況來制定,避免頻繁的呼叫造成系統壓力過大。

  5. 監控和紀錄檔記錄需要對系統進行實時監控,及時發現和處理問題,並記錄紀錄檔以便進行排查和分析。

 
綜上所述,在多方合作時,需要全面考慮系統間的互動問題,以確保系統間的互動穩定可靠,保障合作的順利進行。
最後提醒大家一下,多方合作少不了開會對齊,在溝通的時候還是要耐心和主動一些,都是幹活的應該互幫互助才對,齊心協力才能少加班嘛!