開放api接口平臺(tái):appid、appkey乡翅、appsecret

一鳞疲、什么是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

https://www.jb51.net/article/239939.htm

https://blog.csdn.net/yaomingyang/article/details/108246334

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末雄嚣,一起剝皮案震驚了整個(gè)濱河市晒屎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌缓升,老刑警劉巖鼓鲁,帶你破解...
    沈念sama閱讀 217,084評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異港谊,居然都是意外死亡骇吭,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門歧寺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來燥狰,“玉大人,你說我怎么就攤上這事斜筐×拢” “怎么了?”我有些...
    開封第一講書人閱讀 163,450評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵顷链,是天一觀的道長目代。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么榛了? 我笑而不...
    開封第一講書人閱讀 58,322評(píng)論 1 293
  • 正文 為了忘掉前任在讶,我火速辦了婚禮,結(jié)果婚禮上霜大,老公的妹妹穿的比我還像新娘构哺。我一直安慰自己,他們只是感情好战坤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,370評(píng)論 6 390
  • 文/花漫 我一把揭開白布曙强。 她就那樣靜靜地躺著,像睡著了一般湖笨。 火紅的嫁衣襯著肌膚如雪旗扑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,274評(píng)論 1 300
  • 那天慈省,我揣著相機(jī)與錄音臀防,去河邊找鬼。 笑死边败,一個(gè)胖子當(dāng)著我的面吹牛袱衷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播笑窜,決...
    沈念sama閱讀 40,126評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼致燥,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了排截?” 一聲冷哼從身側(cè)響起嫌蚤,我...
    開封第一講書人閱讀 38,980評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎断傲,沒想到半個(gè)月后脱吱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,414評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡认罩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,599評(píng)論 3 334
  • 正文 我和宋清朗相戀三年箱蝠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片垦垂。...
    茶點(diǎn)故事閱讀 39,773評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡宦搬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出劫拗,到底是詐尸還是另有隱情间校,我是刑警寧澤,帶...
    沈念sama閱讀 35,470評(píng)論 5 344
  • 正文 年R本政府宣布页慷,位于F島的核電站撇簿,受9級(jí)特大地震影響聂渊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜四瘫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,080評(píng)論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望欲逃。 院中可真熱鬧找蜜,春花似錦、人聲如沸稳析。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽彰居。三九已至诚纸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間陈惰,已是汗流浹背畦徘。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留抬闯,地道東北人井辆。 一個(gè)月前我還...
    沈念sama閱讀 47,865評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像溶握,于是被迫代替她去往敵國和親杯缺。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,689評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容