開放api接口平臺:appid、appkey、appsecret

一、什么是appid、appkey、appsecret

AppID:應用的唯一標識。
AppKey:公匙(相當于賬號)。
AppSecret:私匙(相當于密碼)

token:令牌(過期失效)

  • app_id:是用來標記你的開發者賬號的,是你的用戶id,這個id 在數據庫添加檢索,方便快速查找。

  • app_key 和 app_secret 是一對出現的賬號,同一個 app_id 可以對應多個 app_key+app_secret,這樣平臺就可以分配你不一樣的權限,比如 app_key1 + app_secect1 只有只讀權限 但是 app_key2+app_secret2 有讀寫權限......,這樣你就可以把對應的權限放給不同的開發者,其中權限的配置都是直接跟app_key 做關聯的,app_key 也需要添加數據庫檢索,方便快速查找。

  • 至于為什么 要有app_key + app_secret 這種成對出現的機制呢,因為 要加密,通常 在首次驗證(類似登錄場景) ,你需要用 app_key(標記要申請的權限有哪些) + app_secret(密碼,表示你真的擁有這個權限) 來申請一個token,就是我們經常用到的 access_token,之后的數據請求,就直接提供access_token 就可以驗證權限了。

簡化的場景

  • 1、省去 app_id,他默認每一個用戶有且僅有一套權限配置,所以直接將 app_id = app_key,然后外加一個app_secret就夠了。
  • 2、省去app_id 和 app_key,相當于 app_id = app_key = app_secret,通常用于開放性接口的地方,特別是很多地圖類api 都采用這種模式,這種模式下,帶上app_id 的目的僅僅是統計 某一個用戶調用接口的次數而已了。

使用方法

  • 1、向第三方服務器請求授權時,帶上AppKey和AppSecret(需存在服務器端)
  • 2、第三方服務器驗證AppKey和AppSecret在DB中有無記錄
  • 3、如果有,生成一串唯一的字符串(token令牌),返回給服務器,服務器再返回給客戶端
  • 4、客戶端下次請求敏感數據時帶上令牌

二、云服務AppId或AppKey和AppSecret生成策略

App key簡稱API接口驗證序號,是用于驗證API接入合法性的。接入哪個網站的API接口,就需要這個網站允許才能夠接入,如果簡單比喻的話:可以理解成是登陸網站的用戶名。

App Secret簡稱API接口密鑰,是跟App Key配套使用的,可以簡單理解成是密碼。

App Key 和 App Secret 配合在一起,通過其他網站的協議要求,就可以接入API接口調用或使用API提供的各種功能和數據。

比如淘寶聯盟的API接口,就是淘寶客網站開發的必要接入,淘客程序通過API接口直接對淘寶聯盟的數據庫調用近億商品實時數據。做到了輕松維護,自動更新。

2.1 UUID

UUID是指在一臺機器在同一時間中生成的數字在所有機器中都是唯一的。按照開放軟件基金會(OSF)制定的標準計算,用到了以太網卡地址、納秒級時間、芯片ID碼和許多可能的數字
UUID由以下幾部分的組合:

  • 1、當前日期和時間。
  • 2、時鐘序列。
  • 3、全局唯一的IEEE機器識別號,如果有網卡,從網卡MAC地址獲得,沒有網卡以其他方式獲得。

標準的UUID格式為:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12),以連字號分為五段形式的36個字符,示例:550e8400-e29b-41d4-a716-446655440000

Java標準類庫中已經提供了UUID的API。

UUID.randomUUID()

2.2 代碼實現

  • AppSecret 使用SHA-1生成20位byte數組,基本很難重復,再轉化為40位16進制數字字符串。
/**
 * @author: huangyibo
 * @Date: 2022/6/15 16:17
 * @Description: AppSecret 使用SHA-1生成20位byte數組,基本很難重復,再轉化為40位16進制數字字符串。
 */

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思想其實借鑒微博短域名的生成方式,但是其重復概率過高,而且每次生成4個,需要隨即選取一個。
     * 本算法利用62個可打印字符,通過隨機生成32位UUID,由于UUID都為十六進制,所以將UUID分成8組,每4個為一組,然后通過模62操作,結果作為索引取出字符,
     * 這樣重復率大大降低。
     * 經測試,在生成一千萬個數據也沒有出現重復,完全滿足大部分需求。
     * </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和內置關鍵詞生成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數組,基本很難重復,再轉化為64位16進制數字字符串。
/**
 * @author: huangyibo
 * @Date: 2022/6/30 16:36
 * @Description: AppSecret使用sha-256生成32位byte數組,基本很難重復,再轉化為64位16進制數字字符串。
 */

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思想其實借鑒微博短域名的生成方式,但是其重復概率過高,而且每次生成4個,需要隨即選取一個。
     * 本算法利用62個可打印字符,通過隨機生成32位UUID,由于UUID都為十六進制,所以將UUID分成8組,每4個為一組,然后通過模62操作,結果作為索引取出字符,
     * 這樣重復率大大降低。
     * 經測試,在生成一千萬個數據也沒有出現重復,完全滿足大部分需求。
     * </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和內置關鍵詞生成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得到一位的進行補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 接口開發安全性

