1. 程式人生 > >阿里雲API閘道器配置詳解

阿里雲API閘道器配置詳解

首先講一下使用API閘道器的原因:

       我想很多公司都因API或開放API的安全性感到苦惱吧,大部分公司都會自己的API進行加密處理,或token驗證,可這就能防範,其他人抓取介面進行非法操作了嗎?答案是肯定的,不能。他人可能不能破解你的加密方式,或token驗證方式,但他不管這些,他就是專門搞破壞,進行重放攻擊,頻繁的傳送請求,造成伺服器的負荷。還有一些公司的API根本沒有做加密驗證和token,就相當於裸奔的伺服器,任何人只要抓取介面就就可以呼叫API,這非常危險。這些都是我們要使用API閘道器,API閘道器可以防止這些漏洞。

阿里雲API閘道器能幹什麼:

     

它基本上能幹有關我們遇到所有問題。所有我們很有必要整合阿里雲API閘道器。

OK我們開始進入正題,如何配置API的

我相信看到這篇文章的童鞋,應該都是看過阿里雲網關的接入文件的。難點應該是對於簽名這塊,我會在文章的末尾放出demo,(Java和android版本的 PS:ios我不會))

 我將阿里雲API接入文件整理了一下,大致分為3個模組

   一:配置API

   二:管理API

   三:呼叫API

我主要講的模組是如何呼叫API,當然其他兩個模組我也會簡單的描述一下。

一:配置API

       1.前端配置

       2.後端配置

先畫張圖來解釋一下什麼是前端配置什麼是後端配置


二.管理API

三.呼叫API,這是我今天重點講的模組

     步驟:

     1.建立APP

         如何建立APP

         

          首先登陸阿里雲,找到控制檯API閘道器,點選應用管理,點選右上角建立APP,然後填寫APP資訊,點選確定。

     2.授權

              首先,檢視建立的APPID

              

             然後找到API對這個APP進行授權,如果API屬於自己,直接找到API進行授權, 

            PS:如果你是第三方需要把,APPID告訴API持有者公司,告知給予授權(一般都會有開發文件的)


     3.呼叫API

     呼叫示例:

     我整理過後的示例:

      使用的庫:

    //網路請求相關
    compile 'io.reactivex.rxjava2:rxjava:2.0.7'
    compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
    compile 'com.squareup.retrofit2:retrofit:2.1.0'
    compile 'com.squareup.retrofit2:adapter-rxjava2:2.2.0'
    compile 'com.squareup.retrofit2:converter-gson:2.1.0'
    compile 'com.squareup.okhttp3:okhttp:3.5.0'
    compile 'com.squareup.okhttp3:logging-interceptor:3.6.0'
    compile 'com.squareup.okio:okio:1.11.0'
    compile 'com.umeng.analytics:analytics:latest.integration'

    //簽名相關
    compile('org.bitbucket.b_c:jose4j:0.4.1')
    compile('commons-io:commons-io:2.5')
    compile('org.apache.directory.studio:org.apache.commons.codec:1.8')


      以下操作程式碼都是在OKhttp3攔截器裡實現的(還沒有使用過OKhttp的童鞋建議去Google或百度瞭解一下)

      1.填寫頭部資訊

          實現方式是攔截器,攔截請求資訊,對其進行設定然後繼續請求

 long mTimestamp = System.currentTimeMillis();
        Request request = oldrequest.newBuilder()
                .addHeader("Host", "apis.80ct.com")
                .addHeader("Date", CommonUtil.dateFormat(mTimestamp))
                .addHeader("User-Agent", "Apache-HttpClient/4.1.2 (java 1.6)")
                .addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")////請求體型別,請根據實際請求體內容設定。
                .addHeader("Accept", "application/json")////請求響應體型別,部分 API 可以根據指定的響應型別來返回對應資料格式,建議手動指定此請求頭,如果不設定,部分 HTTP 客戶端會設定預設值 */*,導致簽名錯誤。
                .addHeader("X-Ca-Request-Mode", "debug")////是否開啟 Debug 模式,大小寫不敏感,不設定預設關閉,一般 API 除錯階段可以開啟此設定。
                .addHeader("X-Ca-Version", "1")//API版本號,為日期形式:YYYY-MM-DD,本版本對應為2016-07-14
                .addHeader("X-Ca-Signature-Headers", "X-Ca-Request-Mode,X-Ca-Version,X-Ca-Stage,X-Ca-Key,X-Ca-Timestamp")////參與簽名的自定義請求頭,服務端將根據此配置讀取請求頭進行簽名,此處設定不包含 Content-Type、Accept、Content-MD5、Date 請求頭,這些請求頭已經包含在了基礎的簽名結構中,詳情參照請求籤名說明文件。
                .addHeader("X-Ca-Stage", "RELEASE")////請求 API的Stage,目前支援 TEST、PRE、RELEASE 三個 Stage,大小寫不敏感,API 提供者可以選擇釋出到哪個 Stage,只有釋出到指定 Stage 後 API 才可以呼叫,否則會提示 API 找不到或 Invalid Url。
                .addHeader("X-Ca-Key", HttpConfig.APPKEY)////請求的 AppKey,請到 API 閘道器控制檯生成,只有獲得 API 授權後才可以呼叫,通過雲市場等渠道購買的 API 預設已經給APP授過權,阿里雲所有云產品共用一套 AppKey 體系,刪除 ApppKey 請謹慎,避免影響到其他已經開通服務的雲產品。
                .addHeader("X-Ca-Timestamp", mTimestamp + "")
                .addHeader("X-Ca-Nonce", CommonUtil.getRandom() + "")////請求唯一標識,15分鐘內 AppKey+API+Nonce 不能重複,與時間戳結合使用才能起到防重放作用。
                .build();


      2. 首先要對請求進行簽名

