调用API进行线下应用开发

使能平台的API是基于HTTP协议来调用的,开发者可以根据协议来封装HTTP请求进行调用,以下主要是针对自行封装HTTP请求进行API调用的原理进行详细解说。

1. 调用流程

根据协议填充参数 > 生成签名 > 拼装HTTP请求 > 发起HTTP请求> 得到HTTP响应 > 解释结果。

直接调用API时,需要自行对数据进行签名并提交签名数据。签名源数据包含时间戳、App Key、HTTP BODY、以及用户在API中配置的所有参数字段,开发者需要在调用前将这些数据拼接成字符串,并使用App Secret进行签名以获得签名数据。

2. 调用入口

调用API的服务URL地址,开发者可以在正式环境中使用。

3. 调用参数

API调用所需的公共参数与业务参数请参考API文档说明

4. 时间修正

调用需要签名认证的API之前,先要计算客户端与API网关的时间偏移量,避免过时的请求被重复使用。提交请求的时间戳(修正后)与服务器时间相差小于系统参数值clock_skew的时候,请求合法,否则会拒绝请求。如果是不需签名认证的API请求则不校验时间合法性。

开发者可以通过http://${正式环境地址}/echo接口的响应报文头的timestamp字段获得服务器时间t1,在服务器返回的同时获得本地时间t2,计算出大致的时间偏移量。

offset = t1-t2

在后续的请求,公共参数中需要提供的timestamp参数,需要根据这个偏移量进行修正后再写入到请求或签名中。

timestamp = System.currentTimeMillis() + offset

5. 获取时间偏移量示例

public static long getTimeOffset() {
    long offset = 0;
    HttpResponse response = null;

    //构造httpGet请求
    CloseableHttpClient httpClient = HttpClientBuilder.create().build();
    HttpGet httpTimeGet = new HttpGet("https://ag-api.ctwing.cn/echo");

    try {
     long start = System.currentTimeMillis();
     response = httpClient.execute(httpTimeGet);
     long end = System.currentTimeMillis();
     //时间戳在返回的响应的head的x-ag-timestamp中
     Header[] headers = response.getHeaders("x-ag-timestamp");
     if (headers.length > 0) {
         long serviceTime = Long.parseLong(headers[0].getValue());
         offset = serviceTime - (start + end) / 2L;
     }
     httpClient.close();
    } catch (ClientProtocolException e) {
     e.printStackTrace();
    } catch (IOException e) {
     e.printStackTrace();
    }
    return offset;
}

6. 签名算法

为了防止API调用过程中被黑客恶意篡改,调用任何一个API都需要携带签名,API网关服务端会根据请求参数,对签名进行验证,签名不合法的请求将会被拒绝。API网关目前只支持HAMC_SHA1签名算法,

签名大体过程如下:

(1)对API中定义的所有输入参数(注意签名字段必须包括API定义中的所有字段,如果某个字段不需要输入参数值,则使用0长度字符串代替参数值,并加入到签名字段列表中),根据参数名称的ASCII码表的顺序排序。如:

foo = 1
bar = 2
foo_bar = null
foobar = 4

排序后的顺序是

bar = 2
foo = 1
foo_bar = null
foobar = 4

(2)在上述输入参数的前面添加上公共参数中的application和timestamp(使用offset修正后)参数:

application = 10000.1234567
timestamp = 1519637736018
bar = 2
foo = 1
foo_bar = null
foobar = 4

(3)将排序好的参数名和参数值使用冒号按成对拼接后,再通过换行符将所有键值对连接在一起,根据上面的示例得到的字符串结果为:

application:10000.1234567
timestamp:1519637736018
bar:2
foo:1
foo_bar:
foobar:4

(4)对于HTTP BODY数据的签名通过二进制的方式追加到最后,参考下面的签名示例代码。

(5)把拼装好的字符串采用utf-8编码,使用签名算法对编码后的字节流进行HMAC_SHA1摘要计算。

(5)将摘要得到的字节流结果再使用Base64进行编码计算得到最后结果:

SeLSjSQezf/z1FqDmuQYsu9B/+g=

7. 签名与调用示例

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.TreeSet;

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

import org.apache.commons.codec.binary.Base64;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;

public class ApiExample {
    public static void main(String args[]) throws Exception {
        httpGetExample();
        httpPostExample();
    }