接口的安全性主要圍繞token、timestamp和sign三個機制展開設計,保證接口的數據不會被篡改和重復調用。

在代碼層面,對接口進行安全設計

  • 1、使用token進行用戶身份認證
  • 2、使用sign防止傳入參數被篡改
  • 3、使用timestamp時間戳防止暴力請求

3.1 使用token進行用戶身份認證授權

具體說明如下:

  • 1、 用戶登錄時,客戶端請求接口,傳入用戶名和密文的密碼
  • 2、 后臺服務對用戶身份進行驗證。若驗證失敗,則返回錯誤結果;若驗證通過,則生成一個隨機不重復的token(可以是UUID),并將其存儲在redis中,設置一個過期時間。
    • 其中,redis的key為token,value為驗證通過后獲得的用戶信息
  • 3、 用戶身份校驗通過后,后臺服務將生成的token返回客戶端。
    • 客戶端請求后續其他接口時,需要帶上這個token。后臺服務會統一攔截接口請求,進行token有效性校驗,并從中獲取用戶信息,供后續業務邏輯使用,Token是客戶端訪問服務端的憑證。

3.2 使用sign防止傳入參數被篡改

為了防止中間人攻擊(客戶端發來的請求被第三方攔截篡改),引入參數的簽名機制。

  • 1、客戶端和服務端約定一個加密算法(MD5或SHA-1算法(可根據情況加點鹽)), 客戶端發起請求時,將所有的非空參數按升序拼在一起,通過加密算法形成一個sign,將其放在請求頭中傳遞給后端服務。
  • 2、后端服務統一攔截接口請求,用接收到的非可空參數根據約定好的規則進行加密,和傳入的sign值進行比較。若一致則予以放行,不一致則拒絕請求。

由于中間人不知道加密方法,也就不能偽造一個有效的sign。從而防止了中間人對請求參數的篡改。

3.3 用時間戳防止暴力請求

時間戳超時機制
用戶每次請求都帶上當前時間的時間戳timestamp,服務端接收到timestamp后跟當前時間進行比對,如果時間差大于一定時間(比如5分鐘),則認為該請求失效。時間戳超時機制是防御DOS攻擊的有效手段。

sign機制可以防止參數被篡改,但無法防dos攻擊(第三方使用正確的參數,不停請求服務器,使之無法正常提供服務)。因此,還需要引入時間戳機制。

具體的操作為:

  • 客戶端在形成sign值時,除了使用所有參數和token外,再加一個發起請求時的時間戳。即 sign值來源 = 所有非空參數升序排序+token+timestamp

  • 而服務端則需要根據當前時間和sign值的時間戳進行比較,差值超過一段時間則不予放行。

  • 若要求不高,則客戶端和服務端可以僅僅使用精確到秒或分鐘的時間戳,據此形成sign值來校驗有效性。這樣可以使一秒或一分鐘內的請求是有效的。

  • 若要求較高,則還需要約定一個解密算法,使后端服務可以從sign值中解析出發起請求的時間戳。

總結后的流程圖如下:


3.4 拒絕重復調用(非必須)

客戶端第一次訪問時,將簽名sign存放到緩存服務器中,超時時間設定為跟時間戳的超時時間一致,二者時間一致可以保證無論在timestamp限定時間內還是外 URL都只能訪問一次。如果有人使用同一個URL再次訪問,如果發現緩存服務器中已經存在了本次簽名,則拒絕服務。如果在緩存中的簽名失效的情況下,有人使用同一個URL再次訪問,則會被時間戳超時機制攔截。這就是為什么要求時間戳的超時時間要設定為跟時間戳的超時時間一致。拒絕重復調用機制確保URL被別人截獲了也無法使用(如抓取數據)。

在以上三種機制的保護下,如果有人劫持了請求,并對請求中的參數進行了修改,簽名就無法通過;

如果有人使用已經劫持的URL進行DOS攻擊,服務器則會因為緩存服務器中已經存在簽名或時間戳超時而拒絕服務,所以DOS攻擊也是不可能的;

所有的安全措施都用上的話有時候難免太過復雜,在實際項目中需要根據自身情況作出裁剪,比如可以只使用簽名機制就可以保證信息不會被篡改,或者定向提供服務的時候只用Token機制就可以了。如何裁剪,全看項目實際情況和對接口安全性的要求。

四、基于AccessToken方式實現API設計

需求:

  • A、B機構需要調用X服務器的接口,那么X服務器就需要提供開放的外網訪問接口。

