首先讲一下使用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.后端配置
先画张图来解释一下什么是前端配置什么是后端配置
具体配置细节去看官方文档:https://help.aliyun.com/document_detail/48805.html?spm=5176.doc48777.6.546.490Zm0
二.管理API
如何对已经配置好的API进行管理呢,我一句两句也讲不清楚直接看官方文档吧:https://help.aliyun.com/document_detail/29480.html?spm=5176.doc48805.6.574.P7fymx
三.调用API,这是我今天重点讲的模块
步骤:
1.创建APP
如何创建APP
首先登陆阿里云,找到控制台API网关,点击应用管理,点击右上角创建APP,然后填写APP信息,点击确定。
2.授权
首先,查看创建的APPID
然后找到API对这个APP进行授权,如果API属于自己,直接找到API进行授权,
PS:如果你是第三方需要把,APPID告诉API持有者公司,告知给予授权(一般都会有开发文档的)
3.调用API
调用示例:
官方文档调用示例:https://help.aliyun.com/document_detail/29490.html?spm=5176.doc29480.6.554.7HhxYr
我整理过后的示例:
使用的库:
//网络请求相关 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. 首先要对请求进行签名
SignUtil类//进行签名 String clientSign = SignUtil.getSign(request); Log.e(TAG, "intercept clientSign: " + clientSign);
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 yrmao9893@163.com * @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);
以上就是核心代码了,好了本篇文章的内容就结束了。
github地址:https://git.oschina.net/yrmao/api-connect-call-example.git