大家好!我是sum墨,一個一線的底層碼農,平時喜歡研究和思考一些技術相關的問題並整理成文,限於本人水平,如果文章和程式碼有表述不當之處,還請不吝賜教。
以下是正文!
我們最近做了很多專案,有些我們是總負責的,有些是合作的。這些專案涉及的系統各種各樣,但基本上沒有一家公司會主動去做『開放平臺』。這是因為投入產出比較低,專案一旦完成就結束了,而且標書裡也沒有要求做開放平臺。雖然這些專案都是業務系統,沒有通用能力好開放的,但在同一個專案中,總是有些東西需要打通,還是需要一種輕量、安全的互動方式。
單點登入是一種方便的登入方式,它可以應用在各種場景中,比如入口網站和小程式跳轉。使用者只需要在登入入口網站時輸入使用者名稱和密碼,就可以輕鬆存取其他相關子系統,無需反覆輸入登入資訊。這不僅方便了使用者,還幫助了IT管理人員更好地管理系統。
以百度為例:
這就是一個典型的單點登入案例,那麼我們怎麼實現單點登入功能呢?
從『系統A門戶頁』點選導航進入『系統B』,使用者資訊是怎麼同步的呢?把資訊放在跳轉連結上傳給系統A肯定不合適,這相當於洩漏了使用者資訊,方案不可行。我們的做法是:
為了安全起見,這個userToken一般都是有時限性的,過了1個小時就不能用了,而且只能用一次,用完就廢棄掉。
我畫個時序圖解釋一下這個邏輯
介面呼叫方式一般有兩種:http介面和rpc介面。
我們都知道http介面是什麼,也能夠輕易地使用Java呼叫Get、Post請求。然而,我們需要考慮http介面的資料安全問題。當我們在瀏覽器或者postman工具中呼叫介面時,資料會以明文形式返回,不需要認證也不需要解密,這顯然是不太安全的。我在開發過程中,經常會遇到合作方提供的介面直接以明文返回資料,甚至包括敏感資訊如手機號碼等。雖然這種方式方便快捷,但總體來說並不太安全和可靠。
想要實現一個相對安全的http介面一般有兩種辦法:
使用請求頭或Cookie的方式將token放置於請求中的優點是安全性高,因為token不易被竊取或篡改。而使用appId和appSecret進行加密和解密的方式的優點是方便性高,因為appId和appSecret可以在介面檔案或其他途徑中公開,呼叫方只需要使用這些資訊即可進行加解密操作,無需每次都進行認證獲取token。
因此,兩種方式的選擇應根據具體情況而定,通常安全性較為重要的場景可以選擇使用token方式,而方便性較為重要的場景可以選擇使用appId和appSecret方式。
這裡我給大家提供一份可用的程式碼工具類,親測可用。
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));
}
}
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; } }
RPC(Remote Procedure Call)遠端過程呼叫是一種程序間通訊的方式,可以讓不同的系統之間通過網路進行通訊和互動。然而,由於RPC介面需要事先定義好介面的引數、返回值、異常等,並且多方合作的開發框架要大致一樣,因此其應用場景比較受限制。此外,不同系統之間的RPC介面需要保持相容性,否則可能會出現介面不匹配、資料傳輸錯誤等問題。因此,在使用RPC介面時,需要進行充分考慮和設計,以確保介面的正確性和可靠性。雖然RPC介面的應用場景有限,但在特定的場景下,RPC介面可以提供高效、可靠的通訊方式,如分散式架構中系統間的服務呼叫。
我在工作中只遇到過一次RPC呼叫的情況。當時,我與公司的不同部門合作,我們使用了同一套框架,他們提供的是RPC介面,我只需引入他們的jar包就能輕鬆呼叫他們的服務。不過,除了公司內部,我很少遇到其他機構或公司使用RPC呼叫的方式。通常,大多數外部介面服務都是通過HTTP介面實現的。
這裡我參照一下ChatGPT的回答:
我遇到的情況:有一次,A方需要主動將資料推播給B方,於是提出了用訊息佇列的方案,一聽兩方都覺得既解耦又方便,於是開始行動。A方在自己的伺服器上部署了訊息佇列,但沒想到,各方的伺服器環境是隔離的,網路不通,B方根本無法連線到A方的訊息佇列。他們於是找到了私有云的運維人員,問他們能不能做開放埠、IP加白等一大堆操作,但不知道啥原因就是不行。最後他們只好改為B方提供一個Http介面,A方主動呼叫介面把資料送過去才得以解決。。。
在多系統合作的場景中,系統間的互動是非常關鍵的。互動協定的一致性、資料格式的一致性、安全性保障、錯誤處理機制、互動頻率、監控和紀錄檔記錄等方面,都需要特別注意,以確保系統間的互動穩定和可靠。
互動協定的一致性是系統間進行資料傳輸的基礎,需要明確定義請求和響應報文格式、資料型別、處理規則等。資料格式的一致性也非常重要,需要確定資料交換的格式和編碼方式,避免由於格式不一致而導致的資料解析異常。
安全性保障是防止系統中出現非法存取和資料洩漏的重要手段,需要採用各種安全措施來保障系統的安全性。
錯誤處理機制需要考慮系統中可能出現的各種異常情況,並對不同的異常情況進行分類處理,確保資訊及時反饋給使用者。
互動頻率需要根據實際情況來制定,避免頻繁的呼叫造成系統壓力過大。
監控和紀錄檔記錄需要對系統進行實時監控,及時發現和處理問題,並記錄紀錄檔以便進行排查和分析。