# Restful签名鉴权

Restful接口使用HTTP无状态协议,并使用密钥签名的方式进行鉴权。平台整体API设计使用Restful风格,但某些特殊操作会根据情况不同使用适合的命名方式和方法,具体根据API文档进行调用。

# HTTP请求说明

# 请求方法

方法 说明
GET 用于获取数据信息,不能包含任何Body信息。
POST 用于增加数据,Header必须包含 Content-Type: application/jsonBody内容必须是JSON格式。
PUT 用于修改数据,Header与Body同POST方法。
DELETE 用于删除数据,不能包含Body信息。

# 请求固定参数

以下参数为鉴权固定参数,每次请求均需要在query中携带以下参数(非鉴权方法无需传递)。就是一般url中?后面部分,例如:

// HTTP协议网关API
https://api.hanclouds.com/api/v1/pushsvcs/createAuthToken?ts=1531709593000&nonce=xxxxxxx&signature=xxxxx

// HTTP协议图片网关API
https://api.hanclouds.com/image/v1/devices/deviceKey/datastreams/dataName/images?ts=1531709593000&nonce=xxxxxxx&signature=xxxxx
参数 说明
ts API发送时间戳,单位是毫秒,服务器会判断该请求时间是否在允许范围内,避免重放攻击。允许波动范围为5分钟
nonce 随机字符。
signature 签名值,使用base64编码传递,具体签名算法参考后面文档。

# Key信息传递

服务器在接受请求时,需要知道当前您使用了什么级别的鉴权参数,所以需要在Header中固定传递相关信息。

授权类型 Header参数 说明
用户级 HC-USER-KEY 用户唯一key,描述使用哪位用户身份进行鉴权访问。与HC-USER-AUTH-KEY共存。
用户级 HC-USER-AUTH-KEY 用户鉴权参数。
产品级 HC-PRODUCT-KEY 产品唯一key,描述使用哪个产品身份进行鉴权访问。与HC-PRODUCT-SERVICE-KEY共存。
产品级 HC-PRODUCT-SERVICE-KEY 产品鉴权参数,可以是任何功能的一种。
设备级 HC-DEVICE-KEY 设备唯一key,描述使用哪个设备身份进行鉴权访问。

以上参数不需要全部传递,取决于当前您使用什么级别鉴权方式。

# 签名算法

# 签名字符串生成

以下用抽象语言描述

  1. {key}={value}的方式将所有query参数进行拼接,放入字符串数组中。(signature为最后生成参数,请求参数中不能携带该参数,如有重名请修改)
  2. 根据字符串ASCII码值进行正向排序。
  3. 将字符串数组数据使用&字符进行拼接。
  4. 如果有Body参数,直接将Body参数放到上述生成字符串最后。

在query参数中,如果只有key而没有value,不要放入字符串数组中,例如: aa=&ff=cc,aa不参与签名计算。

# 网关API签名

Java语言签名计算示例:

Map<String, String[]> parameterMap = request.getParameterMap();
if (parameterMap.size() == 0) {
    return null;
}

List<String> paramStrs = new ArrayList<>(parameterMap.size());
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
    if (entry.getValue().length == 0) {
        continue;
    }
    //签名参数不参与签名
    if (entry.getKey().equals("signature")) {
        continue;
    }
    for (String v : entry.getValue()) {
        paramStrs.add(entry.getKey() + "=" + v);
    }
}

Collections.sort(paramStrs);

StringBuilder signStrBuilder = new StringBuilder();

boolean isFirstLoop = true;
for (String paramStr : paramStrs) {
    if (isFirstLoop) {
        isFirstLoop = false;
    } else {
        signStrBuilder.append('&');
    }
    signStrBuilder.append(paramStr);
}

try {
    byte[] bodyContent = StreamUtils.copyToByteArray(request.getInputStream());
    signStrBuilder.append(new String(bodyContent, Charset.forName("UTF-8")));
} catch (IOException e) {
    return null;
}

# 图片网关API签名

Java语言签名计算示例:

