一鳞疲、什么是appid、appkey蠕蚜、appsecret
AppID:應(yīng)用的唯一標(biāo)識(shí)尚洽。
AppKey:公匙(相當(dāng)于賬號(hào))。
AppSecret:私匙(相當(dāng)于密碼)
token:令牌(過期失效)
app_id:是用來標(biāo)記你的開發(fā)者賬號(hào)的靶累,是你的用戶id腺毫,這個(gè)id 在數(shù)據(jù)庫添加檢索,方便快速查找挣柬。
app_key 和 app_secret 是一對(duì)出現(xiàn)的賬號(hào)潮酒,同一個(gè) app_id 可以對(duì)應(yīng)多個(gè) app_key+app_secret,這樣平臺(tái)就可以分配你不一樣的權(quán)限邪蛔,比如 app_key1 + app_secect1 只有只讀權(quán)限 但是 app_key2+app_secret2 有讀寫權(quán)限......急黎,這樣你就可以把對(duì)應(yīng)的權(quán)限放給不同的開發(fā)者,其中權(quán)限的配置都是直接跟app_key 做關(guān)聯(lián)的侧到,app_key 也需要添加數(shù)據(jù)庫檢索勃教,方便快速查找。
至于為什么 要有app_key + app_secret 這種成對(duì)出現(xiàn)的機(jī)制呢匠抗,因?yàn)?要加密故源,通常 在首次驗(yàn)證(類似登錄場(chǎng)景) ,你需要用 app_key(標(biāo)記要申請(qǐng)的權(quán)限有哪些) + app_secret(密碼汞贸,表示你真的擁有這個(gè)權(quán)限) 來申請(qǐng)一個(gè)token绳军,就是我們經(jīng)常用到的 access_token,之后的數(shù)據(jù)請(qǐng)求矢腻,就直接提供access_token 就可以驗(yàn)證權(quán)限了门驾。
簡化的場(chǎng)景
- 1、省去 app_id踏堡,他默認(rèn)每一個(gè)用戶有且僅有一套權(quán)限配置猎唁,所以直接將 app_id = app_key,然后外加一個(gè)app_secret就夠了顷蟆。
- 2诫隅、省去app_id 和 app_key,相當(dāng)于 app_id = app_key = app_secret帐偎,通常用于開放性接口的地方逐纬,特別是很多地圖類api 都采用這種模式,這種模式下削樊,帶上app_id 的目的僅僅是統(tǒng)計(jì) 某一個(gè)用戶調(diào)用接口的次數(shù)而已了豁生。
使用方法
- 1、向第三方服務(wù)器請(qǐng)求授權(quán)時(shí)漫贞,帶上AppKey和AppSecret(需存在服務(wù)器端)
- 2甸箱、第三方服務(wù)器驗(yàn)證AppKey和AppSecret在DB中有無記錄
- 3、如果有迅脐,生成一串唯一的字符串(token令牌)芍殖,返回給服務(wù)器,服務(wù)器再返回給客戶端
- 4谴蔑、客戶端下次請(qǐng)求敏感數(shù)據(jù)時(shí)帶上令牌
二豌骏、云服務(wù)AppId或AppKey和AppSecret生成策略
App key簡稱API接口驗(yàn)證序號(hào),是用于驗(yàn)證API接入合法性的隐锭。接入哪個(gè)網(wǎng)站的API接口窃躲,就需要這個(gè)網(wǎng)站允許才能夠接入,如果簡單比喻的話:可以理解成是登陸網(wǎng)站的用戶名钦睡。
App Secret簡稱API接口密鑰蒂窒,是跟App Key配套使用的,可以簡單理解成是密碼荞怒。
App Key 和 App Secret 配合在一起洒琢,通過其他網(wǎng)站的協(xié)議要求,就可以接入API接口調(diào)用或使用API提供的各種功能和數(shù)據(jù)挣输。
比如淘寶聯(lián)盟的API接口纬凤,就是淘寶客網(wǎng)站開發(fā)的必要接入,淘客程序通過API接口直接對(duì)淘寶聯(lián)盟的數(shù)據(jù)庫調(diào)用近億商品實(shí)時(shí)數(shù)據(jù)撩嚼。做到了輕松維護(hù)停士,自動(dòng)更新。
2.1 UUID
UUID是指在一臺(tái)機(jī)器在同一時(shí)間中生成的數(shù)字在所有機(jī)器中都是唯一的完丽。按照開放軟件基金會(huì)(OSF)制定的標(biāo)準(zhǔn)計(jì)算恋技,用到了以太網(wǎng)卡地址、納秒級(jí)時(shí)間逻族、芯片ID碼和許多可能的數(shù)字
UUID由以下幾部分的組合:
- 1蜻底、當(dāng)前日期和時(shí)間。
- 2聘鳞、時(shí)鐘序列薄辅。
- 3要拂、全局唯一的IEEE機(jī)器識(shí)別號(hào),如果有網(wǎng)卡站楚,從網(wǎng)卡MAC地址獲得脱惰,沒有網(wǎng)卡以其他方式獲得。
標(biāo)準(zhǔn)的UUID格式為:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12)窿春,以連字號(hào)分為五段形式的36個(gè)字符拉一,示例:550e8400-e29b-41d4-a716-446655440000
Java標(biāo)準(zhǔn)類庫中已經(jīng)提供了UUID的API。
UUID.randomUUID()
2.2 代碼實(shí)現(xiàn)
- AppSecret 使用SHA-1生成20位byte數(shù)組旧乞,基本很難重復(fù)蔚润,再轉(zhuǎn)化為40位16進(jìn)制數(shù)字字符串。
/**
* @author: huangyibo
* @Date: 2022/6/15 16:17
* @Description: AppSecret 使用SHA-1生成20位byte數(shù)組尺栖,基本很難重復(fù)嫡纠,再轉(zhuǎn)化為40位16進(jìn)制數(shù)字字符串。
*/
public class AppUtils {
//生成 app_secret 密鑰
private final static String SERVER_NAME = "mazhq_abc123";
private final static String[] CHARS = new String[]{"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", "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"};
/**
* @Description: <p>
* 短8位UUID思想其實(shí)借鑒微博短域名的生成方式决瞳,但是其重復(fù)概率過高货徙,而且每次生成4個(gè),需要隨即選取一個(gè)皮胡。
* 本算法利用62個(gè)可打印字符痴颊,通過隨機(jī)生成32位UUID,由于UUID都為十六進(jìn)制屡贺,所以將UUID分成8組蠢棱,每4個(gè)為一組甩栈,然后通過模62操作玉转,結(jié)果作為索引取出字符究抓,
* 這樣重復(fù)率大大降低。
* 經(jīng)測(cè)試稽荧,在生成一千萬個(gè)數(shù)據(jù)也沒有出現(xiàn)重復(fù)擅腰,完全滿足大部分需求惕鼓。
* </p>
*/
public static String getAppId() {
StringBuilder shortBuffer = new StringBuilder();
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 % 0x3E]);
}
return shortBuffer.toString();
}
/**
* <p>
* 通過appId和內(nèi)置關(guān)鍵詞生成APP Secret
* </P>
*/
public static String getAppSecret(String appId) {
try {
String[] array = new String[]{appId, SERVER_NAME};
StringBuilder sb = new StringBuilder();
// 字符串排序
Arrays.sort(array);
for (String str : array) {
sb.append(str);
}
String str = sb.toString();
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(str.getBytes());
byte[] digest = md.digest();
System.out.println(digest.length);
StringBuilder hexstr = new StringBuilder();
String shaHex = "";
for (int i = 0; i < digest.length; i++) {
shaHex = Integer.toHexString(digest[i] & 0xFF);
if (shaHex.length() < 2) {
hexstr.append(0);
}
hexstr.append(shaHex);
}
return hexstr.toString();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException();
}
}
public static void main(String[] args) {
String appId = getAppId();
String appSecret = getAppSecret(appId);
System.out.println("appId: "+appId);
System.out.println("appSecret: "+appSecret);
String random = RandomStringUtils.randomAlphanumeric(63);
System.out.println(random);
System.out.println("09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7");
}
}
- AppSecret使用sha-256生成32位byte數(shù)組一膨,基本很難重復(fù),再轉(zhuǎn)化為64位16進(jìn)制數(shù)字字符串瞒津。
/**
* @author: huangyibo
* @Date: 2022/6/30 16:36
* @Description: AppSecret使用sha-256生成32位byte數(shù)組,基本很難重復(fù)濒翻,再轉(zhuǎn)化為64位16進(jìn)制數(shù)字字符串淌喻。
*/
public class AppUtils {
//某某服務(wù) 生成 app_secret 密鑰
private final static String SERVER_NAME = "mazhq_abc123";
private final static String[] CHARS = new String[]{"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", "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"};
/**
* @Description: <p>
* 短8位UUID思想其實(shí)借鑒微博短域名的生成方式,但是其重復(fù)概率過高涯塔,而且每次生成4個(gè),需要隨即選取一個(gè)。
* 本算法利用62個(gè)可打印字符药薯,通過隨機(jī)生成32位UUID真屯,由于UUID都為十六進(jìn)制泵额,所以將UUID分成8組篓叶,每4個(gè)為一組缸托,然后通過模62操作,結(jié)果作為索引取出字符京革,
* 這樣重復(fù)率大大降低。
* 經(jīng)測(cè)試廊勃,在生成一千萬個(gè)數(shù)據(jù)也沒有出現(xiàn)重復(fù),完全滿足大部分需求冰悠。
* </p>
*/
public static String getAppId() {
StringBuilder shortBuffer = new StringBuilder();
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 % 0x3E]);
}
return shortBuffer.toString();
}
/**
* 通過appId和內(nèi)置關(guān)鍵詞生成APP Secret
* @param appId
* @return
*/
public static String getAppSecret(String appId) {
String[] array = new String[]{appId, SERVER_NAME};
StringBuilder stringBuilder = new StringBuilder();
// 字符串排序
Arrays.sort(array);
for (String str : array) {
stringBuilder.append(str);
}
String encodeStr = "";
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(stringBuilder.toString().getBytes(StandardCharsets.UTF_8));
encodeStr = byte2Hex(messageDigest.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return encodeStr;
}
private static String byte2Hex(byte[] bytes) {
System.out.println(bytes.length);
StringBuilder stringBuilder = new StringBuilder();
String temp = null;
for (int i = 0; i < bytes.length; i++) {
temp = Integer.toHexString(bytes[i] & 0xFF);
if (temp.length() == 1) {
// 1得到一位的進(jìn)行補(bǔ)0操作
stringBuilder.append(0);
}
stringBuilder.append(temp);
}
return stringBuilder.toString();
}
public static void main(String[] args) {
System.out.println("appId: " + getAppId());
System.out.println("appSecret: " + getAppSecret("130"));
System.out.println(getAppSecret("13034234324weweasxwsqszASxsadreqqscdzsd"));
System.out.println(getAppSecret("13034234324weweasxwsqszASxsadreq{ww=bb,see=2ss}"));
}
}
三桑寨、API 接口開發(fā)安全性
接口的安全性主要圍繞token、timestamp和sign三個(gè)機(jī)制展開設(shè)計(jì)沙咏,保證接口的數(shù)據(jù)不會(huì)被篡改和重復(fù)調(diào)用徒役。
在代碼層面瞻讽,對(duì)接口進(jìn)行安全設(shè)計(jì)
- 1速勇、使用token進(jìn)行用戶身份認(rèn)證
- 2烦磁、使用sign防止傳入?yún)?shù)被篡改
- 3都伪、使用timestamp時(shí)間戳防止暴力請(qǐng)求
3.1 使用token進(jìn)行用戶身份認(rèn)證授權(quán)
具體說明如下:
- 1帝璧、 用戶登錄時(shí)先誉,客戶端請(qǐng)求接口,傳入用戶名和密文的密碼
- 2的烁、 后臺(tái)服務(wù)對(duì)用戶身份進(jìn)行驗(yàn)證褐耳。若驗(yàn)證失敗,則返回錯(cuò)誤結(jié)果渴庆;若驗(yàn)證通過铃芦,則生成一個(gè)隨機(jī)不重復(fù)的token(可以是UUID)买雾,并將其存儲(chǔ)在redis中,設(shè)置一個(gè)過期時(shí)間胧砰。
- 其中贪薪,redis的key為token典格,value為驗(yàn)證通過后獲得的用戶信息
- 3本鸣、 用戶身份校驗(yàn)通過后,后臺(tái)服務(wù)將生成的token返回客戶端宁否。
- 客戶端請(qǐng)求后續(xù)其他接口時(shí),需要帶上這個(gè)token狠角。后臺(tái)服務(wù)會(huì)統(tǒng)一攔截接口請(qǐng)求,進(jìn)行token有效性校驗(yàn)窿克,并從中獲取用戶信息衬横,供后續(xù)業(yè)務(wù)邏輯使用霉翔,Token是客戶端訪問服務(wù)端的憑證子眶。
3.2 使用sign防止傳入?yún)?shù)被篡改
為了防止中間人攻擊(客戶端發(fā)來的請(qǐng)求被第三方攔截篡改)臭杰,引入?yún)?shù)的簽名機(jī)制来吩。
- 1伦仍、客戶端和服務(wù)端約定一個(gè)加密算法(MD5或SHA-1算法(可根據(jù)情況加點(diǎn)鹽))结窘, 客戶端發(fā)起請(qǐng)求時(shí),將所有的非空參數(shù)按升序拼在一起充蓝,通過加密算法形成一個(gè)sign隧枫,將其放在請(qǐng)求頭中傳遞給后端服務(wù)。
- 2谓苟、后端服務(wù)統(tǒng)一攔截接口請(qǐng)求悠垛,用接收到的非可空參數(shù)根據(jù)約定好的規(guī)則進(jìn)行加密,和傳入的sign值進(jìn)行比較娜谊。若一致則予以放行,不一致則拒絕請(qǐng)求斤讥。
由于中間人不知道加密方法纱皆,也就不能偽造一個(gè)有效的sign。從而防止了中間人對(duì)請(qǐng)求參數(shù)的篡改芭商。
3.3 用時(shí)間戳防止暴力請(qǐng)求
時(shí)間戳超時(shí)機(jī)制
用戶每次請(qǐng)求都帶上當(dāng)前時(shí)間的時(shí)間戳timestamp派草,服務(wù)端接收到timestamp后跟當(dāng)前時(shí)間進(jìn)行比對(duì),如果時(shí)間差大于一定時(shí)間(比如5分鐘)铛楣,則認(rèn)為該請(qǐng)求失效近迁。時(shí)間戳超時(shí)機(jī)制是防御DOS攻擊的有效手段。
sign機(jī)制可以防止參數(shù)被篡改簸州,但無法防dos攻擊(第三方使用正確的參數(shù)鉴竭,不停請(qǐng)求服務(wù)器歧譬,使之無法正常提供服務(wù))。因此搏存,還需要引入時(shí)間戳機(jī)制瑰步。
具體的操作為:
客戶端在形成sign值時(shí),除了使用所有參數(shù)和token外璧眠,再加一個(gè)發(fā)起請(qǐng)求時(shí)的時(shí)間戳缩焦。即 sign值來源 = 所有非空參數(shù)升序排序+token+timestamp
而服務(wù)端則需要根據(jù)當(dāng)前時(shí)間和sign值的時(shí)間戳進(jìn)行比較,差值超過一段時(shí)間則不予放行责静。
若要求不高袁滥,則客戶端和服務(wù)端可以僅僅使用精確到秒或分鐘的時(shí)間戳,據(jù)此形成sign值來校驗(yàn)有效性灾螃。這樣可以使一秒或一分鐘內(nèi)的請(qǐng)求是有效的题翻。
若要求較高,則還需要約定一個(gè)解密算法睦焕,使后端服務(wù)可以從sign值中解析出發(fā)起請(qǐng)求的時(shí)間戳藐握。
總結(jié)后的流程圖如下:
3.4 拒絕重復(fù)調(diào)用(非必須)
客戶端第一次訪問時(shí),將簽名sign存放到緩存服務(wù)器中垃喊,超時(shí)時(shí)間設(shè)定為跟時(shí)間戳的超時(shí)時(shí)間一致猾普,二者時(shí)間一致可以保證無論在timestamp限定時(shí)間內(nèi)還是外 URL都只能訪問一次。如果有人使用同一個(gè)URL再次訪問本谜,如果發(fā)現(xiàn)緩存服務(wù)器中已經(jīng)存在了本次簽名初家,則拒絕服務(wù)。如果在緩存中的簽名失效的情況下乌助,有人使用同一個(gè)URL再次訪問溜在,則會(huì)被時(shí)間戳超時(shí)機(jī)制攔截。這就是為什么要求時(shí)間戳的超時(shí)時(shí)間要設(shè)定為跟時(shí)間戳的超時(shí)時(shí)間一致他托。拒絕重復(fù)調(diào)用機(jī)制確保URL被別人截獲了也無法使用(如抓取數(shù)據(jù))掖肋。
在以上三種機(jī)制的保護(hù)下,如果有人劫持了請(qǐng)求赏参,并對(duì)請(qǐng)求中的參數(shù)進(jìn)行了修改志笼,簽名就無法通過;
如果有人使用已經(jīng)劫持的URL進(jìn)行DOS攻擊把篓,服務(wù)器則會(huì)因?yàn)榫彺娣?wù)器中已經(jīng)存在簽名或時(shí)間戳超時(shí)而拒絕服務(wù)纫溃,所以DOS攻擊也是不可能的;
所有的安全措施都用上的話有時(shí)候難免太過復(fù)雜韧掩,在實(shí)際項(xiàng)目中需要根據(jù)自身情況作出裁剪紊浩,比如可以只使用簽名機(jī)制就可以保證信息不會(huì)被篡改,或者定向提供服務(wù)的時(shí)候只用Token機(jī)制就可以了。如何裁剪坊谁,全看項(xiàng)目實(shí)際情況和對(duì)接口安全性的要求费彼。
四、基于AccessToken方式實(shí)現(xiàn)API設(shè)計(jì)
需求:
- A呜袁、B機(jī)構(gòu)需要調(diào)用X服務(wù)器的接口敌买,那么X服務(wù)器就需要提供開放的外網(wǎng)訪問接口。
分析:
- 1阶界、開放平臺(tái)提供者X虹钮,為每一個(gè)合作機(jī)構(gòu)提供對(duì)應(yīng)的appid、app_secret膘融。
- 2芙粱、appid是唯一的(不能改變),表示對(duì)應(yīng)的第三方合作機(jī)構(gòu)氧映,用來區(qū)分不同機(jī)構(gòu)的春畔。
- 3、app_secret在傳輸中實(shí)現(xiàn)加密功能(秘鑰)岛都,該秘鑰可以發(fā)生改變的律姨。
- 4、為什么app_secret是可以改變的臼疫?調(diào)用接口需要appid+app_secret生成對(duì)應(yīng)的access_token(臨時(shí)性)择份,如果appid和app_secret被泄密,產(chǎn)生安全性問題烫堤,如果一但發(fā)現(xiàn)被泄密荣赶,可以重新生成一個(gè)app_secret。
原理:為每個(gè)合作機(jī)構(gòu)創(chuàng)建對(duì)應(yīng)的appid鸽斟、app_secret拔创,生成對(duì)應(yīng)的access_token(有效期2小時(shí)),在調(diào)用外網(wǎng)開放接口的時(shí)候富蓄,必須傳遞有效的access_token剩燥。
4.1 開發(fā)步驟
4.1.1、使用appid+app_secret生成對(duì)應(yīng)的access_token
- 1立倍、獲取生成的AppId和appSecret躏吊,并驗(yàn)證是否可用
- 2、刪除之前的accessToken
- 3帐萎、AppId和appSecret保證生成對(duì)應(yīng)唯一的accessToken
- 注意:以上第二步必須保證在同一事務(wù)中
- 4、返回最新的accessToken
4.1.2胜卤、使用accessToken調(diào)用第三方接口
- 1疆导、獲取對(duì)應(yīng)的accessToken
- 2、使用AccessToken查詢r(jià)edis對(duì)應(yīng)的value(appId)
- 3葛躏、如果沒有獲取到對(duì)應(yīng)的appid澈段,直接返回錯(cuò)誤提示
- 4悠菜、如果能獲取到對(duì)應(yīng)的appid,使用appid查詢對(duì)應(yīng)的APP信息
- 5败富、使用appId查詢數(shù)據(jù)庫app信息悔醋,獲取is_flag狀態(tài),如果為1兽叮,則不能調(diào)用接口芬骄,否則正常執(zhí)行
- 6、直接調(diào)用接口業(yè)務(wù)
五鹦聪、常見問題總結(jié)
做API接口账阻,為什么access_token要放在Header頭里傳遞?
如果是OAuth2, 使用 Header傳遞token是屬于規(guī)范的一種泽本,Header中有一個(gè)Authorization頭專門用于存放認(rèn)證信息每一次登錄淘太,會(huì)生成一個(gè)新的Token, 此時(shí)舊的token并不會(huì)立即失效(取決于該token生成時(shí),設(shè)置的失效時(shí)間)
六规丽、代碼實(shí)現(xiàn)
服務(wù)提供方
- 處理無法重復(fù)讀取stream流,使之可以在一個(gè)stream流中多次讀取同一個(gè)request值
import org.apache.commons.io.IOUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* @Author: huangyibo
* @Date: 2023/1/6 14:43
* @Description: 處理無法重復(fù)讀取stream流,使之可以在一個(gè)stream流中多次讀取同一個(gè)request值
*/
public class RequestWrapper extends HttpServletRequestWrapper {
//參數(shù)字節(jié)數(shù)組
private byte[] requestBody;
//Http請(qǐng)求對(duì)象
private HttpServletRequest request;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.request = request;
}
/**
* @return
* @throws IOException
*/
@Override
public ServletInputStream getInputStream() throws IOException {
/**
* 每次調(diào)用此方法時(shí)將數(shù)據(jù)流中的數(shù)據(jù)讀取出來蒲牧,然后再回填到InputStream之中
* 解決通過@RequestBody和@RequestParam(POST方式)讀取一次后控制器拿不到參數(shù)問題
*/
if (null == this.requestBody) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
IOUtils.copy(request.getInputStream(), baos);
this.requestBody = baos.toByteArray();
}
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() {
return bais.read();
}
};
}
public byte[] getRequestBody() {
return requestBody;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
- 攔截所有請(qǐng)求過濾器,并將請(qǐng)求類型是HttpServletRequest類型的請(qǐng)求替換為自定義{@link RequestWrapper}
import org.springframework.stereotype.Component;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* @Author: huangyibo
* @Date: 2023/1/5 15:36
* @Description: 攔截所有請(qǐng)求過濾器赌莺,并將請(qǐng)求類型是HttpServletRequest類型的請(qǐng)求替換為自定義{@link RequestWrapper}
*/
@Component
@WebFilter(filterName = "ChannelFilter", urlPatterns = {"/*"})
public class ChannelFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if (request instanceof HttpServletRequest) {
requestWrapper = new RequestWrapper((HttpServletRequest) request);
}
if (requestWrapper == null) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response);
}
}
}
- 商戶鑒權(quán)攔截器-TenantAuthInterceptor
/**
* @Author: huangyibo
* @Date: 2023/1/3 18:29
* @Description: 商戶對(duì)外開放鑒權(quán)攔截器
*/
@Component
@Slf4j
public class TenantAuthInterceptor extends HandlerInterceptorAdapter {
@Resource
private RedisUtil redisUtil;
@Resource
private SysTenantAppFeign sysTenantAppFeign;
/**
* 單次請(qǐng)求timestamp參數(shù)過期時(shí)間為5分鐘
*/
private static final long TIMESTAMP_EXPIRE = 5 * 60 * 1000;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//從header中獲取接口簽名
String sign = request.getHeader(JwtConstants.ACCESS_OPEN_API_SIGN_HEADER);
if(StringUtils.isEmpty(sign)){
log.error("商戶接口簽名為空, url={}", request.getRequestURL());
ResultData<String> resultData = ResultData.fail(SysResultEnum.TENANT_SIGN_EMPTY.getStatus(), SysResultEnum.TENANT_SIGN_EMPTY.getMessage(), null);
ResultUtil.responseJsonMsg(response, resultData, null);
return Boolean.FALSE;
}
Map paramMap = new TreeMap<>();
if(HttpMethod.GET.name().equals(request.getMethod())){
queryGetParamterMap(request,paramMap);
}else {
//獲取請(qǐng)求body
byte[] bodyBytes = StreamUtils.copyToByteArray(request.getInputStream());
String body = new String(bodyBytes, request.getCharacterEncoding());
paramMap = JSONObject.parseObject(body, TreeMap.class);
}
String timestampStr = String.valueOf(paramMap.get("timestamp"));
if(StringUtils.isEmpty(timestampStr)){
log.error("商戶接口請(qǐng)求參數(shù)時(shí)間戳為空, url={}", request.getRequestURL());
ResultData<String> resultData = ResultData.fail(SysResultEnum.TENANT_TIMESTAMP_EMPTY.getStatus(), SysResultEnum.TENANT_TIMESTAMP_EMPTY.getMessage(), null);
ResultUtil.responseJsonMsg(response, resultData, null);
return Boolean.FALSE;
}
long timestamp = Long.parseLong(timestampStr);
if((System.currentTimeMillis() - timestamp) >= TIMESTAMP_EXPIRE){
log.error("商戶接口單次請(qǐng)求timestamp參數(shù)已失效, url={}", request.getRequestURL());
ResultData<String> resultData = ResultData.fail(SysResultEnum.TENANT_TIMESTAMP_EXPIRE.getStatus(), SysResultEnum.TENANT_TIMESTAMP_EXPIRE.getMessage(), null);
ResultUtil.responseJsonMsg(response, resultData, null);
return Boolean.FALSE;
}
String appId = (String)paramMap.get("appId");
SysTenantAppRedisVo tenantAppRedisVo = (SysTenantAppRedisVo) redisUtil.get(RedisKeyConstant.SYS_TENANT_APP_INFO + appId);
if(tenantAppRedisVo == null){
tenantAppRedisVo = sysTenantAppFeign.selectTenantByAppId(appId).pickBody();
if (Objects.nonNull(tenantAppRedisVo.getSysResultEnum())) {
log.error("商戶鑒權(quán)攔截校驗(yàn), 驗(yàn)證不通過,appId={}, message={}", appId, tenantAppRedisVo.getSysResultEnum().getMessage());
ResultData<String> resultData = ResultData.fail(tenantAppRedisVo.getSysResultEnum().getStatus(), tenantAppRedisVo.getSysResultEnum().getMessage(), null);
ResultUtil.responseJsonMsg(response, resultData, null);
return Boolean.FALSE;
}
//保存20分鐘
redisUtil.set(RedisKeyConstant.SYS_TENANT_APP_INFO + appId, tenantAppRedisVo, 1200);
log.info("商戶鑒權(quán)攔截校驗(yàn), 商戶信息獲取成功, 保存redis20分鐘, appId={}, tenantAppRedisVo={}",
appId, JSON.toJSONString(tenantAppRedisVo));
}
StringBuilder stringBuilder = new StringBuilder();
paramMap.forEach((key, value) -> {
if(!StringUtils.isEmpty(value)){
stringBuilder.append(value);
}
});
stringBuilder.append(tenantAppRedisVo.getSecurityKey());
String localSign = DigestUtils.md5DigestAsHex(stringBuilder.toString().getBytes()).toUpperCase();
if(!sign.equals(localSign)){
log.error("商戶接口簽名異常, appId={}, url={}", appId, request.getRequestURL());
ResultData<String> resultData = ResultData.fail(SysResultEnum.TENANT_SIGN_ILLEGAL.getStatus(), SysResultEnum.TENANT_SIGN_ILLEGAL.getMessage(), null);
ResultUtil.responseJsonMsg(response, resultData, null);
return Boolean.FALSE;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
TenantAuthAnnotation auth = handlerMethod.getMethod().getAnnotation(TenantAuthAnnotation.class);
if (auth == null) {
// 如果注解為null, 說明不需要攔截, 直接放過
return Boolean.TRUE;
}
//校驗(yàn)用戶是否有權(quán)限
if (!hasAuth(tenantAppRedisVo.getInterfaceCodeList(), auth)) {
log.error("商戶權(quán)限攔截校驗(yàn), 接口權(quán)限不通過, appId={}, auth={}", appId, JSON.toJSONString(auth.value()));
ResultData<String> resultData = ResultData.fail(SysResultEnum.UNAUTHORIZED.getStatus(), SysResultEnum.UNAUTHORIZED.getMessage(), null);
ResultUtil.responseJsonMsg(response, resultData, null);
return Boolean.FALSE;
}
log.info("商戶鑒權(quán)成功, appId={}, url={}", appId, request.getRequestURL());
return Boolean.TRUE;
}
/**
* 獲取Get請(qǐng)求參數(shù)
* @param request
* @param reqMap
*/
private void queryGetParamterMap(HttpServletRequest request, Map reqMap) {
Map parameterMap = request.getParameterMap();
Set<Map.Entry<String,String[]>> entry = parameterMap.entrySet();
Iterator<Map.Entry<String,String[]>> it = entry.iterator();
while (it.hasNext()){
Map.Entry<String,String[]> me = it.next();
String key = me.getKey();
String value = me.getValue()[0];
reqMap.put(key,value);
}
}
/**
* 校驗(yàn)權(quán)限是否匹配
* @param authList
* @param auth
* @return
*/
private boolean hasAuth(List<InterfaceCodeEnum> authList, TenantAuthAnnotation auth) {
if (!CollectionUtils.isEmpty(authList)) {
for (InterfaceCodeEnum authEnum : auth.value()) {
if (authList.contains(authEnum)) {
return true;
}
}
}
return false;
}
}
接入方
- 接入方OpenApiFeign
@FeignClient(name = "OpenApiFeign", url= "${openapi.url}", configuration = FeignSSLConfiguration.class)
public interface OpenApiFeign {
@GetMapping(value = "/security/open/out/demo/queryGetDemo", produces = "application/json;charset=utf-8")
ResultBody<OpenDemo> queryGetDemo(OpenDemo demo);
@PostMapping(value = "/security/open/out/demo/queryPostDemo", produces = "application/json;charset=utf-8")
ResultBody<OpenDemo> queryPostDemo(OpenDemo demo);
}
- feign client配置, 調(diào)用https接口時(shí)繞過SSL證書驗(yàn)證
import feign.Client;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory;
import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient;
import org.springframework.context.annotation.Bean;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
/**
* @Author: huangyibo
* @Date: 2023/1/6 17:49
* @Description: feign client配置, 調(diào)用https接口時(shí)繞過SSL證書驗(yàn)證
*/
public class FeignSSLConfiguration {
@Bean
public CachingSpringLoadBalancerFactory cachingFactory(SpringClientFactory clientFactory) {
return new CachingSpringLoadBalancerFactory(clientFactory);
}
/**
* 調(diào)用https接口時(shí)繞過SSL證書驗(yàn)證
* @param cachingFactory
* @param clientFactory
* @return
* @throws NoSuchAlgorithmException
* @throws KeyManagementException
*/
@Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) throws NoSuchAlgorithmException, KeyManagementException {
SSLContext ctx = SSLContext.getInstance("TLSv1.2");
X509TrustManager tm = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
ctx.init(null, new TrustManager[]{tm}, null);
return new LoadBalancerFeignClient(new Client.Default(ctx.getSocketFactory(),
(hostname, session) -> true),
cachingFactory, clientFactory);
}
}
- feign請(qǐng)求參數(shù)攔截器
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import java.util.Map;
import java.util.TreeMap;
/**
* @Author: huangyibo
* @Date: 2023/1/6 17:57
* @Description: feign請(qǐng)求參數(shù)攔截器
*/
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
private static final Logger logger = LoggerFactory.getLogger(FeignRequestInterceptor.class);
@Value("${openapi.appId}")
private String appId;
@Value("${openapi.securityKey}")
private String securityKey;
@Value("${openapi.version}")
private String version;
private static final String KFANG_PRICE_OPEN_URL = "/security/open/out";
public static final String OPEN_API_SIGN_HEADER = "Sign";
@Override
public void apply(RequestTemplate requestTemplate) {
String url = requestTemplate.url();
if (url.contains(KFANG_PRICE_OPEN_URL)) {
//獲取請(qǐng)求體
byte[] bodyBytes = requestTemplate.body();
Map paramMap = new TreeMap<>();
try {
String body = new String(bodyBytes, requestTemplate.requestCharset() == null ? "utf-8": requestTemplate.requestCharset().name());
paramMap = JSONObject.parseObject(body, TreeMap.class);
paramMap.put("appId", appId);
if (HttpMethod.GET.name().equals(requestTemplate.method())) {
paramMap.forEach((key, value) -> {
if(!StringUtils.isEmpty(value)){
// 將body的參數(shù)寫入queries
requestTemplate.query(String.valueOf(key), String.valueOf(value));
}
});
}else {
//POST設(shè)置請(qǐng)求體
requestTemplate.body(JSON.toJSONString(paramMap));
}
StringBuilder stringBuilder = new StringBuilder();
paramMap.forEach((key, value) -> {
if(!StringUtils.isEmpty(value)){
stringBuilder.append(value);
}
});
stringBuilder.append(securityKey);
String sign = DigestUtils.md5DigestAsHex(stringBuilder.toString().getBytes()).toUpperCase();
requestTemplate.header(OPEN_API_SIGN_HEADER, sign);
requestTemplate.header("Content-Type", "application/json;charset=utf-8");
} catch (Exception e) {
logger.error("feign參數(shù)攔截, 添加接口簽名異常冰抢, url:{}", url, e);
}
}
}
}
- HttpContextUtils
public class HttpContextUtils {
/**
* 獲取query參數(shù)
* @param request
* @return
*/
public static Map<String, String> getParameterMapAll(HttpServletRequest request) {
Enumeration<String> parameters = request.getParameterNames();
Map<String, String> params = new HashMap<>();
while (parameters.hasMoreElements()) {
String parameter = parameters.nextElement();
String value = request.getParameter(parameter);
params.put(parameter, value);
}
return params;
}
/**
* 獲取請(qǐng)求Body
* @param request
* @return
* @throws Exception
*/
public Map<String, Object> parsePostBodyToMap(HttpServletRequest request) throws Exception {
StringBuilder sb = new StringBuilder();
String line;
BufferedReader reader = request.getReader();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
String body = sb.toString();
return JSON.parseObject(body);
// 這里使用Jackson進(jìn)行轉(zhuǎn)換,確保項(xiàng)目中已經(jīng)引入了Jackson依賴
/*ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(body, new TypeReference<Map<String, Object>>(){});*/
}
}
參考:
https://docker.blog.csdn.net/article/details/103140515
https://www.cnblogs.com/owenma/p/11419341.html
https://www.cnblogs.com/yaoyu1983/p/12267809.html
https://blog.csdn.net/wjg8209/article/details/118806853
https://www.cnblogs.com/kevin-ying/p/10800934.html
https://www.jb51.net/article/239665.htm