    /**
     * http GET请求示例
     */
    public static void httpGetExample() throws Exception {
        String secret = "FJDq8agNp5";// 密钥,到控制台->应用管理打开应用可以找到此值
        String application = "91Ebv1S0HBb";// appKey,到应用管理打开应用可以找到此值
        String version = "20181031202055";// api版本,到文档中心->使能平台API文档打开要调用的api可以找到此值
        String MasterKey = "25ce00cc28c1498c833276110ee483f0";// MasterKey,在产品中心打开对应的产品查看此值

        HttpResponse response = null;
        CloseableHttpClient httpClient = null;
        httpClient = HttpClientBuilder.create().build();

        long offset = getTimeOffset();// 获取时间偏移量,方法见前面

        // 下面示例以根据产品ID查询产品信息的API为例【具体信息请以使能平台的API文档为准】。
        // 构造请求的URL,具体参考文档中心->API文档中的请求地址和访问路径
        URIBuilder uriBuilder = new URIBuilder();
        uriBuilder.setScheme("https");// 请求用的协议,http或者https
        uriBuilder.setHost("ag-api.ctwing.cn/aep_product_management");//请求地址

        uriBuilder.setPath("/product");//访问路径,可以在API文档中对应的API中找到此访问路径

        // 在请求的URL中添加参数,具体参考文档中心->API文档中“请求参数”说明
        // (如果有MasterKey,将MasterKey加到head中,不加在此处)
        uriBuilder.addParameter("productId", "9392");

        HttpGet httpGet = new HttpGet(uriBuilder.build());//构造get请求

        long timestamp = System.currentTimeMillis() + offset;// 获取时间戳
        Date date = new Date(timestamp);
        SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
        dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
        String dataString = dateFormat.format(date);// 生成格式化的日期字符串

        // head中添加公共参数,具体参考文档中心->API文档中“公共参数”里的公共请求参数
        // httpGet.addHeader("MasterKey", MasterKey);// MasterKey加在此处head中
        httpGet.addHeader("application", application);
        httpGet.addHeader("timestamp", "" + timestamp);
        httpGet.addHeader("version", version);
        httpGet.addHeader("Content-Type", "application/json; charset=UTF-8");
        httpGet.addHeader("Date", dataString);

        // 下列注释的head暂时未用到
        // httpGet.addHeader("sdk", "GIT: a4fb7fca");
        // httpGet.addHeader("Accept", "gzip,deflate");
        // httpGet.addHeader("User-Agent", "Telecom API Gateway Java SDK");

        // 构造签名需要的参数,如果参数中有MasterKey,则添加来参与签名计算,其他参数根据实际API从URL中获取
        Map<String, String> param = new HashMap<String, String>();
        // param.put("MasterKey", MasterKey);

        // 从URL中获取参数加到param中
        List<NameValuePair> list = uriBuilder.getQueryParams();
        for (int i = 0; i < list.size(); i++)
            param.put(list.get(i).getName(), list.get(i).getValue());

        // 添加签名
        httpGet.addHeader("signature", sign(param, timestamp, application, secret, null));

        System.out.println(httpGet.getURI());

        try {
            // 发送请求
            response = httpClient.execute(httpGet);

            // 从response获取响应结果
            System.out.println(new String(EntityUtils.toByteArray(response.getEntity())));

            httpClient.close();
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * http POST请求示例
     */
    public static void httpPostExample() throws Exception {
        String secret = "FJDq8agNp5";//密钥,到控制台->应用管理打开应用可以找到此值
        String application = "91Ebv1S0HBb";//appKey,到控制台->应用管理打开应用可以找到此值
        String version = "20181031202117";//api版本,到文档中心->使能平台API文档打开要调用的api可以找到版本值
        String MasterKey = "25ce00cc28c1498c833276110ee483f0";//MasterKey,在产品中心打开对应的产品查看此值

        // 下面以增加设备的API为例【具体信息请以使能平台的API文档为准】。

        //请求BODY,到文档中心->使能平台API文档打开要调用的api中,在“请求BODY”中查看
        String bodyString = "{\"deviceName\":\"testDevice\",\"deviceSn\":\"\",\"imei\":123456789012345,\"operator\":\"admin\",\"productId\":\"9392\"}";

        CloseableHttpClient httpClient = null;
        HttpResponse response = null;
        httpClient = HttpClientBuilder.create().build();

        long offset = getTimeOffset();// 获取时间偏移量,方法见前面

        // 构造请求的URL,具体参考文档中心->使能平台API文档中的请求地址和访问路径
        URIBuilder uriBuilder = new URIBuilder();
        uriBuilder.setScheme("https");
        uriBuilder.setHost("ag-api.ctwing.cn/aep_device_management"); //请求地址
        uriBuilder.setPath("/device"); //访问路径,可以在API文档中对应API中找到此访问路径

        // 在请求的URL中添加参数,具体参考文档中心->API文档中请求参数说明
        // (如果有MasterKey,将MasterKey加到head中,不加在此处)
        //uriBuilder.addParameter("productId", "9392");//如果没有其他参数,此行不要

        HttpPost httpPost = new HttpPost(uriBuilder.build());//构造post请求

        long timestamp = System.currentTimeMillis() + offset;// 获取时间戳
        Date date = new Date(timestamp);
        SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
        dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
        String dataString = dateFormat.format(date);// 生成格式化的日期字符串

        // head中添加公共参数
        httpPost.addHeader("MasterKey", MasterKey);// MasterKey加在此处head中
        httpPost.addHeader("application", application);
        httpPost.addHeader("timestamp", "" + timestamp);
        httpPost.addHeader("version", version);
        httpPost.addHeader("Content-Type", "application/json; charset=UTF-8");
        httpPost.addHeader("Date", dataString);

        // 下列注释的head暂时未用到
        // httpPost.addHeader("sdk", "GIT: a4fb7fca");
        // httpPost.addHeader("Accept", "gzip,deflate");
        // httpPost.addHeader("User-Agent", "Telecom API Gateway Java SDK");

        // 构造签名需要的参数,如果参数中有MasterKey,则添加来参与签名计算,
        // 其他参数根据实际API从URL中获取,如有其他参数,写法参考get示例
        Map<String, String> param = new HashMap<String, String>();
        param.put("MasterKey", MasterKey);

        // 添加签名
        httpPost.addHeader("signature", sign(param, timestamp, application, secret, bodyString.getBytes()));

        //请求添加body部分
        httpPost.setEntity(new StringEntity(bodyString));

        try {
            // 发送请求
            response = httpClient.execute(httpPost);

            // 从response获取响应结果
            System.out.println(new String(EntityUtils.toByteArray(response.getEntity())));

            httpClient.close();
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     * 
     * @param param    api 配置参数表
     * @param timestamp UNIX格式时间戳
     * @param application appKey,到应用管理打开应用可以找到此值
     * @param secret 密钥,到应用管理打开应用可以找到此值
     * @param body 请求body数据,如果是GET请求,此值写null
     * @return 签名数据
     */
    public static String sign(Map<String, String> param, long timestamp, String application, String secret, byte[] body) throws Exception {


        // 连接系统参数
        StringBuffer sb = new StringBuffer();
        sb.append("application").append(":").append(application).append("\n");
        sb.append("timestamp").append(":").append(timestamp).append("\n");

        // 连接请求参数
        if (param != null) {
        TreeSet<String> keys = new TreeSet<String>(param.keySet());
        Iterator<String> i = keys.iterator();
        while (i.hasNext()) {
            String s = i.next();
            String val = param.get(s);
            sb.append(s).append(":").append(val == null ? "" : val).append("\n");
        }
    }

    //body数据写入需要签名的字符流中
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(sb.toString().getBytes("utf-8"));
    if (body != null && body.length > 0) {
        baos.write(body);
        baos.write("\n".getBytes("utf-8"));
    }

        // 得到需要签名的字符串
        String string = baos.toString();
        System.out.println("Sign string: " + string);

        // hmac-sha1编码
        byte[] bytes = null;
        SecretKey secretKey = new SecretKeySpec(secret.getBytes("utf-8"), "HmacSha1");
        Mac mac = Mac.getInstance(secretKey.getAlgorithm());
        mac.init(secretKey);
        bytes = mac.doFinal(string.getBytes("utf-8"));

        // base64编码
        String encryptedString = new String(Base64.encodeBase64(bytes));

        // 得到需要提交的signature签名数据
        return encryptedString;
    }
}

8. 注意事项

  • 验证签名的时候,不建议使用工具发送请求,应该直接在程序中发送请求来验证签名。网关服务器有防止交易重放功能,如果一个请求的时间戳和服务器接收该请求时的时间戳时间超过限制(clock_skew参数),服务器会拒绝该请求,提示Time expired。所以,在验证签名的时候,应该在程序中发送POST请求去验证。

  • 所有的请求和响应数据编码皆为utf-8格式,URL里的所有参数名和参数值请做URL编码。如果请求的Content-Type是application/x-www-form-urlencoded,则HTTP Body体里的所有参数值也做URL编码;如果是multipart/form-data格式,每个表单字段的参数值无需编码,但每个表单字段的charset部分需要指定为utf-8。

  • HTTP 1.1协议中对应GET请求只允许URI长度小于1024,数据过多的时候应当考虑将API设置为POST请求。

  • 如需要在沙箱环境测试,请在应用控制台的沙箱管理页面获取沙箱环境对应的App Key和App Secret,对应的session值也用沙箱帐号登录授权获得,沙箱环境授权和正式环境授权类似。

  • 生成签名仅对未使用SDK进行API调用时需要操作,如使用了SDK,该步骤SDK会自动完成。

搜索结果 ""

    没有搜索结果 ""