Map<String, String[]> parameterMap = request.getParameterMap();
if (parameterMap.size() == 0) {
    return null;
}

List<String> paramStrs = new ArrayList<>(parameterMap.size());
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
    if (entry.getValue().length == 0) {
        continue;
    }
    //签名参数不参与签名
    if (entry.getKey().equals("signature")) {
        continue;
    }
    for (String v : entry.getValue()) {
        paramStrs.add(entry.getKey() + "=" + v);
    }
}

Collections.sort(paramStrs);

StringBuilder signStrBuilder = new StringBuilder();

boolean isFirstLoop = true;
for (String paramStr : paramStrs) {
    if (isFirstLoop) {
        isFirstLoop = false;
    } else {
        signStrBuilder.append('&');
    }
    signStrBuilder.append(paramStr);
}

try {
    //body参数
    byte[] bodyContent = StreamUtils.copyToByteArray(request.getInputStream());
    //图片网关API通过base64编码
    signStrBuilder.append(Base64Utils.encodeToString(bodyContent));
} catch (IOException e) {
    return null;
}

python语言签名计算示例:

import requests
import random
import string
import base64
import time
import hmac
from hashlib import sha1
import urllib
import os


def uploadimg(uploadToken, devicekey, jpgimgPath):
    # 读取图片数据
    file = open(jpgimgPath, "rb")
    imgdata = file.read()
    file.close()
    
    # 组装请求参数
    # 表示图片是jpg
    getParm = ['imageType=1']  
    # ts
    getParm.append("ts=" + str(round(time.time() * 1000)))  
    # 16个随机字符
    nonce = ''.join(random.sample(string.ascii_letters + string.digits, 16))  
    getParm.append('nonce=' + nonce)
    
    # 参数排序
    getParm.sort()
    
     # 拼接get请求参数
    total_str = ""
    for parm in getParm: 
        total_str = total_str + parm + "&"
        
    # 图片数据base64编码
    b64imgdata = base64.b64encode(imgdata)
    imgstring = b64imgdata.decode('utf-8')
    
    # 组成最终的预签名字符串
    content = total_str[:-1] + imgstring

    # 开始hmac的sha1签名
    hmac_code = hmac.new(uploadToken.encode('utf-8'), content.encode('utf-8'), sha1).digest()
    sign = base64.b64encode(hmac_code)
    
    # 包含有signature的请求参数
    # 务必要对base64后的签名经行url编码,否则里面的特殊符号影响请求
    total_str = total_str + "signature=" + urllib.parse.quote(str(sign, "utf-8"))  
    url = 'https://api.hanclouds.com/image/v1/devices/' + devicekey + '/datastreams/img/images?' + total_str
    
    # 组装设备级授权的header device-key
    headers = {"Content-Type": "application/json", "HC-DEVICE-KEY": devicekey} 

    print("url = ", url)
    print("headers = ", headers)
    
    # 开始发送请求
    req = requests.post(url, headers=headers, data=imgdata)
    print("status = ", req.status_code)
    print("result = ", req.content)
    print("result = ", req.text.encode('utf8', 'ignore'))


uploadimg('WpptFiHQWH8zzEtT', '88a6dd41fddb4a1e8553d87cb5c948c2', 'C:\\res.jpg')

# HMAC-SHA1签名

大部分语言都实现了 HMAC-SHA1 算法,将上述生成的签名字符串与对应的密钥(**Secret)作为参数进行签名,并进行Base64编码,即可得到 signature 值。

Java 示例 代码:

public static String signWithHmacsh1(String secret, String content) {
    try {
        byte[] keyBytes = secret.getBytes("utf-8");
        SecretKey secretKey = new SecretKeySpec(keyBytes, "HmacSHA1");
        Mac mac = Mac.getInstance("HmacSHA1");
        mac.init(secretKey);
        byte[] rawHmac = mac.doFinal(content.getBytes("utf-8"));
        String hmacSHA1Encode = new String((new BASE64Encoder()).encode(rawHmac));
        return hmacSHA1Encode;
    } catch (Exception e) {
        //异常处理
    }
}