背景
随着公司跟chinaCache公司的cdn服务到期,最终选择了微软的Azure Cdn进行合作,因此需要在将原有的接口上进行拓展,支持Azure Cdn接口的支持。
官网对接文档:https://docs.azure.cn/zh-cn/cdn/cdn-api-signature
各位假如直接看官方接口文档进行对接,可能多少会有点懵,因为接口比较多,不知道具体要对接哪几个,以及请求header入参等多少都会有些坑。
如果大家以前公司用的是CHINACACHE公司的cdn产品的话,应该都知道这个对接,是直接通过输入用户名、密码、task的json格式进行传输。而Azure CDN 跟这种方式传输还是蛮大差别的。最近接口对接也已经上线, 我这边整理了自己对接的一些感受,还有遇到过的坑,希望对有需要的朋友有帮助。
步骤
建议可以先去看下官方的对接文档,先大致熟悉下对接流程。
我这边主要需要用到以下几个菜单,大家可以直接参考官网
- CDN API签名机制
- 节点管理-获取订阅下所有节点信息
- 缓存刷新-添加缓存刷新
对接流程也是按照上面几个步骤进行的,下面我会结合代码分别进行介绍
1.CDN API签名机制
这一步主要根据入参条件不同生成不同的签名字符串,签名生成方法封装到AzureSignTool
package com.trendy.ccp.server.cdn.tool;
import java.net.URL;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/****
*
* @ClassName: AzureSignTool
* @Description: 微软生产api签名
* @author youqiang.xiong
* @date 2018年1月10日下午5:30:35
*
*/
publicclass AzureSignTool {
/**
* Calculate the authorization header
*
* @param requestURL Complete request URL with scheme, host, path and queries
* @param requestTime UTC request time with format "yyyy-MM-dd hh:mm:ss"
* @param keyID API key ID
* @param keyValue API key value
* @param httpMethod HTTP method in upper case
* @return Calculated authorization header
*/
publicstatic String calculateAuthorizationHeader(String requestURL,
String requestTime, String keyID, String keyValue, String httpMethod)
throws Exception {
URL url = new URL(requestURL);
String path = url.getPath();
// Get query parameters
String query = url.getQuery();
String[] params = query.split("&");
Map<String, String> paramMap = new TreeMap<String, String>();
for (String param : params) {
String[] paramterParts = param.split("=");
if (paramterParts.length != 2) {
continue;
}
paramMap.put(paramterParts[0], paramterParts[1]);
}
String orderedQueries = paramMap.entrySet().stream()
.map(entry -> entry.getKey() + ":" + entry.getValue())
.collect(Collectors.joining(", "));
String content = String.format("%s\r\n%s\r\n%s\r\n%s", path,
orderedQueries, requestTime, httpMethod);
Mac sha256HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(keyValue.getBytes(),"HmacSHA256");
sha256HMAC.init(secret_key);
byte[] bytes = sha256HMAC.doFinal(content.getBytes());
StringBuffer hash = new StringBuffer();
for (inti = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
hash.append('0');
}
hash.append(hex);
}
return String.format("AzureCDN %s:%s", keyID, hash.toString()
.toUpperCase());
}
String orderedQueries = paramMap.entrySet().stream()
.map(entry -> entry.getKey() + ":" + entry.getValue())
.collect(Collectors.joining(", "));
注意:以上这段代码采用的jdk1.8的stream语法,需要使用jdk1.8进行编译,如果原来并未使用1.8的话,需要将1.8的语法转化成低版本的语法
Map<String, String> paramMap = new HashMap<String, String>();
paramMap.put("a", "1");
paramMap.put("b", "2");
paramMap.put("c", "3");
paramMap.put("d", "4");
StringBuffer sb = new StringBuffer("");
for(Map.Entry<String, String> entry :paramMap.entrySet()){
sb.append(entry.getKey()+":"+entry.getValue()).append(", ");
}
System.out.println("jdk1.6:" + sb.substring(0,sb.length()-2));
String orderedQueries = paramMap.entrySet().stream()
.map(entry -> entry.getKey() + ":" + entry.getValue())
.collect(Collectors.joining(", "));
System.out.println("jdk1.8:"+orderedQueries);
这里我的测试用例,可以将jdk1.8转换成jdk1.6的语法
打印结果如下所示:
jdk1.6:a:1, b:2, c:3, d:4
jdk1.8:a:1, b:2, c:3, d:4
2.HttpKit请求工具类
因为所有接口的请求都需要通过http封装header信息和body信息发送get和post请求,因此我将get和post的方法进行封装到HttpKit工具类中
package com.trendy.ccp.server.cdn.tool;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.io.IOUtils;
import com.trendy.fw.common.transfer.CharsetGetMethod;
import com.trendy.fw.common.transfer.CharsetPostMethod;
import com.trendy.fw.common.transfer.HttpClientResultBean;
/****
*
* @ClassName: HttpKit
* @Description: 接口请求封装类,目前只提供get和post两个方法
* @author youqiang.xiong
* @date 2018年1月29日下午4:50:10
*
*/
public class HttpKit {
/*****
* 通过http的get方式,获取服务器的数据
* @param url
* @param headerMap
* @param charset
* @return
*/
public static HttpClientResultBean getContext(String url,Map<String, String> headerMap, String charset){
HttpClientResultBean result = new HttpClientResultBean();
HttpClient httpClient = new HttpClient();
CharsetGetMethod getMethod = new CharsetGetMethod(url, charset);
InputStream responseStream = null;
if(headerMap.keySet().size() > 0){
for(Map.Entry<String, String> entry:headerMap.entrySet()){
Header header = new Header();
header.setName(entry.getKey());
header.setValue(entry.getValue());
getMethod.addRequestHeader(header);
}
}
try {
int e = httpClient.executeMethod(getMethod);
if (e == 200) {
responseStream = getMethod.getResponseBodyAsStream();
result.setResult(true);
result.setResultByteContent(IOUtils.toByteArray(responseStream));
result.setResultContent(new String(result.getResultByteContent(), charset));
} else {
responseStream = getMethod.getResponseBodyAsStream();
result.setResult(false);
result.setResultContent(new String(IOUtils.toByteArray(responseStream), charset));
}
} catch (Exception arg15) {
result.setResult(false);
result.setResultContent(arg15.toString());
} finally {
if (responseStream != null) {
try {
responseStream.close();
responseStream = null;
} catch (Exception arg14) {
}
}
if (getMethod != null) {
getMethod.releaseConnection();
getMethod = null;
}
}
return result;
}
/****
* 通过http的post方式,提交数据到服务器
* @param url
* @param headerMap
* @param data
* @param charset
* @return
*/
public static HttpClientResultBean postContent(String url,Map<String, String> headerMap, String data,
String charset) {
HttpClientResultBean result = new HttpClientResultBean();
HttpClient httpClient = new HttpClient();
CharsetPostMethod postMethod = new CharsetPostMethod(url, charset);
if(headerMap.keySet().size() > 0){
for(Map.Entry<String, String> entry:headerMap.entrySet()){
Header header = new Header();
header.setName(entry.getKey());
header.setValue(entry.getValue());
postMethod.addRequestHeader(header);
}
}
postMethod.setRequestEntity(new StringRequestEntity(data));
try {
int e = httpClient.executeMethod(postMethod);
if (e == 200 || e == 202) {
result.setResult(true);
result.setResultByteContent(IOUtils.toByteArray(postMethod
.getResponseBodyAsStream()));
result.setResultContent(new String(result
.getResultByteContent(), charset));
} else {
result.setResult(false);
postMethod.getParams().setParameter(
"http.protocol.content-charset", charset);
}
} catch (HttpException arg11) {
result.setResult(false);
result.setResultContent(arg11.toString());
} catch (IOException arg12) {
result.setResult(false);
result.setResultContent(arg12.toString());
} finally {
if (postMethod != null) {
postMethod.releaseConnection();
postMethod = null;
}
}
return result;
}
}
3.封装请求的实体类
将入参的请求固定格式进行封装MircosoftRequestParam
package com.trendy.ccp.server.cdn.config;
/****
*
* @ClassName: MircosoftRequestParam
* @Description: 微软CDN 接口请求参数固定格式类
* @author youqiang.xiong
* @date 2018年1月12日下午4:13:19
*
*/
publicclass MircosoftRequestParam {
/***
* 获取订阅下所有节点信息,符合yyyy-MM-dd hh:mm:ss格式的UTC当前请求时间
*/
publicstatic String MICROSOFT_CDN_GET_NODES_HEADER1 = "x-azurecdn-request-date";
/****
* 获取订阅下所有节点信息,必填授权
*/
publicstatic String MICROSOFT_CDN_GET_NODES_HEADER2 = "Authorization";
/***
* 添加缓存刷新,符合yyyy-MM-dd hh:mm:ss格式的UTC当前请求时间
*/
publicstatic String MICROSOFT_CDN_CACHE_REFRESH_HEADER1 = "x-azurecdn-request-date";
/****
* 添加缓存刷新,必填授权
*/
publicstatic String MICROSOFT_CDN_CACHE_REFRESH_HEADER2 = "Authorization";
/****
* 添加缓存刷新,必填。application/json
*/
publicstatic String MICROSOFT_CDN_CACHE_REFRESH_HEADER3 = "content-type";
}
4.缓存配置类
新增一个常量定义类MircosoftCacheConfig 配置类
package com.trendy.ccp.server.cdn.config;
import java.util.HashMap;
import java.util.Map;
import com.trendy.fw.common.config.Constants;
import com.trendy.fw.common.util.PropertiesKit;
/****
*
* @ClassName: MircosoftCacheConfig
* @Description: 微软CDN配置
* 1. 读取cdn节点属性配置初始化到hostEndpointMap (域名->节点ID)
* 2. 定义cdn两个接口常量和参数常量等
* @author youqiang.xiong
* @date 2018年1月10日上午11:30:22
*
*/
publicclassMircosoftCacheConfig {
publicstaticfinal String AZURE_PROP_FILE_NAME = Constants.PROP_FILE_PATH + "/azure_cdn_endpoint";
//Microsoft MICROSOFT cdn配置
publicstatic Map<String, String> hostEndpointMap = new HashMap<String, String>();
static{
hostEndpointMap = PropertiesKit.getBundleAllProperties(AZURE_PROP_FILE_NAME);
}
publicstatic Map<String, String> getHostEndpointMap() {
returnhostEndpointMap;
}
//Microsoft MICROSOFT cdn配置
publicstatic String MICROSOFT_CDN_SUB_SCRIPTIONID = "MICROSOFT_CDN_SUB_SCRIPTIONID";
publicstatic String MICROSOFT_CDN_KEY_ID = "MICROSOFT_CDN_KEY_ID";
publicstatic String MICROSOFT_CDN_KEY_VALUE = "MICROSOFT_CDN_KEY_VALUE";
//接口url,主要两个接口获取订阅下所有节点信息、缓存刷新接口
publicstatic String MICROSOFT_CDN_URL_GET_NODES = "https://restapi.cdn.azure.cn/subscriptions/subscriptionId/endpoints?apiVersion=1.0";
publicstatic String MICROSOFT_CDN_URL_CACHE_REFRESH = "https://restapi.cdn.azure.cn/subscriptions/subscriptionId/endpoints/endpointId/purges?apiVersion=1.0";
//url中的两个常量定义
publicstatic String MICROSOFT_CDN_URL_PARAM_SUB_SCRIPTIONID = "subscriptionId";
publicstatic String MICROSOFT_CDN_URL_PARAM_ENDPOINTID = "endpointId";
}
5.域名-节点ID属性配置文件
新增一个属性配置文件azure_cdn_endpoint.properties,大致内容如下,这些内容来自于调"用节点管理-获取订阅下所有节点信息" 接口返回的数据,然后解析成域名和endpointID这样的关系,存入属性配置文件中,以供缓存MircosoftCacheConfig配置类进行初始化到hostEndpointMap中
好处:减少获取节点接口调用,提升访问效率(毕竟接口调用是需要时间的,经过跟cdn运维人员确认,每个订阅ID下的节点数据都会一样,所以可以事先调用一次接口获取所有的节点)
my.covengarden.com=052b3030-f5dd-11e7-be87-0017fa000,
act.ochirly.com.cn=07ae85d2-f1bf-11e7-be87-0017fa000",
passport.ochirlyonline.com.cn=091491cd-f5be-11e7-be87-0017fa000,
passport.covengarden.com=0cfde6b1-f5df-11e7-be87-0017fa000"
6.单元测试类
提供两个接口的测试入口
1. 获取订阅下所有节点信息 2.缓存指定目录和文件接口
package com.trendy.ccp.server.cdn.tool;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.httpclient.util.DateUtil;
import org.junit.Test;
import com.trendy.ccp.server.cdn.config.MircosoftCacheConfig;
import com.trendy.ccp.server.cdn.config.MircosoftRequestParam;
import com.trendy.fw.common.config.Constants;
import com.trendy.fw.common.transfer.HttpClientResultBean;
import com.trendy.fw.common.util.JsonKit;
/****
* 接口测试类
* @ClassName: HttpTest
* @Description 主要两个接口 1. 获取订阅下所有节点信息 2.缓存指定目录和文件接口
* @author youqiang.xiong
* @date 2018年1月29日 下午3:55:03
*
*/
public class AzureCdnTest {
private String subId = "c3068370-40e0-49dc-aa3e-244f070603ee";
private String keyId = "52b67e39-856c-4930-a8e9-acf551201c39";
private String keyValue = "MWZkZGU5YWMtNjI1NS00YjFkLThiZTMtODRjOTE3MWNiYWYw";
/****
*获取订阅下所有节点信息
*/
@Test
public void testGetEndpoint(){
String url = MircosoftCacheConfig.MICROSOFT_CDN_URL_GET_NODES;
if(url.contains(MircosoftCacheConfig.MICROSOFT_CDN_URL_PARAM_SUB_SCRIPTIONID)){
url = url.replaceAll(MircosoftCacheConfig.MICROSOFT_CDN_URL_PARAM_SUB_SCRIPTIONID, subId);
}
System.out.println("url:\t" +url);
String requestTime = DateUtil.formatDate(new Date(), "yyyy-MM-dd hh:mm:ss");
System.out.println("requestTime:\t"+requestTime);
String httpMethod = "GET";
try {
String authorizationHeader = AzureSignTool.calculateAuthorizationHeader(url, requestTime, keyId, keyValue, httpMethod);
System.out.println("Signature:\t"+authorizationHeader);
Map<String, String> paramMap = new HashMap<String, String>();
paramMap.put(MircosoftRequestParam.MICROSOFT_CDN_GET_NODES_HEADER1, requestTime);
paramMap.put(MircosoftRequestParam.MICROSOFT_CDN_GET_NODES_HEADER2, authorizationHeader);
HttpClientResultBean responseBean = HttpKit.getContext(url, paramMap, Constants.CODE_UNICODE);
System.out.println(responseBean.getResultContent());
} catch (Exception e) {
e.printStackTrace();
}
}
/****
* 刷新指定 文件 和目录下的缓存
*/
@Test
public void testCndReresh(){
String endpoint = "";
HashMap<String, Object> map = new HashMap<String, Object>();
List<String> urlList = new ArrayList<String>();
List<String> dirList = new ArrayList<String>();
dirList.add("http://ochirly.trendy-global.com/cn/");
map.put("Files", urlList);
map.put("Directories", dirList);
String key = "ochirly.trendy-global.com";
//第一步获取的endpoints,根据域名,从原有的endpoints中找到对应的endpoint
endpoint = MircosoftCacheConfig.getHostEndpointMap().get(key);
String task = JsonKit.toJson(map);
System.out.println("body:\t"+task);
// 首先根据KeyId,keyValue,url生产authorization
String url = MircosoftCacheConfig.MICROSOFT_CDN_URL_CACHE_REFRESH;
if (url.contains(MircosoftCacheConfig.MICROSOFT_CDN_URL_PARAM_SUB_SCRIPTIONID)) {
url = url.replaceAll(MircosoftCacheConfig.MICROSOFT_CDN_URL_PARAM_SUB_SCRIPTIONID,subId);
}
if (url.contains(MircosoftCacheConfig.MICROSOFT_CDN_URL_PARAM_ENDPOINTID)) {
url = url.replaceAll(MircosoftCacheConfig.MICROSOFT_CDN_URL_PARAM_ENDPOINTID,endpoint);
}
System.out.println("url:\t" +url);
String requestTime = DateUtil.formatDate(new Date(), "yyyy-MM-dd hh:mm:ss");
String httpMethod = "POST";
try {
String authorizationHeader = AzureSignTool.calculateAuthorizationHeader(url, requestTime, keyId,keyValue, httpMethod);
System.out.println("时间:\t" + requestTime);
System.out.println("签名:\t"+authorizationHeader);
System.out.println("content-type:\tapplication/json");
Map<String, String> paramMap = new HashMap<String, String>();
paramMap.put(MircosoftRequestParam.MICROSOFT_CDN_CACHE_REFRESH_HEADER1, requestTime);
paramMap.put(MircosoftRequestParam.MICROSOFT_CDN_CACHE_REFRESH_HEADER2,authorizationHeader);
paramMap.put(MircosoftRequestParam.MICROSOFT_CDN_CACHE_REFRESH_HEADER3,"application/json");
HttpClientResultBean responseBean = HttpKit.postContent(url,paramMap, task, Constants.CODE_UNICODE);
System.out.println(responseBean.getResultContent());
} catch (Exception e) {
}
}
}
步骤
-
执行testGetEndpoint 这个接口会输出 订阅id下所有的endpointiD接口,整理并解析这份json格式,转换成一份host-endpointId的映射对象,保存到上一步的 azure_cdn_endpoint.properties配置文件中
说明:
String subId = ";
String keyId = "";
String keyValue = "";
这3个参数跟公司的同事获取(应该就是CDN服务商提供)
- 整理好属性配置文件后,执行testCndReresh 方法就可以成功刷新对应文件和目录下的缓存
特别声明遇到过的大坑
String requestTime = DateUtil.formatDate(new Date(), "yyyy-MM-dd hh:mm:ss");
日期格式一定要是yyyy-MM-dd hh:mm:ss要不能接口调用会报异常,就因为这个日期格式的错误,还花了不少的没必要的时间
总结
上面介绍的步骤,是我开发过程中的主要思路和步骤,您们需要关注的并不需要以上那么多,因为我已经做了封装,你们只需要按照下面的步骤可以直接进行对接。
- 获取API key ID,API key value,subscriptionId(订阅ID)
- 进入到第6步,执行testGetEndpoint,输出节点信息
- 整理host和endpointid的对应关系
- 将上一步的数据存放第5步的azure_cdn_endpoint.properties配置文件中
- 进入到第6步,修改testCndReresh中的url和dir改成自己的数据,然后执行testCndReresh方法即可。