SaaS应用对接

更新时间:

SaaS应用介绍

在应用托管体系中,并未所有应用的分发模式都是按照Kubernetes方式,对每一次应用向最终用户的交付形态,都是交付一个真实的物理运行空间。还有这里要提到的SaaS类应用。此类应用,其分发模式,仅仅是在原有系统中,分配了一个账户,交付给客户。因此,为了兼容单租户模式的独立托管部署和这类多租户的SaaS系统,平台抽象了与这类SaaS系统的交互行为,应用为满足这个交互行为,需要完成本文所提的对接要求。

整体链路

image.png

功能边界

租户管理

image.png

IoT平台管理阿里云租户与应用内租户的映射关系(支持一个阿里云租户拥有同一个应用的多个租户),SaaS管理应用内部的租户结构

组织架构

image.png本期无需对接IoT平台的组织架构体系,SaaS内部自行管理组织架构,但是两边的组织架构需要能够定位到同一个员工身份, 目前通过员工电话作为识别的唯一标识;

权限管理

image.pngIoT平台管理租户及其组织架构中的员工是否有权限访问应用,SaaS管理租户及组织架构中员工访问功能列表;

对接开发

第一步:AppKey的获取

托管应用自动产生

应用部署在应用托管平台中,系统会为部署的每个应用颁发AppKey,AppKey通过环境变量方式注入至业务代码运行的容器中,供代码获取并使用;

人工创建

这种场景适用于您自行通过外置服务器的方式来搭建SaaS服务的模式。创建AppKey的过程如下问:登录卖家中心,在“应用接入”->”应用管理”->“创建应用”账号分发类型:账号分发

选择云端外部接入的模式:外部接入

第二步:应用开发

应用开发,是指按照平台对SaaS了应用的对接要求,要求应用提供指定的接口,或者平台为应用提供必要的接口。目前,平台要求应用提供的接口有3个(生产租户接口、回收租户接口、免密登录接口)。另外,为了实现对接应用与平台之间的账户互认,平台提供了获取当前用户(包括子账户)的手机号。

接口开发1: 生产租户

场景描述

用户(阿里云账户体系)在物联网市场下单购买应用后需要通知SaaS为当前用户开立一个可访问SaaS服务的租户(账户),用户可以通过点击物联网市场中已购买的应用或者直接访问SaaS提供的公网域名来访问应用,当用户购买的应用到期后系统会自动发起SaaS租户的回收操作。所以您需要实现CreateInstance接口,来实现用户购买应用场景。

超时时间

默认接口超时时间为5秒。

调用时序

image.png

接口说明

  1. CreateInstance生产服务

Protocol

HTTPS

Method

POST

  1. 请求参数

参数

类型

必填

描述

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】

  1. 返回参数

参数

类型

描述

code

Integer

调用成功返回200;若调用失败返回203;

message

String

code返回203时填写具体失败原因,code返回200填写success

userId

String

SaaS标识一个租户的唯一ID,对应的appid不同返回的userid也不同

注:同一个用户购买同一款SaaS软件,会多次请求该接口,请求参数中appId不同,saas应用需要返回不同的userId

  1. 验签说明参见【验签说明】

  2. 返回示例

    {
     "code":200, 
     "message":"success", 
     "userId":"my saas user id"
    }
  3. 参数介绍【moduleAttribute】

  • step1: 卖家在进行商品上架时指定额外计费项,当前示例中指定该收费项参数为”service_door”。image.png

  • step2: 买家在市场下单购买商品时输入的额外计费项,当前示例中输入购买数量为200。image.png

  • step3:买家下单后iot平台调用生产租户接口时传递的moduleAttribute参数值为{“service_door”:”200”}

接口开发2: 回收租户

场景描述

用户(阿里云账户体系)在物联网市场下单购买应用后需要通知SaaS为当前用户开立一个可访问SaaS服务的租户(账户),用户可以通过点击物联网市场中已购买的应用或者直接访问SaaS提供的公网域名来访问应用,当用户购买的应用到期后系统会自动发起SaaS租户的回收操作。所以您需要实现DeleteInstance接口,来实现用户购买应用到期回收场景。

超时时间

默认接口超时时间为5秒。

调用时序

image.png

接口说明

  1. DeleteInstance注销服务

Protocol

HTTPS

Method

POST

  1. 请求参数

参数

类型

必填

描述

id

String

该次访问唯一标示符(幂等验证ID)

tenantId

String

IoT平台标识一个租户的唯一ID

userId

String

SaaS标识一个租户的唯一ID

appId

String

应用唯一ID,一个租户可以重复购买一款软件,每次购买appId都不同

  1. 返回参数

参数

类型

描述

code

