# 服務云開放平臺
> 服務云開放平臺是西部機場集團面向全集團所有機場推出的在線旅客服務能力開放,自2018年推出以來,目前支持了各機場及成員企業共20多個SAAS化旅客服務在線應用,對外提供能力和數據接口服務,本接口文檔包含了如何接入開放平臺、現有的API接口分類和接口明細以及在對接中的常見問題,最大化的指導第三方平臺和系統的接入與接出
本文主要提供給第三方軟件服務商或有開發能力的成員機場公司將自有系統接入服務云使用
> 服務云API對于環境要求近乎沒有,API的返回數據均使用的是JSON格式,PHP、C、C#、C++、ASP、Javascript等等只要支持JSON格式或XML的都可以調用!
本接口開放平臺提供的都是測試環境的地址,在測試環境對接調試完成后,我們會提供具體接口的生產環境地址
測試環境地址:
*****
## 接入指南

#
> 1. 申請appcode和secretKey
*****
第三方需線下申請服務云應用參數,因為每個接口調用都需要應用代碼(appcode)進行簽名驗證,所以在調用前需要申請兩個參數,如果已經申請過了則不需要重復申請,分配的應用參數包括:
* appCode: 應用code
* secretKey: 應用key
申請appcode請聯系服務云技術負責人任波,微信手機同號18309287010
#
> 2. 開通接口權限
*****
調用方有了appcode和secretKey后,需線下告訴服務云需要調用的接口是什么,服務云進行第三方應用和接口的授權,第三方需要提供以下信息
* 調用場景,用來做什么
* 調用頻層,大概并發有多少
* 是否有同時多路調用的情況
* 分配的appcode
#
> 3、公共參數
*****
每個調用需要的請求公共參數包括以下幾個(調用接口時務必帶上以下參數):
* appCode:應用code (由服務云提供)
* timeStamp:時間戳(精度為毫秒,時間以標準北京時間為準,時間差不能大于服務器設置的值,當前設置為300秒)
* sign:簽名(簽名方式參考下面實例)
**注意:secretKey無需作為參數向后臺傳遞,secretKey是為了生成sign使用,請第三方系統保存好,不要外泄**
每個調用需要返回參數有:
* code: 0 - 失敗 1 - 成功
* message: 請求失敗或者發生錯誤的具體描述,部分業務消息中攜帶接口編號
* timeStamp:請求的時間戳
#
> 4、請求方式
*****
* 所有接口請求方式均采用POST方式;
* Content-type 使用: application/x-www-form-urlencoded; charset=utf-8
#
> 5、簽名方法
*****
* [ ] 生成簽名步驟
有了安全憑證 appCode和 secretKey后,就可以生成簽名串了。生成簽名串的詳細過程如下:
??
* [ ] 簽名舉例
* 假如用戶的appCode和secretKey如下
appCode:AS015
secretKey: \u9017\u6bd4
#
* 假如需要請求的參數為
name:admin
age:30
timestamp當前時間戳:1545927421045
appCode:U8Q5BKRT27BI
總結:公共請求參數有:appCode和timestamp;業務請求參數有:name和age
#
* 參數排序
首先對所有請求參數按參數名做字典序升序排列。(所謂字典序升序排列,直觀上就如同在字典中排列單詞一樣排序,按照字母表或數字表里遞增順序的排列次序,即先考慮第一個“字母”,在相同的情況下考慮第二個“字母”,依此類推)。上述示例參數的排序結果如下:
```
"age" : 30,"appCode" : "U8Q5BKRT27BI","name" : "admin","timeStamp" : 1545927421045
```
**注意:參數排序時不需要加sign,我們看到age排在appCode前面是因為它們的第二個字母g在p前面**
* 拼接請求字符串
此步驟將生成請求字符串,將把上一步排序好的請求參數格式化成“參數名稱”=“參數值”的形式,如對 age 參數,其參數名稱為"age",參數值為"30",因此格式化后就為 age=30,然后將格式化后的各個參數用"&"拼接在一起,最終生成的請求字符串為:
```
age=30&appCode=U8Q5BKRT27BI&name=admin&timeStamp=1545927421045
```
* 生成簽名串
簽名使用HmacSHA1 算法進行簽名
使用簽名算法HmacSHA1和secretKey對上一步中獲得的 請求字符串 進行簽名,獲得最終的簽名串。
最終得到的簽名串為:
3359CF98FE4BB6BDC99B157165E32B4E02651926
最后將生成的簽名串作為參數sign的值拼接到請求中傳到后臺。
```
age=30&appCode=U8Q5BKRT27BI&name=admin&timeStamp=1545927421045&sign=3359CF98FE4BB6BDC99B157165E32B4E02651926
```
* [ ] 簽名串生成java版代碼示例
~~~
package com.cwag.pss.ipss.butt.provider.util;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
/**
* 簽名
* 2018-12-25 20:21
*
* @author yangk
**/
@Component
@Slf4j
public class SignUtil {
@Value("${apiAuth.authKey}")
private String authKey;
// HMAC 加密算法名稱
public static final String HMAC_MD5 = "HmacMD5";// 128位
public static final String HMAC_SHA1 = "HmacSHA1";// 126
//獲取secretKey
public String getSecretKey(String appCode) {
TimeInterval time = DateUtil.timer();
authKey = authKey.toUpperCase();
appCode = appCode.toUpperCase();
String secretKey = hmacDigest(appCode, authKey, HMAC_MD5);
System.out.println("appCode:" + appCode);
System.out.println("secretKey:" + secretKey);
return secretKey;
}
//加密
public String getSign(SortedMap<Object, Object> parameters, String secretKey) {
StringBuffer sb = new StringBuffer();
StringBuffer sbkey = new StringBuffer();
Iterator it = parameters.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
Object v = entry.getValue();
if (null != v && !"".equals(v)) {
sb.append(k + "=" + v + "&");
sbkey.append(k + "=" + v + "&");
}
}
String valStr = sb.toString().substring(0, sbkey.toString().length() - 1);
log.info("字符串:" + valStr);
String sign = sbkey.toString().substring(0, sbkey.toString().length() - 1);
sign = hmacDigest(sign, secretKey, HMAC_SHA1);
log.info("加密值:" + sign);
return sign;
}
/**
* 生成HMAC摘要
*
* @param plaintext 明文
* @param secretKey 安全秘鑰
* @param algName 算法名稱
* @return 摘要
*/
public static String hmacDigest(String plaintext, String secretKey, String algName) {
try {
Mac mac = Mac.getInstance(algName);
byte[] secretByte = secretKey.getBytes();
byte[] dataBytes = plaintext.getBytes();
SecretKey secret = new SecretKeySpec(secretByte, algName);
mac.init(secret);
byte[] doFinal = mac.doFinal(dataBytes);
return byte2HexStr(doFinal);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
/**
* 字節數組轉字符串
*
* @param bytes 字節數組
* @return 字符串
*/
private static String byte2HexStr(byte[] bytes) {
StringBuilder hs = new StringBuilder();
String stmp;
for (int n = 0; bytes != null && n < bytes.length; n++) {
stmp = Integer.toHexString(bytes[n] & 0XFF);
if (stmp.length() == 1)
hs.append('0');
hs.append(stmp);
}
return hs.toString().toUpperCase();
}
/**
* 大巴開發票加密
*
* @param reqMap
* @param signKey
* @param charset
* @return
*/
public static String signBeforePost(Map<String, String> reqMap, String signKey, String charset) {
Predicate<Map.Entry<String, String>> predicate = new Predicate<Map.Entry<String, String>>() {
@Override
public boolean apply(Map.Entry<String, String> entry) {
return !"sign".equals(entry.getKey());
}
};
return mdsign(reqMap, signKey, charset, predicate);
}
public static String mdsign(Map<String, String> reqMap, String signKey, String charset, Predicate<Map.Entry<String, String>> predicate) {
if (StringUtils.isBlank(charset)) {
charset = "UTF-8";
}
Map<String, String> treeMap = Maps.newTreeMap();
treeMap.putAll(Maps.filterEntries(reqMap, predicate));
// 將null置空
String join = Joiner.on('&').withKeyValueSeparator("=").useForNull("").join(treeMap);
String signSrc = join + "&" + GetMessageDigest(signKey, "MD5", charset);
String sign = GetMessageDigest(signSrc, "MD5", charset);
return sign;
}
public static String GetMessageDigest(String strSrc, String encName, String charset) {
String charset_inner = StringUtils.isBlank(charset) ? "UTF-8" : StringUtils.trimToEmpty(charset);
MessageDigest md = null;
String strDes = null;
final String ALGO_DEFAULT = "SHA-1";
try {
if (StringUtils.isBlank(encName)) {
encName = ALGO_DEFAULT;
}
md = MessageDigest.getInstance(encName);
md.update(strSrc.getBytes(charset_inner));
strDes = bytes2Hex(md.digest()); // to HexString
} catch (NoSuchAlgorithmException e) {
e.getMessage();
} catch (UnsupportedEncodingException e) {
e.getMessage();
}
return strDes;
}
public static String bytes2Hex(byte[] bts) {
String des = StringUtils.EMPTY;
String tmp = null;
for (int i = 0; i < bts.length; i++) {
tmp = (Integer.toHexString(bts[i] & 0xFF));
if (tmp.length() == 1) {
des += "0";
}
des += tmp;
}
return des;
}
public static void main(String[] args) {
}
public static void strSort(String[] str) {
for (int i = 0; i < str.length; i++) {
for (int j = i + 1; j < str.length; j++) {
if (str[i].compareTo(str[j]) > 0) { //對象排序用camparTo方法
swap(str, i, j);
}
}
}
}
private static void swap(String[] strSort, int i, int j) {
String t = strSort[i];
strSort[i] = strSort[j];
strSort[j] = t;
}
private static void printArr(String[] str) {
for (int i = 0; i < str.length; i++) {
System.out.print(str[i] + "\t");
}
System.out.println();
}
}
~~~
獲取sign方法
~~~
//secretKey appcode 替換成相應的 值
String secretKey = signUtil.getSecretKey("secretKey ","appcode");
String sign = signUtil.getSign(map, secretKey);
~~~
Java版AS128加密示例
~~~
package com.cwag.pss.ipss.butt.provider.util;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* @author Administrator
*AES128加密
*/
@Slf4j
public class AES128 {
// 加密
public static String Encrypt(String sSrc, String sKey) throws Exception {
if (sKey == null) {
log.error("Key為空null");
return null;
}
// 判斷Key是否為16位
if (sKey.length() != 16) {
log.error("Key長度不是16位");
return null;
}
byte[] raw = sKey.getBytes("utf-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");//"算法/模式/補碼方式"
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(sSrc.getBytes("utf-8"));
return new Base64().encodeToString(encrypted);//此處使用BASE64做轉碼功能,同時能起到2次加密的作用。
}
// 解密
public static String Decrypt(String sSrc, String sKey) throws Exception {
try {
// 判斷Key是否正確
if (sKey == null) {
log.error("Key為空null");
return null;
}
// 判斷Key是否為16位
if (sKey.length() != 16) {
log.error("Key長度不是16位");
return null;
}
byte[] raw = sKey.getBytes("utf-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] encrypted1 = new Base64().decode(sSrc);//先用base64解密
try {
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original,"utf-8");
return originalString;
} catch (Exception e) {
log.error(e.toString());
return null;
}
} catch (Exception ex) {
log.error(ex.toString());
return null;
}
}
public static byte[] AESDecrypt(byte[] encryptedBytes, byte[] keyBytes, String keyAlgorithm, String cipherAlgorithm, String IV)
throws Exception {
try {
// AES密鑰長度為128bit、192bit、256bit,默認為128bit
if (keyBytes.length % 8 != 0 || keyBytes.length < 16 || keyBytes.length > 32) {
throw new Exception("AES密鑰長度不合法");
}
Cipher cipher = Cipher.getInstance(cipherAlgorithm);
SecretKey secretKey = new SecretKeySpec(keyBytes, keyAlgorithm);
if (IV != null && StringUtils.trimToNull(IV) != null) {
IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivspec);
} else {
cipher.init(Cipher.DECRYPT_MODE, secretKey);
}
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
return decryptedBytes;
} catch (NoSuchAlgorithmException e) {
throw new Exception(String.format("沒有[%s]此類加密算法", cipherAlgorithm));
} catch (NoSuchPaddingException e) {
throw new Exception(String.format("沒有[%s]此類填充模式", cipherAlgorithm));
} catch (InvalidKeyException e) {
throw new Exception("無效密鑰");
} catch (InvalidAlgorithmParameterException e) {
throw new Exception("無效密鑰參數");}
catch (BadPaddingException e) {
throw new Exception("錯誤填充模式");
} catch (IllegalBlockSizeException e) {
throw new Exception("解密塊大小不合法");
}
}
public static String AESdecodeParam(String strKey,String param){
if(StringUtils.isEmpty(param)){
return null;
}
String resData = "";
try {
byte[] decodeBase64DataBytes = Base64.decodeBase64(param.getBytes("UTF-8"));
byte[] merchantXmlDataBytes = AESDecrypt(decodeBase64DataBytes,strKey.getBytes("UTF-8"), "AES", "AES/ECB/PKCS5Padding", null);
resData = new String(merchantXmlDataBytes,"UTF-8");
} catch (UnsupportedEncodingException e) {
//log.info(e.getMessage());
e.printStackTrace();
} catch (Exception e) {
//log.info(e.getMessage());
e.printStackTrace();
}
return resData;
}
public static void main(String[] args) throws Exception {
/*
* 此處使用AES-128-ECB加密模式,key需要為16位。
*/
String cKey = "1111111111111111";
// 需要加密的字串
String cSrc = "612724199006260030";
log.error(cSrc);
// 加密
String enString = AES128.Encrypt(cSrc, cKey);
log.error("加密后的字串是:" + enString);
// 解密
String DeString = AES128.Decrypt(enString, cKey);
// log.error("解密后的字串是:" + AES128.AESdecodeParam("7ol8fXTdzjosHxC8e6Y38iZFw%2B4ICG6oD1GIsxBieK8%3D"));
}
}
//源代碼片段來自云代碼http://yuncode.net
~~~