背景
週三,18:00。
小明扭了扭微微發酸的脖子,揉了揉盯著螢幕有些乾澀的眼睛。
終於忙完了,臨近下班,整個人心也變得放鬆起來。
“對接方需要我們提供新的服務,下週二上線,需求我發你了,很簡單的。”
產品經理髮過來一條訊息,打破了這份美好。
“我可去他的吧,每次需求都是快下班就來了。”小明不免心裡嘀咕了起來,不過手上可沒停。
“好的,我先看下需求。”
回覆完後,點開了需求文件,確實很簡單。
為外部對接方提供一個新增商戶的介面。
保持和內部控臺新增商戶一致。
複製程式碼
確實不太難,小明想了想,內部控臺新增商戶雖然不是自己做的,但是介面應該可以直接複用,程式碼應該也能複用。
不過自己以前沒做過外部對接,不知道有沒有其他的坑。
下週二上線,那麼下週一就需要讓提測,讓測試介入進來。
小明點開了日曆,今天週三,自己介面文件編寫,詳細設計,編碼和自測的時間只剩兩天。
本來應該是充足的,不過平時還會有各種工作瑣事需要處理,會降低整體的工作效率。
還是先問同事要下以前的程式碼和文件吧。
看著時間已經超過了下班時間,小明嘆了一口氣。
介面文件
文件編寫
週四,9:30。
小明來到公司就開始著手處理介面文件的編寫,有以前的文件基礎,寫起來還是很快的。
不過對外的介面還是有些不同,小明按照自己的理解加上了 traceId,requestTime 等欄位,便於問題的排查和定位。
於是基本的介面就寫好了:
請求引數:
序號 | 引數 | 描述 | 是否必填 | 說明 |
---|---|---|---|---|
1 | traceId | 唯一標識 | 是 | 用於唯一定位一筆請求,32 位字串 |
2 | requestTime | 請求引數 | 是 | 請求時間,yyyyMMddHHmmssSSS 17 位時間戳 |
3 | username | 使用者名稱 | 是 | 最長 64 位字串 |
4 | password | 密碼 | 是 | 最長 128 位字串, md5 加密 |
5 | address | 地址 | 否 | 最長 128 位字串 |
響應引數:
序號 | 引數 | 描述 | 是否必填 | 說明 |
---|---|---|---|---|
1 | respCode | 響應編碼 | 是 | 000000 表示成功,其他見響應編碼列舉 |
2 | respDesc | 響應描述 | 是 | 請求時間,yyyyMMddHHmmssSSS 17 位時間戳 |
3 | userId | 使用者標識 | 是 | 32 位字串,成功建立後使用者的唯一標識 |
小明詳細的寫下了整個介面的請求方式,注意事項,以及對應的各種列舉值等。
並且把基本的詳細設計文件也整理了一下。
1762 個字,小明看了看總字數,苦澀的笑了笑。
這份文件,顯然沒有需求文件那麼簡潔。
一抬頭,已經 11:30 了,好傢伙,時間過得真快。
於是預訂了下午 14:00 的會議室,準備和產品經理,測試,專案經理過一下文件。
文件評審
會議室 14:00。
小明按時來到會議室,提前插好投影儀,等著大家的到來。
“我昨天提的需求簡單吧。”,未見其人先問其聲,產品經理剛到門口就笑著走了進來。
“是的,還好。”
接著,專案經理和測試也一起走了進來。
“快點過需求文件吧”,專案經理說道。
小明清了清嗓子,講了下整體的專案背景。並且把自己的詳細設計和介面文件過了一遍。
過的時候,產品經理低頭在做其他的事情,這些細節並不需要關心。
測試聽的比較認真,不停的提出自己的疑問,後面需要自己進行驗證。
“還有其他問題嗎?”,小明自己一個人不停說了半個多小時,覺得有些枯燥。
“我沒什麼問題了”,測試說,“我最關心的就是什麼時候可以提測?”
“下週一吧”,小明頓了頓,“我估計要會後才能開始編碼。”
“那還好”,測試回道,並表示自己沒有其他疑問。
“你這個文件寫的挺詳細的”,專案經理略帶讚許的目光看了下小明,“不過有一個問題,你這個介面連簽名都沒有。”
“簽名,什麼簽名?”,小明有點懵。
“你連對外介面簽名都不會知道?有時間還是要學習學習。”,專案經理顯然有些失望。
“好了,不說這些了。”,產品經理這時加入了談話,“這麼簡單的需求下週二上沒問題吧?”
“應該沒問題,只要按時提測就行”,測試看向了小明。
“應該沒問題”,小明腦子裡還在想介面簽名的事情,“我回去看下介面簽名,調整下介面。”
介面簽名
簽名作用
小明去查了查,發現對外的介面,安全性肯定是需要考慮的。
為了保證資料的安全性,防止資訊被篡改,簽名是比較常見的一種方式。
簽名實現
實現的方式有很多種,比較常用的方式:
(1)將所有引數,除去 checkValue 本身,按引數名字母升序排序。
(2)排序後的引數,按照引數和值的方式拼接為字串
(3)對拼接完的字串,使用雙方約定好的 key 進行 md5 加密,得到 checkValue 值
(4)將對應的值設定到 checkValue 屬性上
當然,在簽名的實現上可能會有差異,但是雙方保持一致即可。
簽名校驗
知道如何進行加簽,校驗也是類似的。
重複上面的 (1)(2)(3)步,得到對應的 checkValue。
然後和對方傳遞的值進行對比,如果一致,說明簽名驗證通過。
介面的調整
在瞭解了簽名的必要性之後,小明在介面文件中新增了 checkValue 這個屬性值。
並且和測試進行了私下的溝通,到這裡,文件才算初步結束。
看著時間已經超過了下班時間,小明嘆了一口氣。
程式碼實現
v1 版本
週五,10:00。
小明來到公司就開始進行編碼,其他的東西處理的差不多之後,就剩下一個簽名的實現問題。
一開始也沒多想,直接實現如下:
/**
* 手動構建驗簽結果
* @return 結果
* @since 0.0.2
*/
public String buildCheckValue() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(name);
stringBuilder.append(password);
// 其他一堆屬性
return Md5Util.md5(stringBuilder.toString());
}
複製程式碼
當然,作為一個拿來主義者,小明意識到一個問題。
其他專案中肯定有類似的工具類,自己不應該重複造輪子。
v2 版本
從其他應用拷貝了一份工具類過來,大概實現如下:
import com.github.houbb.heaven.util.lang.StringUtil;
import com.github.houbb.heaven.util.lang.reflect.ClassUtil;
import com.github.houbb.heaven.util.lang.reflect.ReflectFieldUtil;
import com.github.houbb.heaven.util.secrect.Md5Util;
import java.lang.reflect.Field;
import java.util.*;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class CheckValueUtils {
private CheckValueUtils(){}
public static String buildCheckValue(Object object) {
Class<?> clazz = object.getClass();
// 獲取所有欄位的 fieldMap
Map<String, Field> fieldMap = ClassUtil.getAllFieldMap(clazz);
// 移除 checkValue 名稱的
fieldMap.remove("checkValue");
// 對欄位按名稱排序
Set<String> fieldNameSet = fieldMap.keySet();
List<String> fieldNameList = new ArrayList<>(fieldNameSet);
Collections.sort(fieldNameList);
// 反射獲取所有字串的值
StringBuilder stringBuilder = new StringBuilder();
for(String fieldName : fieldNameList) {
Object value = ReflectFieldUtil.getValue(fieldName, object);
// 反射獲取值
String valueStr = StringUtil.objectToString(value, "");
// 拼接
stringBuilder.append(fieldName).append("=").append(valueStr);
}
//md5 加簽
return Md5Util.md5(stringBuilder.toString());
}
}
複製程式碼
總的來說還是很好用的,而且自己的時間也不多,就直接拿來使用就好。
全部搞定之後,就是自測工作,經過了一次踩坑之後,小明算是把整個介面自測通過了。
“這樣,提測時間也來得及了。”
看著時間已經超過了下班時間,小明嘆了一口氣。
更優雅的加簽實現
一般來說,故事到這裡就結束了。
不過小明的一個想法,讓這個故事繼續走了下去。
工具方法的不足
原有的方法基本可以滿足大部分的需求,不過想要做調整就會變得比較麻煩。
比如,想某些非常大的欄位不參加加簽,加簽欄位的名字不叫 checkValue,而是改成 sign
,調整一下欄位排序的方式等等。
這些都會導致原有的工具方法不可用,需要重新複製,修改。
那能不能實現一個更加靈活的加簽工具呢?
答案是肯定的,小明週末花了 2 天的時間,實現了一個加簽工具。
快速開始
maven 引入
<plugin>
<groupId>com.github.houbb</groupId>
<artifactId>checksum</artifactId>
<version>0.0.6</version>
</plugin>
複製程式碼
pojo 物件
- User.java
public class User {
@CheckField
private String name;
private String password;
@CheckField(required = false)
private String address;
@CheckValue
private String checksum;
//Getter & Setter
//toString()
}
複製程式碼
其中涉及到兩個核心的註解:
@CheckField
表示參與加簽的欄位資訊,預設都是參與加簽的。指定 required=false
跳過加簽。
@CheckValue
表示加簽結果存放的欄位,該欄位型別需要為 String 型別。
後期將會新增一個 String 與不同型別的轉換實現,擴充應用場景。
獲取簽名
所有的工具類方法見 ChecksumHelper
,且下面的幾個方法都支援指定祕鑰。
User user = User.buildUser();
final String checksum = ChecksumHelper.checkValue(user);
複製程式碼
該方法會把 User 物件中指定 @CheckField
的欄位全部進行處理,
通過指定排序後進行拼接,然後結合指定加密策略構建最後的驗簽結果。
填充簽名
User user = User.buildUser();
ChecksumHelper.fill(user);
複製程式碼
可以把對應的 checkValue 值預設填充到 @CheckValue
指定的欄位上。
驗證簽名
User user = User.buildUser();
boolean isValid = ChecksumHelper.isValid(user);
複製程式碼
會對當前的 user 物件進行加簽運算,並且將加簽的結果和 user 本身的簽名進行對比。
引導類
ChecksumBs 引導類
為了滿足更加靈活的場景,我們引入了基於 fluent-api 的 ChecksumBs 引導類。
上面的配置預設等價於:
final String checksum = ChecksumBs
.newInstance()
.target(user)
.charset("UTF-8")
.checkSum(new DefaultChecksum())
.sort(Sorts.quick())
.hash(Hashes.md5())
.times(1)
.salt(null)
.checkFieldListCache(new CheckFieldListCache())
.checkValueCache(new CheckValueCache())
.checkValue();
複製程式碼
配置說明
上面所有的配置都是可以靈活替換的,所有的實現都支援使用者自定義。
屬性 | 說明 |
---|---|
target | 待加簽物件 |
charset | 編碼 |
checkSum | 具體加簽實現 |
sort | 欄位排序策略 |
hash | 字串加密 HASH 策略 |
salt | 加密對應的鹽值 |
times | 加密的次數 |
checkFieldListCache | 待加簽欄位的快取實現 |
checkValueCache | 簽名欄位的快取實現 |
效能
背景
每次我們說到反射第一反應是方便,第二反應就是效能。
有時候往往因為關心效能,而選擇手動一次次的複製,黏貼。
效能
本次進行 100w 次測試驗證,耗時如下。
手動處理耗時:2505ms
註解處理耗時:2927ms
小結
簽名在對外介面的通訊中,可以保證資訊不被篡改。
希望這個工具可以幫到你,讓你按時下班。
我是老馬,期待與你的下次重逢。