Integer

调用成功返回200;若调用失败返回203;

message

String

code返回203时填写具体失败原因,code返回200填写success

  1. 验签说明

参见【验签说明】。

  1. 返回示例

    {
     "code":200, 
     "message":"success"
    }

接口开发3: 免密登录

场景描述

用户(阿里云账户体系)在物联网市场下单购买应用后需要通知SaaS为当前用户开立一个可访问SaaS服务的租户(账户),用户可以通过点击物联网市场中已购买的应用来访问应用,这时用户无需再次输入SaaS的账户/密码即可进入SaaS系统进行业务操作,所以您需要实现GetSSOUrl接口,来实现用户访问应用时的免登场景。

安全说明

SaaS返回的SSOUrl必须满足以下两种安全策略中的一种

  • 每次生成的URL只允许**一次使用**

  • 限制临时token的有效时间范围(推荐30秒)

超时时间

默认接口超时时间为5秒。

调用时序

image.png注: SaaS生成的token信息需要带有验证用户身份及时限性,生成和验证的过程全部由SaaS完成,IoT只做免登URL的透传。

接口说明

  1. GetSSOUrl生产服务

Protocol

HTTPS

Method

POST

  1. 请求参数

参数

类型

必填

描述

id

String

该次访问唯一标示符(幂等验证ID)

tenantId

String

IoT平台标识一个租户的唯一ID

tenantSubUserId

String

IoT平台租户组织架构中的员工唯一ID,当员工账号免登时填写

appId

String

应用唯一ID,一个租户可以重复购买一款软件,每次购买appId都不同

userId

String

SaaS标识一个租户的唯一ID

  1. 返回参数

参数

类型

描述

code

Integer

调用成功返回200;若调用失败返回203;

message

String

code返回203时填写具体失败原因,code返回200填写success

ssoUrl

String

免登url,需要带有验证用户身份及时限性的token信息

  1. 验签说明

参见【验签说明】

  1. 返回示例

    {
     "code":200, 
     "message":"success", 
     "ssoUrl":"https://www.test123.com/login.html?ssoToken=xdasfdasdfasfdasfdaf&checkToken=ddddddd"}
    }

接口提供1: 手机获取

场景描述

SaaS服务如果需要租户或者组织架构中员工手机号,可以通过该接口获取;授权过程: IoT平台授权appkey调用该接口权限展示:该权限属于用户隐私数据,所以用户在市场购买时会提示用户当前应用会读取他的手机,并且SaaS不可泄露用户手机号信息。

安全说明

出于安全考虑,单用户手机号只允许获取一次,如果有业务需求请获取后存储于您的系统中,并在相关性协议条款允许范围内使用该信息。

调用时序

image.png

接口说明

  1. GetUserPhone手机号获取服务

protocol

httpMethod

apiVer

url

host

HTTPS

POST

1.0.0

/app/user/info/get

api.link.aliyun.com

  1. 请求参数

参数

类型

必填

描述

tenantId

String

IoT平台标识一个租户的唯一ID

appId

String

应用唯一ID,一个租户可以重复购买一款软件,每次购买appId都不同

tenantSubUserId

String

IoT平台租户组织架构中的员工唯一ID,当员工账号免登时填写。

userId

String

SaaS返回的租户唯一ID

  1. 返回参数

参数

类型

描述

id

String

IoT平台返回的调用唯一ID

code

Integer

调用成功返回200;若调用失败返回错误码;

message

String

code返回非200时填写具体失败原因,code返回200填写success

data

“data”:{ “phone”:”1300000****” }

当tenantSubUserId未填写时返回租户tenantId对应手机号,当tenantSubUserId填写时返回租户下组织架构中tenantSubUserId的手机号

  1. 加签说明

参见【验签说明】

  1. 返回示例

    {
    ​   "​id":"dj182318jjkhsljkdhfjahjdhfla"
     "code":200, 
     "message":"success", 
     "data":{
         "phone":"1300000****"
     }
    }
  2. 错误码列表

错误码

错误信息

描述

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

https://github.com/aliyun/api-gateway-demo-sign-python

php

https://github.com/aliyun/api-gateway-demo-sign-php

c#

https://github.com/aliyun/api-gateway-demo-sign-net

android

https://github.com/aliyun/api-gateway-demo-sign-android

验签

以下将为您介绍如何通过AppKey&AppSecret来对HTTP/HTTPS请求做加签,验签的过程需要您按照以下加签过程重新加签,如果您计算的签名和请求的签名一致即可认为验签成功。

  1. 签名内容

    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;
}

第四步:接口授权

选择版本管理下的“模型与权限”,如果您需要访问租户的手机号等信息,则需要开通对应服务。权限