1. 联系管理员配置回调信息
由于云手机侧需要开通用户侧的网路策略,使用回调必须联系管理员添加回调接口信息。
2. 构造回调接口
2.1 接口描述
用于接收云手机侧发送的异步任务的结果信息(成功或者失败)。
2.2 请求方法和URI
请求方式:POST
数据类型:JSON
响应类型:JSON
接口路径:https://xxx.com/chinamobile/cloudphone/async-task/callback
2.3 请求头参数
| 序号 | 名称 | 类型 | 描述 | 必选\可选 |
|---|---|---|---|---|
| 1 | callbackSignature | String | 请求体签名值,用于鉴权 | 可选 |
2.4 请求体参数
| 名称 | 类型 | 描述 | 必选\可选 | |
|---|---|---|---|---|
| 1 | phoneId | String | 云手机ID | 必选 |
| 2 | jobId | String | 任务id | 必选 |
| 3 | beginTime | String | 任务处理开始时间, 时间格式为UTC,YYYY- MM-DDTHH:MM:SSZ | 必选 |
| 4 | endTime | String | 任务处理结束时间, 时间格式为UTC,YYYY- MM-DDTHH:MM:SSZ | 必选 |
| 5 | status | String | 任务状态。2: 成功 -1 :失败 | 必选 |
| 6 | errorCode | String | 任务错误码 | 可选 |
| 7 | errorMsg | String | 任务错误码说明 | 可选 |
| 8 | executeMsg | String | 任务执行返回内容,最长1024个字节 | 可选 |
| 9 | content | String | 任务类容 | 可选 |
| 10 | type | Integer | 任务类型(更新中):1:开机、2:关机、3:重启、4:初始化、5:更换镜像、6:安装应用、7:卸载应用、8:执行shell命令、9:执行shell异步命令、10:推送文件、11:备份、12:恢复、13:绑定云硬盘、14:解绑云硬盘、15:更新云手机属性、16:三合一重置、17:三合一重启、18:重启并重置、19:备份,无pod、20:修改机型设备属性、21:截图、22:推送文件到指定位置、23:更换机型、24:异步应用列表、25:订购、26:退订、27:冻结、28:恢复、29:移入回收站、30:从回收站恢复、31:租户回收分配出去的云手机、32:设置云机互踢属性、33:云机拷贝、34:设置云机应用黑名单 | 可选 |
2.5 返回结果
暂不要求具体格式,云手机侧调用用户提供的回调接口以接口响应的HTTP状态码为200,即视为回调成功。
响应状态码不为200,或是响应时间超过5秒判定为超时,都视为回调失败。若回调失败(客户回调url配置错误、客户回调服务异常、网络波动等)云手机侧会重试回调2次,即最多回调3次。
3. 鉴权校验
云手机侧通过RSA 3072bits非对称加密算法对回调接口的请求体进行私钥加密,生成签名放到请求头中,客户根据公钥对签名值解密,防止第三方篡改回调请求体;
公钥:MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAg77tZpqWyKhaWyDe+Xw+75nJ29hDlisj1+EHoQCMZzrr2pAKMuGPg5mi1pxHlzIjqcM+8iK+E7+qQi8w5F5qzatxgjjkfoe81muCcNSxrfE58YlLHAG32x3LMTLTTd9iMHaiQLXbPQVthLdhu/lN21LRKEXB8inBmb4QXd0YhdwjSNh7TaBHKCfW3trcQq3usEgJM7A2XY8LrpLOBI59VpNFpHJ4w0R/3N5/142eSjHMOFPvw7ZS6V0SyIScCc2Ftam0Ikv9PNzDwxd7C3FrJLYuWdKcwUd/Vq1rhEuLxol1JFyp6WnFfxBoBxxOJSvh/glnaNQLSMLzfG8yoXXUgqr8yVFENN94pfubHxfyOD/nQRxsQ4jrJYfPnzF7xA0f6j4Qz7qLc3d4DZDqeugqDUz695BwN1dOp2egF74GVTGb9JgLqpbaSUJQdkZ80tcOaoawXd4KHBGURqceUovHlDgrbnwu1sFDvoUCsDCaygRtsO/IWlpoyWlCxbRnjiMjAgMBAAE=
回调接口样例:
package com.cmss.ecloud.resource.controller.openapi;
import com.cmss.ecloud.resource.dto.base.openapi.OpenapiBaseResp;
import com.cmss.ecloud.resource.dto.req.openapi.CustomerCallback.OpenApiCallbackMockReq;
import com.cmss.ecloud.resource.service.OpenapiCustomerCallbackExampleService;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
@Slf4j
@Api(tags = "OpenAPI-客户回调管理")
@RestController
@RequestMapping("/api/v1/zycloudphone/resource/zh/phones/callback")
public class OpenapiCustomerCallbackController extends OpenapiBaseController {
@Resource
private OpenapiCustomerCallbackExampleService openapiCustomerCallbackExampleService;
@PostMapping("/callback/example")
public OpenapiBaseResp<String> callbackExample(@RequestBody @Valid final OpenApiCallbackMockReq req, @RequestHeader(value = "callbackSignature", required = false) @Valid final String callbackSignature) {
return success(openapiCustomerCallbackExampleService.callbackExample(req,callbackSignature));
}
}
package com.cmss.ecloud.resource.service;
import com.cmss.ecloud.resource.dto.req.openapi.CustomerCallback.OpenApiCallbackMockReq;
public interface OpenapiCustomerCallbackExampleService {
String callbackExample(OpenApiCallbackMockReq req,String callbackSignature);
}
package com.cmss.ecloud.resource.service.impl;
import com.alibaba.fastjson.JSON;
import com.cmss.ecloud.resource.dto.req.openapi.CustomerCallback.*;
import com.cmss.ecloud.resource.service.OpenapiCustomerCallbackExampleService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
@Slf4j
@Service
public class OpenapiCustomerCallbackExampleServiceImpl implements OpenapiCustomerCallbackExampleService {
private String callbackPublicKey = "MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAg77tZpqWyKhaWyDe+Xw+75nJ29hDlisj1+EHoQCMZzrr2pAKMuGPg5mi1pxHlzIjqcM+8iK+E7+qQi8w5F5qzatxgjjkfoe81muCcNSxrfE58YlLHAG32x3LMTLTTd9iMHaiQLXbPQVthLdhu/lN21LRKEXB8inBmb4QXd0YhdwjSNh7TaBHKCfW3trcQq3usEgJM7A2XY8LrpLOBI59VpNFpHJ4w0R/3N5/142eSjHMOFPvw7ZS6V0SyIScCc2Ftam0Ikv9PNzDwxd7C3FrJLYuWdKcwUd/Vq1rhEuLxol1JFyp6WnFfxBoBxxOJSvh/glnaNQLSMLzfG8yoXXUgqr8yVFENN94pfubHxfyOD/nQRxsQ4jrJYfPnzF7xA0f6j4Qz7qLc3d4DZDqeugqDUz695BwN1dOp2egF74GVTGb9JgLqpbaSUJQdkZ80tcOaoawXd4KHBGURqceUovHlDgrbnwu1sFDvoUCsDCaygRtsO/IWlpoyWlCxbRnjiMjAgMBAAE=";
/**
* 将公钥字符串转化为 公钥对象
* */
private PublicKey loadPublicKeyFromString(String keyString) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(keyString);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(spec);
}
/**
* 对字符串plainText进行SHA-256哈希处理生成哈希值B
* 用RSA-3072bits的公钥对加密过的哈希值A进行解密处理,得到哈希值A
* 判断哈希值A哈希值B与是否一致,一致则证明数据没被篡改
* */
private boolean verify(String plainText, String signature, PublicKey publicKey) throws Exception {
Signature publicSignature = Signature.getInstance("SHA256withRSA");
publicSignature.initVerify(publicKey);
publicSignature.update(plainText.getBytes());
return publicSignature.verify(Base64.getDecoder().decode(signature));
}
@Override
public String callbackExample(OpenApiCallbackMockReq req, String callbackSignature) {
boolean isVerified = false;
if (callbackSignature != null) {
OpenapiCallbackJobInfoDto openapiCallbackJobInfoDto = new OpenapiCallbackJobInfoDto();
//对请求体进行规范处理,便于校验
BeanUtils.copyProperties(req, openapiCallbackJobInfoDto);
String requestBody = JSON.toJSONString(openapiCallbackJobInfoDto);
try {
PublicKey publicKey = loadPublicKeyFromString(callbackPublicKey);
isVerified = verify(requestBody, callbackSignature, publicKey);
}catch (Exception e){
log.error(e.getMessage());
}
if (!isVerified){
return "mock failed";
}
}
return "mock success";
}
}
package com.cmss.ecloud.resource.dto.req.openapi.CustomerCallback;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
@Data
@ApiModel(value = "回调规范化请求体")
public class OpenapiCallbackJobInfoDto {
@ApiModelProperty("云手机ID")
private String phoneId;
@ApiModelProperty("任务ID")
private String jobId;
@ApiModelProperty("任务处理开始时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date beginTime;
@ApiModelProperty("任务处理结束时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date endTime;
@ApiModelProperty("状态:1 :运行中, 2 : 成功,-1 :失败")
private Integer status;
@ApiModelProperty("任务错误码")
private String errorCode;
@ApiModelProperty("任务错误码说明")
private String errorMsg;
@ApiModelProperty("任务执行返回内容,最长1024个字节")
private String executeMsg;
@ApiModelProperty("任务类容")
private String content;
@ApiModelProperty("任务类型(更新中):1:开机、2:关机、3:重启、4:初始化、5:更换镜像、6:安装应用、7:卸载应用、8:执行shell命令、9:执行shell异步命令、10:推送文件、11:备份、12:恢复、13:绑定云硬盘、14:解绑云硬盘、15:更新云手机属性、16:三合一重置、17:三合一重启、18:重启并重置、19:备份,无pod、20:修改机型设备属性、21:截图、22:推送文件到指定位置、23:更换机型、24:异步应用列表、25:订购、26:退订、27:冻结、28:恢复、29:移入回收站、30:从回收站恢复、31:租户回收分配出去的云手机、32:设置云机互踢属性、33:云机拷贝、34:设置云机应用黑名单")
private Integer type;
}
4. 支持回调模式的异步任务OpenAPI
- 云手机开机
- 云手机关机
- 云手机初始化
- 云手机重启
- 异步执行shell命令
- 云手机更新属性
- 恢复云手机数据
- 导出云手机数据
- 推送并解压tar文件
- 云手机卸载应用
- 云手机安装应用
(更新中)