//進行簽名
        String clientSign = SignUtil.getSign(request);
        Log.e(TAG, "intercept clientSign: " + clientSign);
       SignUtil類
package com.sk.openapicallexample_android.http.sign;

import com.sk.openapicallexample_android.http.config.HttpConfig;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import okhttp3.Headers;
import okhttp3.Request;

/**
 * @Author yemao
 * @Email [email protected]
 * @Date 2017/5/19
 * @Des null!
 */

public class SignUtil {

    //所有參與簽名的header的key
    private static String CA_PROXY_SIGN_HEADERS = "X-Ca-Signature-Headers";
    //簽名方式
    private static String SHA256 = "HmacSHA256";
    //HTTP POST
    private static final String HTTP_METHOD_POST = "POST";
    //HTTP PUT
    private static final String HTTP_METHOD_PUT = "PUT";
    private static String LF = "\n";



    /***
     * 將字串簽名
     * 簽名格式 HmacSHA256
     *
     * @param request okHttp Request
     * @return 返回簽名後的字串
     * @throws Exception
     */
    public static String getSign(Request request) {
        String buildToSign = buildStringToSign(request);
        String sign = "";
        try {
            sign = getSign(buildToSign);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return sign;
    }
    /***
     * 將字串簽名
     * 簽名格式 HmacSHA256
     *
     * @param stringToSign 拼接好的字串
     * @return 返回簽名後的字串
     * @throws Exception
     */
    public static String getSign(String stringToSign) throws Exception {
        Mac hmacSha256 = Mac.getInstance(SHA256);
        byte[] keyBytes = HttpConfig.SECRET.getBytes("UTF-8");
        hmacSha256.init(new SecretKeySpec(keyBytes, 0, keyBytes.length, SHA256));
        return new String(Base64.encodeBase64(hmacSha256.doFinal(stringToSign.getBytes("UTF-8"))), "UTF-8");
    }

    /***
     * 組織等待簽名的字串
     * 按以下規則排序拼接
     *  String stringToSign=HTTPMethod + "\n" +Accept + "\n" + Content-MD5 + "\n"Content-Type + "\n" +Date + "\n" +Headers +Url
     *
     *
     * @param mMethedType 請求的型別
     * @param mAccept Accept頭的value
     * @param mContent_Md5  Content-MD5 是指 Body 的 MD5 值,只有當 Body 非 Form 表單時才計算 MD5,
     * @param mContent_Type Content_Type頭的value
     * @param mDate Date頭的value
     * @param mStringHeaders 參與簽名的頭
     * @param mUrl Url 指 Path + Query + Body 中 Form 引數,組織方法:對 Query+Form 引數按照字典對 Key 進行排序後按照如下方法拼接,如果 Query 或 Form 引數為空,則 Url = Path,不需要新增 ?,如果某個引數的 Value 為空只保留 Key 參與簽名,等號不需要再加入簽名。
     * @return
     */

    public static String organizationStringToSign(String mMethedType, String mAccept,
                                                  String mContent_Md5,
                                                  String mContent_Type,
                                                  String mDate,
                                                  String mStringHeaders,
                                                  String mUrl) {
        StringBuilder sb = new StringBuilder();
        sb.append(mMethedType).append(LF);

        sb.append(mAccept).append(LF);

        if (mContent_Md5 != null) {
            sb.append(mContent_Md5);
        }
        sb.append(LF);

        sb.append(mContent_Type).append(LF);

        sb.append(mDate).append(LF);

        sb.append(mStringHeaders);
        sb.append(mUrl);
        return sb.toString();

    }

    public static String buildStringToSign(Request request) {


        Headers headers = request.headers();
        String mMethod = request.method();

        String mAccept = headers.get("Accept");

        byte[] inputStreamBytes = new byte[]{};


        String mContent_Md5 = null;
        try {
            mContent_Md5 = buildBodyMd5(mMethod, inputStreamBytes);
        } catch (Exception e) {
            e.printStackTrace();
        }

        String mContent_Type = headers.get("Content-Type");

        String mDate = headers.get("Date");

        //Headers
        Map<String, String> hedersToSign = buildHeadersToSign(headers);
        String headersString = buildHeaders(hedersToSign);

        String mUrl = request.url().url().getPath();

        if (request.url().query() != null) {
            mUrl += "?" + request.url().query();
        }

        return SignUtil.organizationStringToSign(mMethod, mAccept, mContent_Md5, mContent_Type, mDate, headersString, mUrl);
    }


    /**
     * 組織Headers簽名簽名字串
     *
     * @param headers HTTP請求頭
     * @return Headers簽名簽名字串
     */
    private static String buildHeaders(Map<String, String> headers) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> e : headers.entrySet()) {
            if (e.getValue() != null) {
                sb.append(e.getKey()).append(':').append(e.getValue()).append(LF);
            }
        }
        return sb.toString();
    }

    /**
     * 構建參與簽名的HTTP頭
     * <pre>
     * 傳入的Headers必須將預設的ISO-8859-1轉換為UTF-8以支援中文
     * </pre>
     *
     * @param headers HTTP請求頭
     * @return 所有參與簽名計算的HTTP請求頭
     */
    private static Map<String, String> buildHeadersToSign(Headers headers) {
        Map<String, String> headersToSignMap = new TreeMap<>();

        String headersToSignString = headers.get(CA_PROXY_SIGN_HEADERS);

        if (headersToSignString != null) {
            for (String headerKey : headersToSignString.split("\\,")) {
                headersToSignMap.put(headerKey, headers.get(headerKey));
            }
        }

        return headersToSignMap;
    }

    /**
     * 構建BodyMd5
     *
     * @param httpMethod       HTTP請求方法
     * @param inputStreamBytes HTTP請求Body體位元組陣列
     * @return Body Md5值
     * @throws IOException
     */
    private static String buildBodyMd5(String httpMethod, byte[] inputStreamBytes) throws Exception {
        if (inputStreamBytes == null) {
            return null;
        }

        if (!httpMethod.equalsIgnoreCase(HTTP_METHOD_POST) && !httpMethod.equalsIgnoreCase(HTTP_METHOD_PUT)) {
            return null;
        }

        InputStream inputStream = new ByteArrayInputStream(inputStreamBytes);
        byte[] bodyBytes = IOUtils.toByteArray(inputStream);
        if (bodyBytes != null && bodyBytes.length > 0) {
            return base64AndMD5(bodyBytes).trim();
        }
        return null;
    }

    /**
     * 先進行MD5摘要再進行Base64編碼獲取摘要字串
     *
     * @param bytes 待計算位元組陣列
     * @return
     */
    public static String base64AndMD5(byte[] bytes) throws Exception {
        if (bytes == null) {
            throw new IllegalArgumentException("bytes can not be null");
        }

        try {
            final MessageDigest md = MessageDigest.getInstance("MD5");
            md.reset();
            md.update(bytes);
            final Base64 base64 = new Base64();

            return new String(base64.encode(md.digest()));
        } catch (final NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("unknown algorithm MD5");
        }
    }

    /**
     * 組織Uri+請求引數的簽名字串
     * Url 指 Path + Query + Body 中 Form 引數,組織方法:對 Query+Form 引數按照字典對 Key 進行排序後按照如下方法拼接,如果 Query 或 Form 引數為空,則 Url = Path,不需要新增 ?,如果某個引數的 Value 為空只保留 Key 參與簽名,等號不需要再加入簽名。
     *
     * @param url       HTTP請求url,不包含Query
     * @param paramsMap HTTP請求所有引數(Query+Form引數)
     * @return Uri+請求引數的簽名字串
     */
    private static String buildResource(String url, Map<String, Object> paramsMap) {
        StringBuilder builder = new StringBuilder();

        // url
        builder.append(url);

        if (paramsMap == null)
            return builder.toString();
        // Query+Form
        TreeMap<String, Object> sortMap = new TreeMap<String, Object>();
        sortMap.putAll(paramsMap);


        // 有Query+Form引數
        if (sortMap.size() > 0) {
            builder.append('?');
            builder.append(buildMapToSign(sortMap));
        }

        return builder.toString();
    }

    /**
     * 將Map轉換為用&及=拼接的字串
     */
    private static String buildMapToSign(Map<String, Object> paramMap) {
        StringBuilder builder = new StringBuilder();

        for (Map.Entry<String, Object> e : paramMap.entrySet()) {
            if (builder.length() > 0) {
                builder.append('&');
            }

            String key = e.getKey();
            Object value = e.getValue();

            if (value != null) {
                if (value instanceof List) {
                    List list = (List) value;
                    if (list.size() == 0) {
                        builder.append(key);
                    } else {
                        builder.append(key).append("=").append(String.valueOf(list.get(0)));
                    }
                } else if (value instanceof Object[]) {
                    Object[] objs = (Object[]) value;
                    if (objs.length == 0) {
                        builder.append(key);
                    } else {
                        builder.append(key).append("=").append(String.valueOf(objs[0]));
                    }
                } else {
                    builder.append(key).append("=").append(String.valueOf(value));
                }
            }
        }

        return builder.toString();
    }
}

      3.對簽名結果填入頭部     