分析:

  • 1、開放平臺提供者X,為每一個合作機構提供對應的appid、app_secret。
  • 2、appid是唯一的(不能改變),表示對應的第三方合作機構,用來區分不同機構的。
  • 3、app_secret在傳輸中實現加密功能(秘鑰),該秘鑰可以發生改變的。
  • 4、為什么app_secret是可以改變的?調用接口需要appid+app_secret生成對應的access_token(臨時性),如果appid和app_secret被泄密,產生安全性問題,如果一但發現被泄密,可以重新生成一個app_secret。

原理:為每個合作機構創建對應的appid、app_secret,生成對應的access_token(有效期2小時),在調用外網開放接口的時候,必須傳遞有效的access_token。

4.1 開發步驟

4.1.1、使用appid+app_secret生成對應的access_token

  • 1、獲取生成的AppId和appSecret,并驗證是否可用
  • 2、刪除之前的accessToken
  • 3、AppId和appSecret保證生成對應唯一的accessToken
    • 注意:以上第二步必須保證在同一事務中
  • 4、返回最新的accessToken

4.1.2、使用accessToken調用第三方接口

  • 1、獲取對應的accessToken
  • 2、使用AccessToken查詢redis對應的value(appId)
  • 3、如果沒有獲取到對應的appid,直接返回錯誤提示
  • 4、如果能獲取到對應的appid,使用appid查詢對應的APP信息
  • 5、使用appId查詢數據庫app信息,獲取is_flag狀態,如果為1,則不能調用接口,否則正常執行
  • 6、直接調用接口業務

五、常見問題總結

做API接口,為什么access_token要放在Header頭里傳遞?
如果是OAuth2, 使用 Header傳遞token是屬于規范的一種,Header中有一個Authorization頭專門用于存放認證信息每一次登錄,會生成一個新的Token, 此時舊的token并不會立即失效(取決于該token生成時,設置的失效時間)

六、代碼實現

服務提供方

  • 處理無法重復讀取stream流,使之可以在一個stream流中多次讀取同一個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: 處理無法重復讀取stream流,使之可以在一個stream流中多次讀取同一個request值
 */

public class RequestWrapper extends HttpServletRequestWrapper {

    //參數字節數組
    private byte[] requestBody;

    //Http請求對象
    private HttpServletRequest request;

    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.request = request;
    }

    /**
     * @return
     * @throws IOException
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        /**
         * 每次調用此方法時將數據流中的數據讀取出來,然后再回填到InputStream之中
         * 解決通過@RequestBody和@RequestParam(POST方式)讀取一次后控制器拿不到參數問題
         */
        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()));
    }
}
  • 攔截所有請求過濾器,并將請求類型是HttpServletRequest類型的請求替換為自定義{@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: 攔截所有請求過濾器,并將請求類型是HttpServletRequest類型的請求替換為自定義{@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);
        }
    }
}
  • 商戶鑒權攔截器-TenantAuthInterceptor
/**
 * @Author: huangyibo
 * @Date: 2023/1/3 18:29
 * @Description: 商戶對外開放鑒權攔截器
 */

@Component
@Slf4j
public class TenantAuthInterceptor extends HandlerInterceptorAdapter {

    @Resource
    private RedisUtil redisUtil;

    @Resource
    private SysTenantAppFeign sysTenantAppFeign;

    /**
     * 單次請求timestamp參數過期時間為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 {
            //獲取請求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("商戶接口請求參數時間戳為空, 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("商戶接口單次請求timestamp參數已失效, 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("商戶鑒權攔截校驗, 驗證不通過,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("商戶鑒權攔截校驗, 商戶信息獲取成功, 保存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;
        }

        //校驗用戶是否有權限
        if (!hasAuth(tenantAppRedisVo.getInterfaceCodeList(), auth)) {
            log.error("商戶權限攔截校驗, 接口權限不通過, 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("商戶鑒權成功, appId={}, url={}", appId, request.getRequestURL());
        return Boolean.TRUE;
    }


    /**
     * 獲取Get請求參數
     * @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);
        }
    }


    /**
     * 校驗權限是否匹配
     * @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配置, 調用https接口時繞過SSL證書驗證
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配置, 調用https接口時繞過SSL證書驗證
 */

public class FeignSSLConfiguration {

    @Bean
    public CachingSpringLoadBalancerFactory cachingFactory(SpringClientFactory clientFactory) {
        return new CachingSpringLoadBalancerFactory(clientFactory);
    }

    /**
     * 調用https接口時繞過SSL證書驗證
     * @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請求參數攔截器
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請求參數攔截器
 */

@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)) {
            //獲取請求體
            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的參數寫入queries
                            requestTemplate.query(String.valueOf(key), String.valueOf(value));
                        }
                    });
                }else {
                    //POST設置請求體
                    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參數攔截, 添加接口簽名異常, url:{}", url, e);
            }
        }
    }
}
  • HttpContextUtils
public class HttpContextUtils {

    /**
     * 獲取query參數
     * @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;
    }

    /**
     * 獲取請求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進行轉換,確保項目中已經引入了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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容