SaaS应用对接
SaaS应用介绍
在应用托管体系中,并未所有应用的分发模式都是按照Kubernetes方式,对每一次应用向最终用户的交付形态,都是交付一个真实的物理运行空间。还有这里要提到的SaaS类应用。此类应用,其分发模式,仅仅是在原有系统中,分配了一个账户,交付给客户。因此,为了兼容单租户模式的独立托管部署和这类多租户的SaaS系统,平台抽象了与这类SaaS系统的交互行为,应用为满足这个交互行为,需要完成本文所提的对接要求。
整体链路
功能边界
租户管理
IoT平台管理阿里云租户与应用内租户的映射关系(支持一个阿里云租户拥有同一个应用的多个租户),SaaS管理应用内部的租户结构
组织架构
本期无需对接IoT平台的组织架构体系,SaaS内部自行管理组织架构,但是两边的组织架构需要能够定位到同一个员工身份, 目前通过员工电话作为识别的唯一标识;
权限管理
IoT平台管理租户及其组织架构中的员工是否有权限访问应用,SaaS管理租户及组织架构中员工访问功能列表;
对接开发
第一步:AppKey的获取
托管应用自动产生
应用部署在应用托管平台中,系统会为部署的每个应用颁发AppKey,AppKey通过环境变量方式注入至业务代码运行的容器中,供代码获取并使用;
人工创建
这种场景适用于您自行通过外置服务器的方式来搭建SaaS服务的模式。创建AppKey的过程如下问:登录卖家中心
,在“应用接入”->”应用管理”->“创建应用”账号分发类型:
选择云端外部接入的模式:
第二步:应用开发
应用开发,是指按照平台对SaaS了应用的对接要求,要求应用提供指定的接口,或者平台为应用提供必要的接口。目前,平台要求应用提供的接口有3个(生产租户接口、回收租户接口、免密登录接口)。另外,为了实现对接应用与平台之间的账户互认,平台提供了获取当前用户(包括子账户)的手机号。
接口开发1: 生产租户
场景描述
用户(阿里云账户体系)在物联网市场下单购买应用后需要通知SaaS为当前用户开立一个可访问SaaS服务的租户(账户),用户可以通过点击物联网市场中已购买的应用或者直接访问SaaS提供的公网域名来访问应用,当用户购买的应用到期后系统会自动发起SaaS租户的回收操作。所以您需要实现CreateInstance接口,来实现用户购买应用场景。
超时时间
默认接口超时时间为5秒。
调用时序
接口说明
CreateInstance生产服务
Protocol | HTTPS |
Method | POST |
请求参数
参数 | 类型 | 必填 | 描述 |
id | String | 是 | 该次访问唯一标示符(幂等验证ID) |
tenantId | String | 是 | IoT平台标识一个租户的唯一ID |
appId | String | 是 | 应用唯一ID,一个租户可以重复购买一款软件,每次购买appId都不同 |
appType | String | 是 | “TRYOUT”:试用,用户可以有一个短暂的试用期,该功能在商品上架时ISV可以选择是否提供试用逻辑“PRODUCTION”:正式购买,用户正式下单购买 |
moduleAttribute | String | 否 | JSON字符串,主要包含上架时候配置的额外计费项参数列表。JSON转化成Map之后,示例:{“key1”:”rjmjz2”,”key2”:”2”,”key3”:”rjmjz2”}详见下方参数介绍【moduleAttribute】 |
返回参数
参数 | 类型 | 描述 |
code | Integer | 调用成功返回200;若调用失败返回203; |
message | String | code返回203时填写具体失败原因,code返回200填写success |
userId | String | SaaS标识一个租户的唯一ID,对应的appid不同返回的userid也不同 |
注:同一个用户购买同一款SaaS软件,会多次请求该接口,请求参数中appId不同,saas应用需要返回不同的userId
验签说明参见【验签说明】
返回示例
{ "code":200, "message":"success", "userId":"my saas user id" }
参数介绍【moduleAttribute】
step1: 卖家在进行商品上架时指定额外计费项,当前示例中指定该收费项参数为”service_door”。
step2: 买家在市场下单购买商品时输入的额外计费项,当前示例中输入购买数量为200。
step3:买家下单后iot平台调用生产租户接口时传递的moduleAttribute参数值为{“service_door”:”200”}
接口开发2: 回收租户
场景描述
用户(阿里云账户体系)在物联网市场下单购买应用后需要通知SaaS为当前用户开立一个可访问SaaS服务的租户(账户),用户可以通过点击物联网市场中已购买的应用或者直接访问SaaS提供的公网域名来访问应用,当用户购买的应用到期后系统会自动发起SaaS租户的回收操作。所以您需要实现DeleteInstance接口,来实现用户购买应用到期回收场景。
超时时间
默认接口超时时间为5秒。
调用时序
接口说明
DeleteInstance注销服务
Protocol | HTTPS |
Method | POST |
请求参数
参数 | 类型 | 必填 | 描述 |
id | String | 是 | 该次访问唯一标示符(幂等验证ID) |
tenantId | String | 是 | IoT平台标识一个租户的唯一ID |
userId | String | 是 | SaaS标识一个租户的唯一ID |
appId | String | 是 | 应用唯一ID,一个租户可以重复购买一款软件,每次购买appId都不同 |
返回参数
参数 | 类型 | 描述 |
code | Integer | 调用成功返回200;若调用失败返回203; |
message | String | code返回203时填写具体失败原因,code返回200填写success |
验签说明
参见【验签说明】。
返回示例
{ "code":200, "message":"success" }
接口开发3: 免密登录
场景描述
用户(阿里云账户体系)在物联网市场下单购买应用后需要通知SaaS为当前用户开立一个可访问SaaS服务的租户(账户),用户可以通过点击物联网市场中已购买的应用来访问应用,这时用户无需再次输入SaaS的账户/密码即可进入SaaS系统进行业务操作,所以您需要实现GetSSOUrl接口,来实现用户访问应用时的免登场景。
安全说明
SaaS返回的SSOUrl必须满足以下两种安全策略中的一种
每次生成的URL只允许**一次使用**
限制临时token的有效时间范围(推荐30秒)
超时时间
默认接口超时时间为5秒。
调用时序
注: SaaS生成的token信息需要带有验证用户身份及时限性,生成和验证的过程全部由SaaS完成,IoT只做免登URL的透传。
接口说明
GetSSOUrl生产服务
Protocol | HTTPS |
Method | POST |
请求参数
参数 | 类型 | 必填 | 描述 |
id | String | 是 | 该次访问唯一标示符(幂等验证ID) |
tenantId | String | 是 | IoT平台标识一个租户的唯一ID |
tenantSubUserId | String | 否 | IoT平台租户组织架构中的员工唯一ID,当员工账号免登时填写 |
appId | String | 是 | 应用唯一ID,一个租户可以重复购买一款软件,每次购买appId都不同 |
userId | String | 是 | SaaS标识一个租户的唯一ID |
返回参数
参数 | 类型 | 描述 |
code | Integer | 调用成功返回200;若调用失败返回203; |
message | String | code返回203时填写具体失败原因,code返回200填写success |
ssoUrl | String | 免登url,需要带有验证用户身份及时限性的token信息 |
验签说明
参见【验签说明】
返回示例
{ "code":200, "message":"success", "ssoUrl":"https://www.test123.com/login.html?ssoToken=xdasfdasdfasfdasfdaf&checkToken=ddddddd"} }
接口提供1: 手机获取
场景描述
SaaS服务如果需要租户或者组织架构中员工手机号,可以通过该接口获取;授权过程: IoT平台授权appkey调用该接口权限展示:该权限属于用户隐私数据,所以用户在市场购买时会提示用户当前应用会读取他的手机,并且SaaS不可泄露用户手机号信息。
安全说明
出于安全考虑,单用户手机号只允许获取一次,如果有业务需求请获取后存储于您的系统中,并在相关性协议条款允许范围内使用该信息。
调用时序
接口说明
GetUserPhone手机号获取服务
protocol | httpMethod | apiVer | url | host |
HTTPS | POST | 1.0.0 | /app/user/info/get | api.link.aliyun.com |
请求参数
参数 | 类型 | 必填 | 描述 |
tenantId | String | 是 | IoT平台标识一个租户的唯一ID |
appId | String | 是 | 应用唯一ID,一个租户可以重复购买一款软件,每次购买appId都不同 |
tenantSubUserId | String | 否 | IoT平台租户组织架构中的员工唯一ID,当员工账号免登时填写。 |
userId | String | 是 | SaaS返回的租户唯一ID |
返回参数
参数 | 类型 | 描述 |
id | String | IoT平台返回的调用唯一ID |
code | Integer | 调用成功返回200;若调用失败返回错误码; |
message | String | code返回非200时填写具体失败原因,code返回200填写success |
data | “data”:{ “phone”:”1300000****” } | 当tenantSubUserId未填写时返回租户tenantId对应手机号,当tenantSubUserId填写时返回租户下组织架构中tenantSubUserId的手机号 |
加签说明
参见【验签说明】
返回示例
{ "id":"dj182318jjkhsljkdhfjahjdhfla" "code":200, "message":"success", "data":{ "phone":"1300000****" } }
错误码列表
错误码 | 错误信息 | 描述 |
200 | success | 成功 |
400 | request error | 请求错误 |
401 | request auth error | 请求认证错 |
403 | request forbidden | 请求被禁止 |
404 | service not found | 服务未找到 |
429 | too many requests | 太多请求 |
460 | request parameter error | 请求参数错误 |
500 | service error | 服务端错误 |
503 | service not available | 服务不可用 |
签名机制
加签
您可以直接下载签名的多语言demo,您可以在demo代码中找到加签的逻辑,如果以下列表不包含您的开发语言,您也可以通过阅读【加签说明】来自行开发加签逻辑。
开发语言 | sdk demo地址 |
java | https://github.com/aliyun/iotx-api-gateway-client com.aliyun.api.gatewaysdk-core-java1.1.0 |
python | |
php | |
c# | |
android |
验签
以下将为您介绍如何通过AppKey&AppSecret来对HTTP/HTTPS请求做加签,验签的过程需要您按照以下加签过程重新加签,如果您计算的签名和请求的签名一致即可认为验签成功。
签名内容
String stringToSign= HTTPMethod + "\n" + Accept + "\n" + //建议显示设置 Accept Header。当 Accept 为空时,部分 Http 客户端会给 Accept 设置默认值为 /,导致签名校验失败。 Content-MD5 + "\n" Content-Type + "\n" + Date + "\n" + Headers + Url
HTTPMethod 为全大写,如 POST。Accept、Content-MD5、Content-Type、Date 如果为空也需要添加换行符”\n”,Headers如果为空不需要添加”\n”。
1.1. Content-MD5Content-MD5 是指 Body 的 MD5 值,只有当 Body 非 Form 表单时才计算 MD5,计算方式为:
// bodyStream 为字节数组。
String content-MD5 = Base64.encodeBase64(MD5(bodyStream.getbytes("UTF-8")));
1.2. HeadersHeaders 是指参与 Headers 签名计算的 Header 的 Key、Value 拼接的字符串,建议对 X-Ca 开头以及自定义Header 计算签名,注意如下参数不参与 Headers 签名计算:X-Ca-Signature、X-Ca-Signature-Headers、Accept、Content-MD5、Content-Type、Date。Headers 组织方法: 先对参与 Headers 签名计算的 Header的Key 按照字典排序后使用如下方式拼接,如果某个 Header 的 Value 为空,则使用 HeaderKey + “:” + “\n”参与签名,需要保留 Key 和英文冒号。
String headers =
HeaderKey1 + ":" + HeaderValue1 + "\n"+
HeaderKey2 + ":" + HeaderValue2 + "\n"+
...
HeaderKeyN + ":" + HeaderValueN + "\n"
将 Headers 签名中 Header 的 Key 使用英文逗号分割放到 Request 的 Header 中,Key为:X-Ca-Signature-Headers。
1.3. UrlUrl 指 Path + Query + Body 中 Form 参数,组织方法:对 Query+Form 参数按照字典对 Key 进行排序后按照如下方法拼接,如果 Query 或 Form 参数为空,则 Url = Path,不需要添加 ?,如果某个参数的 Value 为空只保留 Key 参与签名,等号不需要再加入签名。
String url =
Path +
"?" +
Key1 + "=" + Value1 +
"&" + Key2 + "=" + Value2 +
...
"&" + KeyN + "=" + ValueN
注意这里Query或Form参数的Value可能有多个,多个的时候只取第一个 Value 参与签名计算。
2. 计算签名
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
// secret为AppSecret
byte[] keyBytes = secret.getBytes("UTF-8");
hmacSha256.init(new SecretKeySpec(keyBytes, 0, keyBytes.length, "HmacSHA256"));
String sign = new String(Base64.encodeBase64(hmacSha256.doFinal(stringToSign.getBytes("UTF-8")),"UTF-8"));
3. 传递签名将计算的签名结果放到 Request 的 Header 中,Key为:X-Ca-Signature。
4. 签名排查当签名校验失败时,API网关会将服务端的 StringToSign 放到 HTTP Response 的 Header 中返回到客户端,Key为:X-Ca-Error-Message,只需要将本地计算的 StringToSign 与服务端返回的 StringToSign 进行对比即可找到问题;如果服务端与客户端的 StringToSign 一致请检查用于签名计算的密钥是否正确;因为 HTTP Header 中无法表示换行,因此 StringToSign 中的换行符都被过滤掉了,对比时请忽略换行符。
5. Java验证示例
private ApiRequest toApiRequest(HttpServletRequest request){
ApiRequest apiRequest = new ApiRequest(HttpMethod.POST_FORM, request.getRequestURI());
//验签
String signHeaders = request.getHeader("X-Ca-Signature-Headers");
List<String> signHeaderList = new ArrayList<>(16);
if (StringUtils.isNotBlank(signHeaders)) {
signHeaderList = Arrays.asList(signHeaders.split(","));
for (String headerName : signHeaderList) {
if (StringUtils.isNotBlank(headerName)) {
apiRequest.addHeader(headerName, request.getHeader(headerName));
}
}
}
apiRequest.addHeader("Accept", request.getHeader("Accept"));
apiRequest.addHeader("Content-MD5", request.getHeader("Content-MD5"));
apiRequest.addHeader("Content-Type", request.getHeader("Content-Type"));
apiRequest.addHeader("Date", request.getHeader("Date"));
Map<String, String> queryParams = new HashMap<>(8);
Map<String, String[]> requestParams = request.getParameterMap();
if (requestParams.size() > 0) {
for (Map.Entry<String, String[]> param : requestParams.entrySet()) {
queryParams.put(param.getKey(), param.getValue().length > 0 ? param.getValue()[0] : "");
apiRequest.addParam(param.getKey(), param.getValue().length > 0 ? param.getValue()[0] : "",
ParamPosition.QUERY, true);
}
}
return apiRequest;
}
第四步:接口授权
选择版本管理下的“模型与权限”,如果您需要访问租户的手机号等信息,则需要开通对应服务。