request = request.newBuilder()
                .addHeader(X_CA_SIFNATURE_KEY, clientSign)
                .build();

然後貼一個配置類HttpConfig:
public class HttpConfig {
    //網路請求時間
    public static int TIME = 30000;
    //base地址
    public static String BASE_URL = "http://apis.80ct.com";//http://apitest.1cno.com/
    //這裡uuid是表示我們公司API身份驗證的識別符號你可以忽略
    public static String UUID = "你自己的uuid";
    //自己的阿里雲APPkey
    public static  String APPKEY="你自己的APPkey";
    //簽名祕鑰
    public static String SECRET = "你自己的app ecret";


}

如果安全認證方式為OpenID Connect方式還需要新增兩個步驟:

  1.授權介面,獲取token

    /**
     * 獲取token
     *
     * @param chain
     * @return
     * @throws Exception
     */
    private static String refreshToken(Interceptor.Chain chain) throws IOException {

        //經過特殊處理的授權介面
        retrofit2.Call<String> mCall = RetrofitFactory.getInstence().API().authorization1(HttpConfig.UUID);
        Request request = buildRequestToAddHeads(mCall.request());
        Response response = chain.proceed(request);
        //得到響應體
        ResponseBody responseBody = response.body();

        //得到緩衝源
        BufferedSource source = responseBody.source();

        //請求全部
        source.request(Long.MAX_VALUE); // Buffer the entire body.
        Buffer buffer = source.buffer();
        Charset charset = UTF8;

        MediaType contentType = responseBody.contentType();

        if (contentType != null) {
            charset = contentType.charset(UTF8);
        }
        //讀取返回資料
        String bodyString = buffer.clone().readString(charset);

        //解析返回資料
        BaseEntity<String> mBaseEntity = null;
        try {
            if (bodyString != null) {
                Gson gson = new Gson();
                mBaseEntity = gson.fromJson(bodyString, new TypeToken<BaseEntity<String>>() {
                }.getType());
            }
        } catch (Exception e) {
            e.printStackTrace();

        }

        return mBaseEntity == null ? null : mBaseEntity.getData();
    }

  2.token失效處理,重新獲取token

                    Request request = buildRequestToAddHeads(chain.request());
                    Response response = chain.proceed(request);

                    //token失效重新獲取token
                    if (response.code() == 401) {

                        //當token為空時去獲取token
                        token = refreshToken(chain) + "";
                        request = chain.request();
                        request = request.newBuilder()
                                .removeHeader(TOKEN_KEY)
                                .addHeader(TOKEN_KEY, token.trim().toString())
                                .build();
                        request = buildRequestToAddHeads(request);

                        return chain.proceed(request);


以上就是核心程式碼了,好了本篇文章的內容就結束了。