为了保证网关层请求者身份的合法性和请求参数在传输过程中的安全性,OpenAPI 需要对开放服务的请求引入签名机制,对请求参数进行规范性的预处理和 Hash 运算获取请求签名。请求签名将和请求参数一起发送到OpenAPI,同时OpenAPI采用同样的机制对收到的请求进行签名计算,并与请求中的签名进行匹配。网关层对于所有签名不匹配的请求一律不允放行。
准备好签名参数和按照选定需要的服务构建服务请求后,需要对服务请求参数进行正规化处理。正规化请求(CanonicalRequest)的组成规则如下:
CanonicalRequest =
HTTPRequestMethod + '\n' +
CanonicalURI + '\n' +
CanonicalQueryString + '\n' +
CanonicalHeaders + '\n' +
SignedHeaders + '\n' +
HexEncode(Hash(RequestPayload))
字段 | 说明 |
---|---|
HTTPRequestMethod | 指代http请求的method,例如:GET、POST等。 OpenAPI 推荐所有请求以POST方式发起 |
CanonicalURI | 指代正规化后的URI。 如果URI为空,那么使用"/"作为绝对路径。 OpenAPI中,主要接口的的URI设定都为"/"。如果是复杂的path,请通过RFC3986规范进行编码。 |
CanonicalQueryString | 指代正规化后的Query String。 对于Query String的正规化大致的过程如下:1. urlencode(注:同RFC3986方法)每一个querystring参数名称和参数值。2. 按照ASCII字节顺序对参数名称严格排序,相同参数名的不同参数值需保持请求的原始顺序。3. 将排序好的参数名称和参数值用=连接,按照排序结果将“参数对”用&连接。例如:CanonicalQueryString = "Action=FetchUpload&Version=2021-11-09" |
CanonicalHeaders | 指代正规化后的Header字符串。 组成规则如下:CanonicalHeaders = CanonicalHeadersEntry0 + CanonicalHeadersEntry1 + ... + CanonicalHeadersEntryN 其中 CanonicalHeadersEntry = Lowercase(HeaderName) + ':' + Trimall(HeaderValue) + '\n' Lowcase代表将Header的名称全部转化成小写,Trimall表示去掉Header的值的前后多余的空格。特别注意:最后需要添加"\n"的换行符,header的顺序是以headerName的小写后ascii排序 与SignedHeaders中的header顺序需保持一致。 |
SignedHeaders | 指代参与签名的header名称。 签名header包含在正规化headers名称列表中,其目的是指明哪些header参与签名计算。其中X-SL-Action必须添加。构造方式如下:SignedHeaders = Lowercase(HeaderName0) + ';' + Lowercase(HeaderName1) + ";" + ... + Lowercase(HeaderNameN) |
RequestPayload | 指代完整的请求的body。 |
签名字符串主要包含请求以及正规化请求的元数据信息,由签名算法、请求日期、SL规则串和正规化请求哈希值连接组成。
StringToSign =
Algorithm + '\n' +
Timestamp + '\n' +
CredentialScope + '\n' +
HexEncode(Hash(CanonicalRequest))
字段 | 解释 |
---|---|
Algorithm | 签名的算法 目前仅支持HMAC-SHA256的签名算法,目前固定为 SL-HMAC-SHA256。 |
TimeStamp | 请求UTC时间 即请求头公共参数中X-SL-Timestamp的取值 |
CredentialScope | 凭证范围 格式为 Date/service/sl_request,包含日期、所请求的服务和终止字符串(sl_request)。 |
Date | UTC 标准时间的日期 取值需要和公共参数 X-TC-Timestamp 换算的 UTC 标准时间日期一致。 |
service | 产品名,必须与调用的产品域名一致,直播云服务的产品域名为'live'。 例: 2022-02-25/live/sl_request。 |
CanonicalRequest | 指代按照 1.3.2 生成的正规化请求字符串。 |
签名密钥需要避免直接使用用户的基础密钥,而是应当以用户的基础密钥为根密钥,通过计算派生出符合请求的签名密钥,防止用户密钥的扩散带来的不安全性。签名密钥的计算规则如下:
SLSecret = [用户AccessKey对应的SecretKey]
SLDate = HMAC(SLSecret, Date)
SLService = HMAC(SLDate, Service)
SLSigning = HMAC(SLService, "sl_request")
使用签名字符串和签名密钥计算获取最终的请求签名
Signature = HexEncode(HMAC(SLSigning, StringToSign))
Authorization =
Algorithm + ' ' +
'Credential=' + AccessKey + '/' + CredentialScope + ', ' +
'SignedHeaders=' + SignedHeaders + ', ' +
'Signature=' + Signature + "sl_request"
对于一个需要进行FetchUpload的API请求。假定使用方已经在账号系统中创建了一个指定用户,并获取到此用户对应的AK/SK。
AccessKey:"3af394d65d654582bd6e8ad122199558"
SecretKey:"88d749f980554ca79bc6ff9b2ce02c10"
CanonicalRequest =
"POST" + '\n' +
"/" + '\n' +
"Action=DescribeLicense" + '\n' +
"content-type:application/x-www-form-urlencoded\nhost:streamlake-api.staging.kuaishou.com" + '\n' +
"content-type;host" + '\n' +
"c2ef249dbee06fcf906069b4900cc806ddcfdecbaa87552439b87d0ce6ad7e45" //HexEncode(Hash("PackageId=com.kwai.facialassistant.demo&ProdCode=y-tech&Version=2022-02-25"))
StringToSign =
"SL_HMAC-SHA256" + '\n' +
"1658215855" + '\n' +
"2022-07-19/license/sl_request" + '\n' +
"32544b380cd36218b30f6bb6d0bd52b163c997775108893beb1668132a3e9676" //HexEncode(Hash(CanonicalRequest))
Signature="d57996a78008bf1e505f1d677afbfb89d9097f61226b2ca64876bb7523db9f3e"//HexEncode(HMAC(SLSigning, StringToSign))
Authorization= "SL-HMAC-SHA256 Credential=3af394d65d654582bd6e8ad122199558/2022-07-19/license/sl_request, SignedHeaders=content-type;host, Signature=d57996a78008bf1e505f1d677afbfb89d9097f61226b2ca64876bb7523db9f3esl_request"
Authorization:SL-HMAC-SHA256 Credential=3af394d65d654582bd6e8ad122199558/2022-07-19/license/sl_request, SignedHeaders=content-type;host, Signature=d57996a78008bf1e505f1d677afbfb89d9097f61226b2ca64876bb7523db9f3esl_request
/**
* SignatureVO
*/
public class SignatureVO {
/**
* StreamLake 密钥ak
*/
private String accessKeyId;
/**
* StreamLake 密钥sk
*/
private String accessKeySecret;
/**
* StreamLake 加签算法
*/
private String algorithm;
/**
* StreamLake 服务编码
*/
private String service;
/**
* StreamLake request host
*/
private String host;
/**
* StreamLake request content-type
*/
private String contentType;
/**
* StreamLake request region
*/
private String region;
/**
* StreamLake request action
*/
private String action;
/**
* StreamLake request version
*/
private String version;
/**
* HTTP 请求方法(GET、POST )
*/
private String httpRequestMethod;
/**
* 发起 HTTP 请求 URL 中的查询字符串,
* 对于 GET 请求,则为 URL 中问号(?)后面的字符串内容,例如:Limit=10&Offset=0。
*/
private String canonicalQueryString;
/**
* 参与签名的头部信息,
* 至少包含 host 和 content-type 两个头部,
* 也可加入自定义的头部参与签名以提高自身请求的唯一性和安全性。
*/
private String canonicalHeaders;
/**
* 参与签名的头部信息,说明此次请求有哪些头部参与了签名,
* 和 CanonicalHeaders 包含的头部内容是一一对应的。
* content-type 和 host 为必选头部。
*/
private String signedHeaders;
/**
* 请求正文
*/
private String payload;
public String getAccessKeyId() {
return accessKeyId;
}
public void setAccessKeyId(String accessKeyId) {
this.accessKeyId = accessKeyId;
}
public String getAccessKeySecret() {
return accessKeySecret;
}
public void setAccessKeySecret(String accessKeySecret) {
this.accessKeySecret = accessKeySecret;
}
public String getAlgorithm() {
return algorithm;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public String getService() {
return service;
}
public void setService(String service) {
this.service = service;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public String getRegion() {
return region;
}
public void setRegion(String region) {
this.region = region;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getHttpRequestMethod() {
return httpRequestMethod;
}
public void setHttpRequestMethod(String httpRequestMethod) {
this.httpRequestMethod = httpRequestMethod;
}
public String getCanonicalQueryString() {
return canonicalQueryString;
}
public void setCanonicalQueryString(String canonicalQueryString) {
this.canonicalQueryString = canonicalQueryString;
}
public String getCanonicalHeaders() {
return canonicalHeaders;
}
public void setCanonicalHeaders(String canonicalHeaders) {
this.canonicalHeaders = canonicalHeaders;
}
public String getSignedHeaders() {
return signedHeaders;
}
public void setSignedHeaders(String signedHeaders) {
this.signedHeaders = signedHeaders;
}
public String getPayload() {
return payload;
}
public void setPayload(String payload) {
this.payload = payload;
}
public SignatureVO() {
}
}
/**
*
*/
@Slf4j
@Component
public class StandardAccessUtils {
private static final Charset UTF8 = StandardCharsets.UTF_8;
public static void signatureAdd(SignatureVO signatureVO) throws Exception {
String accessKeyId = signatureVO.getAccessKeyId();
String accessKeySecret = signatureVO.getAccessKeySecret();
String service = signatureVO.getService();
String host = signatureVO.getHost();
String contentType = signatureVO.getContentType();
String region = signatureVO.getRegion();
String action = signatureVO.getAction();
String version = signatureVO.getVersion();
String algorithm = signatureVO.getAlgorithm();
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 注意时区,否则容易出错
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
// Date 为 UTC 标准时间的日期,取值需要和公共参数 X-TC-Timestamp 换算的 UTC 标准时间日期一致
String date = sdf.format(new Date(Long.parseLong(timestamp + "000")));
// ************* 步骤 1:拼接规范请求串 *************
String httpRequestMethod = signatureVO.getHttpRequestMethod();
String canonicalUri = "/";
String canonicalQueryString = signatureVO.getCanonicalQueryString();
String canonicalHeaders = signatureVO.getCanonicalHeaders();
String signedHeaders = signatureVO.getSignedHeaders();
String payload = signatureVO.getPayload();
String hashedRequestPayload = sha256Hex(payload);
String canonicalRequest =
httpRequestMethod + "\n"
+ canonicalUri + "\n"
+ canonicalQueryString + "\n"
+ canonicalHeaders + "\n"
+ signedHeaders + "\n"
+ hashedRequestPayload;
// ************* 步骤 2:拼接待签名字符串 *************
String credentialScope = date + "/" + service + "/" + "sl_request";
String hashedCanonicalRequest = sha256Hex(canonicalRequest);
String stringToSign =
algorithm + "\n"
+ timestamp + "\n"
+ credentialScope + "\n"
+ hashedCanonicalRequest;
// ************* 步骤 3:计算签名 *************
byte[] secretDate = hmac256(("SL" + accessKeySecret).getBytes(UTF8), date);
byte[] secretService = hmac256(secretDate, service);
byte[] secretSigning = hmac256(secretService, "sl_request");
String signature = DatatypeConverter.printHexBinary(hmac256(secretSigning, stringToSign)).toLowerCase();
// ************* 步骤 4:拼接 Authorization *************
String authorization = algorithm + " " + "Credential=" + accessKeyId + "/" + credentialScope + ", "
+ "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature + "sl_request";
TreeMap<String, String> header = new TreeMap<String, String>();
header.put("Authorization", authorization);
header.put("Content-Type", contentType);
header.put("Host", host);
header.put("X-SL-Action", action);
header.put("X-SL-Timestamp", timestamp);
header.put("X-SL-Version", version);
header.put("X-SL-Region", region);
header.put("X-SL-Program-Language", "Java");
header.put("SignatureVersion", "1");
header.put("AccessKey", accessKeyId);
StringBuilder sb = new StringBuilder();
sb.append("curl -X POST https://").append(host).append("/?Action=").append(action)
.append(" -H "Authorization: ").append(authorization).append(""")
.append(" -H "Content-Type: ").append(contentType).append(""")
.append(" -H "Host: ").append(host).append(""")
.append(" -H "X-SL-Action: ").append(action).append(""")
.append(" -H "X-SL-Timestamp: ").append(timestamp).append(""")
.append(" -H "X-SL-Version: ").append(version).append(""")
.append(" -H "X-SL-Region: ").append(region).append(""")
.append(" -H "X-SL-Program-Language: ").append(programLanguage).append(""")
.append(" -H "SignatureVersion: ").append("1").append(""")
.append(" -H "AccessKey: ").append(accessKeyId).append(""")
.append(" -d '").append(payload).append("'");
System.out.println(sb.toString());
return header;
}
public static byte[] hmac256(byte[] key, String msg) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
mac.init(secretKeySpec);
return mac.doFinal(msg.getBytes(UTF8));
}
public static String sha256Hex(String s) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] d = md.digest(s.getBytes(UTF8));
return DatatypeConverter.printHexBinary(d).toLowerCase();
}
public static void main(String[] args) throws Exception {
SignatureVO signatureVO = SignatureVO.builder()
.accessKeyId("xxx")
.accessKeySecret("xxx")
.algorithm("SL-HMAC-SHA256")
.action("FetchUpload")
.host("vod.streamlakeapi.com")
.contentType("application/json")
.region("beijing")
.version("2022-06-23")
.service("vod")
.canonicalHeaders("content-type:application/json\nhost:vod.streamlakeapi.com")
.canonicalQueryString("Action=FetchUpload")
.httpRequestMethod("POST")
.signedHeaders("content-type;host")
.payload("{\"URLSets\":[{\"MediaURL\":\"http://j.com/mediacloud/demo/test.mp4\",\"CallbackArgs\":\"test\"}]}")
.build();
signatureAdd(signatureVO);
}
}