AK/SK簡介
AK(Access Key ID,用于標識用戶)/SK(Secret Access Key,是用戶用于加密認證的字串和驗證認證字串的密鑰,SK必須保密),主要用于對用戶的呼叫行為進行鑒權和認證,相當于專用的用戶名和密碼
AK/SK認證流程

客戶端根據雙方協商好的規則演算法生成Signature認證字串,并將生成的Signature認證字串設定到header中,當API網關/服務端接收到請求后,判斷請求中是否包含Signature認證字串,如果包含認證字串,則執行下一步操作,
基于HTTP請求資訊,使用與客戶端相同的規則演算法,生成Signature字串并于與客戶端提供的Signature字串進行比對,如果內容不一致,則認為認證失敗,拒絕該請求;如果內容一致,則表示認證成功,系統將按照客戶端的請求內容進行操作,
客戶端:
構建http請求(包含 access key);
使用請求內容和 使用secret access key計算的簽名(signature);
發送請求到服務端,
服務端:
根據發送的access key 查找資料庫獲得對應的secret-key;
使用一樣的演算法將請求內容和 secret-key一塊兒計算簽名(signature),和步驟2同樣;
對比用戶發送的簽名和服務端計算的簽名,二者相同則認證經過,不然失敗,
實作基本思路
- 客戶端需要在認證服務器中預先設定(AK 或叫 app ID) 和 SK,
- 獲取當前時間時間戳并生成請求唯一標識(隨機碼)
- 在呼叫API前,客戶端需要將對 時間戳、請求標識、請求引數結合SK進行簽名生成一個額外的sign字串
- 將時間戳、請求標識、AK以及生成的sign字串設定到請求header中
- 服務端收到客戶端的請求后,先判斷header中設定的四類認證資料是否存在,
- 根據header中的時間戳與當前時間比對判斷是否該請求以過期,防止抓包后的惡意請求
- 根據header中的請求標識判斷出該請求是否唯一(每次請求將唯一標識保存,待下次請求進來后進行比對判斷,可設定保存時長)
- 根據AK獲取客戶端預先在認證服務器設定好的SK
- 將時間戳、請求標識、請求引數結合客戶端預先設定好的SK使用與客戶端相同的簽名生成方式生成一個臨時的sign字串并與客戶端請求中包含的sign字串比較,
- 5、6、7、8、9這五步全部通過繼續執行下一步操作,否則認證失敗回傳錯誤碼
代碼實作
基于上面的實作思路,大致寫下代碼,代碼中加的有詳細注釋,邏輯就不一一解釋了,寫的比較簡單,
攔截器懶得寫了哈,我這就直接通過AOP 前置通知來實作認證資訊的獲取以及認證
@Before("executePointcut()")
public void before(JoinPoint joinPoint){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Object[] args = joinPoint.getArgs();
JSONObject json = (JSONObject) args[args.length - 1];
Map sortedMap = JsonToMap.sortParams(json);
Long timeStamp = Long.parseLong(request.getHeader("TimeStamp"));
String nonce = request.getHeader("nonce");
String s = map.get(nonce);
if (s!=null){
log.error("重復的請求...");
Asserts.fail("Repeat request");
}
map.put(nonce,nonce);
//開啟守護執行緒 清除請求唯一標識
executorService.execute(new RemoveMapRunnable(nonce));
String sign = request.getHeader("sign");
if (timeStamp==null||timeStamp<1||StringUtils.isNotEmpty(nonce)
||StringUtils.isNotEmpty(sign)){
long endTime = System.currentTimeMillis();
if (endTime-timeStamp > l){
log.error("請求過期失效..");
Asserts.fail("Request expired");
}
}else{
log.error("認證引數缺失..");
Asserts.fail("Missing authentication parameters");
}
if(!SignUtil.checkReqInfo(timeStamp, nonce, sign, sortedMap)){
log.error("認證失敗,sign={}",sign);
Asserts.fail("Authentication failed");
}
log.info("認證成功...");
}
private class RemoveMapRunnable implements Runnable{
private String nonce;
public RemoveMapRunnable(String nonce){
this.nonce = nonce;
}
@Override
public void run() {
synchronized (this){
try {
Thread.sleep(l);
map.remove(this.nonce);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* @Author lijl
* @MethodName wrapperHeader
* @Description 通過請求引數,包裝請求header資訊(含簽名資訊)
* @Date 16:14 2021/11/11
* @Version 1.0
* @param reqParam
* @return: {sign=02C89AD7CEC9C05831520015CD7C3413F1DE03822D2DA015A7B353B7E7F38E7D, nonce=6b10f2ee-aba6-4032-bc9f-ca82c76b30d1, TimeStamp=1636684729852}
**/
public static Map<String, Object> wrapperHeader(Map<String, Object> reqParam) {
Long ts = System.currentTimeMillis();
String nonce = UUID.randomUUID().toString();
Map<String, Object> header = new HashMap<>();
//進行介面呼叫時的時間戳,即當前時間戳(毫秒),服務端會校驗時間戳,例如時間差超過30秒則認為請求無效,防止重復請求的攻擊
header.put("TimeStamp", ts);
//每個請求提供一個唯一的識別符號,服務器能夠防止請求被多次使用
header.put("nonce", nonce);
//按簽名演算法獲取sign
String sign = getSign(appSecret, ts, nonce, reqParam);
header.put("sign", sign);
return header;
}
/**
* @Author lijl
* @MethodName getSign
* @Description 按簽名演算法獲取sign
* @Date 16:04 2021/11/11
* @Version 1.0
* @param appSecret
* @param ts
* @param nonce
* @param reqParam
* @return: java.lang.String
**/
private static String getSign(String appSecret, Long ts, String nonce, Map<String, Object> reqParam) {
// 計算簽名規則:sign = HMACSHA256("ts=1623388123195&noce=d50e301d-ee2c-446e-8f28-013f0fee09fb&appSecret=1ZLAzEgQHfBd19vSapdL8lxzA&1=2&1=2")
// 1.請求引數key升序
// 2.待加密字串
StringBuffer s = new StringBuffer();
s.append("&ts=").append(ts).append("&noce=").append(nonce).append("&appSecret=").append(appSecret);
reqParam.forEach((k, v) -> s.append("&").append(k).append("=").append(v));
// 3.對待加密字串進行加密(對字串HMACSHA256處理,得到sign值)
try {
return HMACSHA256(s.toString());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* @Author lijl
* @MethodName checkReqInfo
* @Description 驗證請求是否有效
* @Date 10:36 2021/11/12
* @Version 1.0
* @param ts
* @param nonce
* @param sign
* @param reqParam
* @return: 是否有效(方便測驗我用Boolean,可根據業務需要,回傳對應錯誤資訊,不一定用Boolean)
**/
public static Boolean checkReqInfo(Long ts, String nonce, String sign,Map<String, Object> reqParam) {
String srvSign = getSign(appSecret, ts, nonce, reqParam);
// 目前能想到的安全驗證就這些,或許大家還能想到其他驗證,讓介面更加安全
return sign.equalsIgnoreCase(srvSign);
}
/**
* @Author lijl
* @MethodName HMACSHA256
* @Description HMAC-SHA256演算法
* @Date 10:32 2021/11/12
* @Version 1.0
* @param data
* @return: java.lang.String
**/
public static String HMACSHA256(String data) throws Exception {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(appSecret.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
public static void main(String[] args) {
Map<String, Object> reqParam = new HashMap<String, Object>();
reqParam.put("1", "2");
reqParam.put("2", "1");
//請求頭(行sign值等資訊)
Map<String, Object> reqHeader = wrapperHeader(reqParam);
System.out.println(reqHeader);
// ==================客戶端發起請求,引數param,并把header帶入請求中
// ============================服務器端,收到請求
// 1.驗證請求資訊
// 2處理業務邏輯
// 3.回傳資料到客戶端
long ts = (long) reqHeader.get("TimeStamp");
String nonce = (String) reqHeader.get("nonce");
String sign = (String) reqHeader.get("sign");
Boolean valid = checkReqInfo(ts,nonce,sign,reqParam);
if (valid){
System.out.println("有效請求,繼續處理...");
}else {
System.out.println("無效");
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/356730.html
標籤:其他
