天天看點

華為雲市場應用接入接口調試

對接華為雲市場,親測調試成功,整理一下給需要的夥伴.
接入華為雲市場saas類商品實作新購商品 商品續費 商品過期 商品資源釋放 商品更新
官方文檔:https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0070649013.html      
華為雲市場應用接入接口調試
目錄結構:      
華為雲市場應用接入接口調試

pojo 請求與響應實體

package com.a.b.isv.pojo;

import java.io.Serializable;
import java.math.BigDecimal;

import com.a.b.isv.domain.ActivityEnum;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * 商品執行個體通知
 *
 * @author l
 * @date 2021/04/01
 */
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class IsvInstance implements Serializable {
    private static final long serialVersionUID = 1L;
   /**
   * 執行個體ID(新購場景無執行個體ID,其餘場景必須要有)
   */
   private String instanceId;
    /**
     * 安全校驗令牌
     */
    private String authToken;
    /**
     * 請求發起時的時間戳,取UTC時間。格式:yyyyMMddHHmmssSSS
     */
    private String timeStamp;
    /**
     * 接口請求辨別,用于區分接口請求場景
     */
    private ActivityEnum activity;
    /**
     * 客戶在華為雲新增賬號的唯一辨別
     */
    private String customerId;
    /**
     * 客戶在華為雲注冊的賬戶名
     */
    private String customerName;
    /**
     * 客戶以IAM使用者認證方式登入時對應子使用者的唯一辨別。
     * 非必傳,如需此參數,在商品釋出時“需要使用者授權”請選擇“基于IAM使用者名建立應用管理賬号等資訊”。
     */
    private String userId;
    /**
     * 客戶以IAM使用者認證方式登入的使用者名。
     * 非必傳,如需此參數,在商品釋出時“需要使用者授權”請選擇“基于IAM使用者名建立應用管理賬号等資訊”。
     */
    private String userName;
    /**
     * 客戶手機号,不包含國家碼。
     * 非必傳,如需此參數,在商品釋出時“需要使用者授權”請選擇“基于手機号碼建立應用管理賬号等資訊”,取值為加密後的手機号碼。
     * 手機号加密規則如下:
     * 由16位iv加密向量和base編碼後的手機号密文組成。
     * iv+base64(AES_CBC(accessKey,mobilePhone))
     * 加密位數取ISV釋出産品時選擇的加密位數。
     */
    private String mobilePhone;
    /**
     * 客戶郵箱。
     * 非必傳,如需此參數,在商品釋出時“需要使用者授權”請選擇“基于郵箱建立應用管理賬号等資訊”,取值為加密後的郵箱。
     * 郵箱加密規則如下:
     * 由16位iv加密向量和base編碼後的郵箱密文組成。
     * iv+base64(AES_CBC(accessKey,email))
     * 加密位數取ISV釋出産品時選擇的加密位數。
     */
    private String email;
    /**
     * 雲市場業務ID。
     * 每一次請求,businessId皆不一緻。
     */
    private String businessId;
    /**
     * 雲市場訂單ID。
     */
    private String orderId;
    /**
     * 産品規格辨別。租戶購買包月或包年的産品後,可能會續費,續費支援變更周期類型(例如包月轉包年),
     * 此時,租戶開通的執行個體instanceId對應的productId會變化,但skuCode不變。
     * 該參數可在商品稽核上架後,進入“賣家中心 > 商品管理 > 我的商品 ”頁面,單擊該商品操作列的“詳情”進入商品詳情頁面擷取
     */
    private String skuCode;
    /**
     * 産品辨別,同一skuCode下,不同周期類型的productId不同。
     * 例如:ISV釋出産品,新增一個規格,會生成一個skuCode,再配置包年價格,包月價格,會生成兩個productId。
     * 該參數可在商品稽核上架後,進入“賣家中心 > 商品管理 > 我的商品 ”頁面,單擊該商品操作列的“詳情”進入商品詳情頁面擷取。
     */
    private String productId;
    /**
     * 是否為調試請求。
     * 1:調試請求
     * 0:非調試請求
     * 取值為“0”時預設不傳。
     */
    private String testFlag;
    /**
     * 是否是開通試用執行個體。
     * 1:試用執行個體
     * 0:非試用執行個體
     * 不傳試用參數:2018年5月12日之前已釋出成功的産品執行個體
     * 取值為“0”時預設不傳。
     */
    private String trialFlag;
    /**
     * 過期時間。
     * 格式:yyyyMMddHHmmss
     * 按周期售賣的商品,會請求該參數。
     * 按次售賣的商品,不會請求該參數。
     * 過期時間根據訂單建立時間和購買周期計算而來,與訂單實際過期時間有誤差,僅供參考。
     */
    private String expireTime;
    /**
     * 計費模式。
     * 3:表示按次購買。
     * 說明:
     * 包周期購買場景請求時不傳該參數。
     * 按次購買場景請求時傳該參數。
     */
    private Integer chargingMode;
    /**
     * 擴充參數。非必填。
     * 擴充參數格式為json數組字元串通過 urlEncode(base64(saasExtendParams))攜帶到url參數中。在得到saasExtendParams參數的值後,需要通過base64Decode(urlDecode(saasExtendParams))擷取擴充參數json數組。
     * 例如:[{"name":"emailDomainName","value":"test.xxxx.com"},{"name":"extendParamName","value":"extendParamValue"}]
     * 其中emailDomainName和extendParamName為釋出商品時填寫值。
     * 說明:
     * (本說明僅适用于WeLink開放平台開發的商品)
     * 請先在應用接入調試頁面調測“WeLink商品接口調測必選參數”,釋出WeLink開放平台開發的商品時,會包含name值為platformParams的擴充參數,值為json格式字元串,包含tenantName、tennantId、userId三個參數。
     * tenantName是WeLink中的企業名
     * tennantId是WeLink中的企業唯一辨別符
     * userId是WeLink中訂閱該應用的使用者,通常為企業管理者(WeLink企業管理者可以有多個且企業管理者賬号可被登出)
     */
    private String saasExtendParams;
    /**
     * 數量類型的商品定價屬性。非必填。
     * 屬性名稱:數量(支援服務商自定義名稱)
     * 機關:個(次)
     * 說明:
     * 對于包周期或一次性計費的SaaS商品,租戶下單購買包含“數量”線性屬性的規格時,會填寫及調整購買的個數或次數。
     * 例如:30個使用者
     */
    private Integer amount;
    /**
     * 數量類型的商品定價屬性。非必填。
     * 屬性名稱:硬碟大小(支援服務商自定義名稱)
     * 機關:GB
     * 說明:
     * 對于包周期或一次性計費的SaaS商品,租戶下單購買包含“硬碟大小”線性屬性的規格時,會填寫及調整購買多少GB。
     * 例如:100GB
     */
    private Integer diskSize;
    /**
     * 數量類型的商品定價屬性。非必填。
     * 屬性名稱:帶寬(支援服務商自定義名稱)
     * 機關:Mbps
     * 說明:
     * 對于包周期或一次性計費的SaaS商品,租戶下單購買包含“帶寬”線性屬性的規格時,會填寫及調整購買多少Mbps。
     * 例如:20Mbps
     */
    private Integer bandWidth;
    /**
     * 周期類型。
     * 說明:
     * 非必傳,如需此參數,計費類型需選擇包周期chargingMode=1,包周期購買場景請求時傳該參數。
     * 年:"year"
     * 月:"month"
     */
    private String periodType;
    /**
     * 周期數量。
     * 說明:
     * 非必傳,如需此參數,計費類型需選擇包周期chargingMode=1,包周期購買場景請求時傳該參數。
     * 周期數量:1,2,3…
     */
    private Integer periodNumber;
    /**
     * 訂單金額。
     * 說明:
     * 該金額為使用者實際支付金額,供服務商對賬參考。
     * 金額值大于等于0,最大三位小數。
     * 機關:元
     */
    private BigDecimal orderAmount;
    /**
     * 商品執行個體開通方式。
     * 說明:
     * 使用者購買後同步開通(預設,雲市場輪詢調用newInstance生産接口)
     * 使用者确認驗收後開通(SaaS涉及服務監管)
     * 使用者購買該商品時,雲市場調用新購商品接口,服務商需傳回結果碼為請求進行中000004或訂單建立成功000000。
     * 當服務商點選開通傳遞時,雲市場調用新購商品接口,服務商需傳回結果碼成功000000。
     * 使用者點選确認驗收時,雲市場調用新購商品傳入使用者驗收時間給到服務商,服務商需傳回結果碼成功000000。
     * ISV接口通知雲市場開通(暫未使用)
     */
    private Integer provisionType;
    /**
     * 使用者驗收時間。
     * 說明:
     * 此時間為使用者計費開始時間,如選擇的商品執行個體開通方式為“使用者确認驗收後開通”,則使用者驗收時間為必填項。
     * 格式:yyyyMMddHHmmssSSS
     */
    private String acceptanceTime;
}      

appInfo

package com.a.b.isv.pojo;

import java.io.Serializable;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * 應用執行個體資訊。
 * 客戶購買商品後,服務商需要傳回登入服務位址(網站位址)或免登位址供客戶後續操作。
 *
 * @author y
 * @date 2021/04/02
 */
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class AppInfo implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 前台位址,客戶購買商品後,可以通路的網站位址。
     */
    private String frontEndUrl;
    /**
     * 管理位址,客戶購買商品後,可以通路的管理背景位址。
     */
    private String adminUrl;
    /**
     * 加密後的管理者帳号。
     * 客戶購買商品後,通路服務商管理背景的賬号(一般為郵箱和手機号)。該值由16位iv加密向量和base編碼後的使用者名密文組成。
     * iv+base64(AES_CBC(accessKey,userName))
     * 需要使用Key值對密碼做加密處理,加密算法以encryptType參數為準。
     */
    private String userName;
    /**
     * 加密後的管理者初始密碼。
     * 客戶購買商品後,通路服務商管理背景的密碼(一般由服務商生成)。該值由16位iv加密向量和base編碼後的密碼密文組成。
     * iv+base64(AES_CBC(accessKey,pwd))
     * 需要使用Key值對密碼做加密處理,加密算法以encryptType參數為準。
     */
    private String password;
    /**
     * 備注。
     * 說明:
     * 如果備注包含中文内容,請将中文轉換成unicode編碼,例如:“中文”可以轉換成“\u4e2d\u6587”。
     */
    private String memo;
}      

ResponseMsg

package com.a.b.isv.core;

import java.io.Serializable;

import com.a.b.isv.domain.ResultCodeEnum;
import com.a.b.isv.pojo.AppInfo;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * 響應消息
 *
 * @author y
 * @date 2021/04/02
 */
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ResponseMsg implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 調用結果碼。
     */
    private String resultCode;
    /**
     * 調用結果描述。
     */
    private String resultMsg;
    /**
     * 應用執行個體資訊。
     * 客戶購買商品後,服務商需要傳回登入服務位址(網站位址)或免登位址供客戶後續操作。
     * 敏感資訊加密算法
     * 1:AES256_CBC_PKCS5Padding(預設值)
     * 2:AES128_CBC_PKCS5Padding
     */
    private String encryptType = "1";
    /**
     * 執行個體ID,服務商提供的唯一辨別。
     */
    private String instanceId;
    /**
     * 應用執行個體資訊。
     * 客戶購買商品後,服務商需要傳回登入服務位址(網站位址)或免登位址供客戶後續操作。
     */
    private AppInfo appInfo;

    public void setResultCode(ResultCodeEnum resultCodeEnum) {
        this.resultCode = resultCodeEnum.getCode();
        this.resultMsg = resultCodeEnum.getLiteral();
    }
}      

ActivityEnum

package com.a.b.isv.domain;


/**
 * 接口請求辨別,用于區分接口請求場景。
 *
 * @author y
 * @date 2021/04/01
 */
public enum ActivityEnum {

	newInstance("新購商品"),
	refreshInstance("續費"),
	expireInstance("商品過期"),
	releaseInstance("商品資源釋放"),
	upgrade("商品更新");

	private final String literal;

	ActivityEnum(String literal) {
		this.literal = literal;
	}
}
      

  

ResultCodeEnum

package com.a.b.isv.domain;

/**
 * 響應碼
 *
 * @author y
 * @date 2021/04/01
 */
public enum ResultCodeEnum  {

	success("000000", "success", "公共"),
	authFailed("000001", "鑒權失敗", "公共"),
	parameterInvalid("000002", "請求參數不合法", "公共"),
	instanceIDNotExist("000003", "執行個體ID不存在", "公共"),
	processing("000004", "請求進行中", "公共"),
	othersFailed("000005", "其它服務内部錯誤", "公共"),

	noEnoughResources("000100", "無可用執行個體資源配置設定", "新購商品");

	private final String code;
	private final String literal;
	private final String module;

	ResultCodeEnum(String code, String literal, String module) {
		this.code = code;
		this.literal = literal;
		this.module = module;
	}

	public String getCode() {
		return code;
	}

	public String getLiteral() {
		return literal;
	}

	public String getModule() {
		return module;
	}

	@Override
	public String toString() {
		return code;
	}
}
      

  

加解密工具

package com.a.b.isv.util;

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.springframework.util.StringUtils;

import cn.hutool.json.JSONObject;

/**
 * 加解密工具
 *
 * @author y
 * @date 2021/04/02
 */
public class SecureUtils {

	/**
	 * 校驗通知消息的合法性
	 *
	 * @param paramsMap     請求通知消息
	 * @param accessKey     接入碼
	 * @param encryptLength 加密長度
	 * @return 驗證結果
	 */
	public static boolean verificationRequestParams(Map<String, Object> paramsMap, String accessKey, int encryptLength) {
		//解析出url内容
		String timeStamp = (String) paramsMap.get("timeStamp");
		String authToken = (String) paramsMap.get("authToken");
		//對剩下的參數進行排序,拼接成加密内容
		Map<String, Object> sortedMap = new TreeMap<>(paramsMap);
		sortedMap.remove("authToken");
		StringBuilder strBuffer = new StringBuilder();
		Set<String> keySet = sortedMap.keySet();
		for(String key : keySet) {
			String value = String.valueOf(sortedMap.get(key));
			strBuffer.append("&")
				.append(key)
				.append("=")
				.append(value);
		}

		//修正消息體,去除第一個參數前面的&
		String reqParams = strBuffer
			.substring(1);
		String key = accessKey + timeStamp;
		String signature = null;
		try {
			signature = generateResponseBodySignature(key, reqParams);
		} catch(InvalidKeyException | NoSuchAlgorithmException
			| IllegalStateException | UnsupportedEncodingException e) {
			// TODO Auto-generated catch block

		}

		return authToken.equals(signature);
	}

	/**
	 * 生成http響應消息體簽名示例Demo
	 *
	 * @param key  使用者在isv console配置設定的accessKey,請登入後檢視
	 * @param body http響應的封包
	 * @return 加密結果
	 * @throws InvalidKeyException
	 * @throws NoSuchAlgorithmException
	 * @throws IllegalStateException
	 * @throws UnsupportedEncodingException
	 */
	public static String generateResponseBodySignature(String key, String body) throws InvalidKeyException,
		NoSuchAlgorithmException, IllegalStateException, UnsupportedEncodingException {

		return base64(hmacSHA256(key, body));
	}

	public static byte[] hmacSHA256(String macKey, String macData)
		throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException {

		SecretKeySpec secret =
			new SecretKeySpec(macKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
		Mac mac = Mac.getInstance("HmacSHA256");
		mac.init(secret);
		return mac.doFinal(macData.getBytes(StandardCharsets.UTF_8));
	}

	/**
	 * 位元組數組轉字元串
	 *
	 * @param bytes 位元組數組
	 * @return 字元串
	 */
	public static String base64(byte[] bytes) {
		return new String(Base64.encodeBase64(bytes));
	}

	/**
	 * 對資源開通後,傳回的使用者名和密碼進行加密
	 *
	 * @param key           秘鑰
	 * @param str           原文
	 * @param encryptLength 加密長度
	 * @return 加密結果
	 */
	public static String generateSaaSUsernameOrPwd(String key, String str, int encryptLength) {

		String iv = getRandomChars(16);
		String afterEncryptStr = "";
		try {
			afterEncryptStr = encryptAESCBCEncode(str, key, iv, encryptLength);
		} catch(InvalidKeyException | NoSuchAlgorithmException
			| NoSuchPaddingException | InvalidAlgorithmParameterException
			| IllegalBlockSizeException | BadPaddingException e) {
			//TODO:異常處理
		}
		return iv + afterEncryptStr;
	}

	/**
	 * 随機生成字元串
	 *
	 * @param length 随機字元串的長度
	 * @return 随機字元串
	 */
	public static String getRandomChars(int length) {

		String randomChars = "";
		SecureRandom random = new SecureRandom();
		for(int i = 0; i < length; i++) {
			//字母和數字中随機
			if(random.nextInt(2) % 2 == 0) {
				//輸出是大寫字母還是小寫字母
				int letterIndex = random.nextInt(2) % 2 == 0 ? 65 : 97;
				randomChars += (char) (random.nextInt(26) + letterIndex);
			} else {
				randomChars += String.valueOf(random.nextInt(10));
			}
		}
		return randomChars;
	}

	/**
	 * AES CBC 位加密
	 *
	 * @param content       加密内容
	 * @param key           加密秘鑰
	 * @param iv            向量iv
	 * @param encryptLength 僅支援128、256長度
	 * @return 加密結果
	 * @throws BadPaddingException
	 * @throws IllegalBlockSizeException
	 * @throws InvalidAlgorithmParameterException
	 * @throws NoSuchPaddingException
	 * @throws NoSuchAlgorithmException
	 * @throws InvalidKeyException
	 */

	public static String encryptAESCBCEncode(String content, String key, String iv, int encryptLength)
		throws InvalidKeyException, NoSuchAlgorithmException,
		NoSuchPaddingException, InvalidAlgorithmParameterException,
		IllegalBlockSizeException, BadPaddingException {

		if(StringUtils.isEmpty(content) || StringUtils.isEmpty(key) || StringUtils.isEmpty(iv)) {
			return null;
		}
		return base64(encryptAESCBC(content.getBytes(StandardCharsets.UTF_8), key.getBytes(StandardCharsets.UTF_8), iv.getBytes(StandardCharsets.UTF_8), encryptLength));
	}

	/**
	 * AES CBC 256位加密
	 *
	 * @param content       加密内容位元組數組
	 * @param keyBytes      加密位元組數組
	 * @param iv            加密向量位元組數組
	 * @param encryptLength 僅支援128、256長度
	 * @return 解密後位元組内容
	 * @throws NoSuchAlgorithmException
	 * @throws NoSuchPaddingException
	 * @throws InvalidKeyException
	 * @throws InvalidAlgorithmParameterException
	 * @throws IllegalBlockSizeException
	 * @throws BadPaddingException
	 */

	public static byte[] encryptAESCBC(byte[] content, byte[] keyBytes, byte[] iv, int encryptLength)
		throws NoSuchAlgorithmException, NoSuchPaddingException,
		InvalidKeyException, InvalidAlgorithmParameterException,
		IllegalBlockSizeException, BadPaddingException {

		KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
		SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
		secureRandom.setSeed(keyBytes);
		keyGenerator.init(encryptLength, secureRandom);
		SecretKey key = keyGenerator.generateKey();
		Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
		cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
		byte[] result = cipher.doFinal(content);
		return result;
	}

	/**
	 * 解密手機号碼或郵箱
	 *
	 * @param key           秘鑰
	 * @param str           密文
	 * @param encryptLength 加密長度
	 * @return 解密結果
	 */
	public static String decryptMobilePhoneOrEMail(String key, String str, int encryptLength) {

		if(null != str && str.length() > 16) {
			String iv = str.substring(0, 16);
			String encryptStr = str.substring(16);
			String result = null;
			try {
				result = decryptAESCBCEncode(encryptStr, key, iv, encryptLength);
			} catch(InvalidKeyException | NoSuchAlgorithmException
				| NoSuchPaddingException | InvalidAlgorithmParameterException
				| IllegalBlockSizeException | BadPaddingException e) {
				//TODO:異常處理

			}
			return result;
		}
		return null;
	}

	/**
	 * 解密AES CBC
	 *
	 * @param content 原文
	 * @param key     秘鑰
	 * @param iv      鹽值
	 * @return 解密結果
	 * @throws BadPaddingException
	 * @throws IllegalBlockSizeException
	 * @throws InvalidAlgorithmParameterException
	 * @throws NoSuchPaddingException
	 * @throws NoSuchAlgorithmException
	 * @throws InvalidKeyException
	 */
	public static String decryptAESCBCEncode(String content, String key, String iv, int encryptType)
		throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException,
		InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {

		if(StringUtils.isEmpty(content) || StringUtils.isEmpty(key)
			|| StringUtils.isEmpty(iv)) {
			return null;
		}
		return new String(decryptAESCBC(Base64.decodeBase64(content.getBytes()),
			key.getBytes(),
			iv.getBytes(), encryptType));
	}

	public static byte[] decryptAESCBC(byte[] content, byte[] keyBytes, byte[] iv, int encryptType)
		throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
		InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {

		KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
		SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
		secureRandom.setSeed(keyBytes);
		keyGenerator.init(encryptType, secureRandom);
		SecretKey key = keyGenerator.generateKey();
		Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
		cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
		return cipher.doFinal(content);
	}
}
      

  

controller

package com.a.b.isv.producer.controller.anonymous;

import javax.servlet.http.HttpServletResponse;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.a.b.isv.conf.IsvConfig;
import com.a.b.isv.core.ResponseMsg;
import com.a.b.isv.pojo.IsvInstance;
import com.a.b.isv.service.IsvService;
import com.a.b.isv.util.SecureUtils;

import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.json.JSONUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * 接入華為雲市場實作新購商品 商品續費 商品過期 商品資源釋放 商品更新
 * 文檔:https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0070649013.html
 *
 * @author*/
@Api(tags = "saas商品接入")
@RequestMapping("isv/")
@RestController
@AllArgsConstructor
@Slf4j
public class IsvController {
    private final IsvService isvService;
    private final IsvConfig isvConfig;

    @ApiOperation("saas商品接入,華為雲市場在客戶購買商品後,将請求本接口")
    @GetMapping("marketplace/saas")
    public String instance(IsvInstance instance, HttpServletResponse response) {
        ResponseMsg responseBody = isvService.instance(instance);
        String signature = null;
        String jsonStr = null;
        try {
        // service傳回的響應轉換為json字元串
            jsonStr = JSONUtil.toJsonStr(responseBody);
        // 對響應json字元串進行簽名
            signature = SecureUtils.generateResponseBodySignature(isvConfig.getAccessKey(), jsonStr);
        } catch(Exception e) {
            log.error("生成響應body簽名異常:{}", ExceptionUtil.getMessage(e));
        }
    // 簽名放入HttpServletResponse 響應頭
        response.addHeader("Body-Sign", "sign_type=\"HMAC-SHA256\"" + "," + "signature=" + "\"" + signature + "\"");
        return jsonStr;
    }
}      

service

package com.a.b.isv.service;

import java.util.Objects;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Service;
import com.a.b.isv.conf.IsvConfig;
import com.a.b.isv.core.ResponseMsg;
import com.a.b.isv.domain.ActivityEnum;
import com.a.b.isv.domain.ResultCodeEnum;
import com.a.b.isv.pojo.AppInfo;
import com.a.b.isv.pojo.IsvInstance;
import com.a.b.isv.util.SecureUtils;

import cn.hutool.cache.impl.TimedCache;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.thread.ThreadUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * SaaS商品接入雲市場
 *
 * @author y
 * @date 2021/03/22
 */

@Service
@AllArgsConstructor
@Slf4j
public class IsvService {
    // 配置檔案包含賣家中心擷取的key值,和各類配置資訊 https://support.huaweicloud.com/accessg-marketplace/zh-cn_topic_0070649065.html
	private final IsvConfig isvConfig;

	public ResponseMsg instance(IsvInstance isvInstance) {
		ResponseMsg responseBody = new ResponseMsg();
		// 校驗通知消息的合法性
		boolean verification = SecureUtils.verificationRequestParams(
			BeanUtil.beanToMap(isvInstance, false, true)
			, isvConfig.getAccessKey(), -1);
		if(!verification) {
			responseBody.setResultCode(ResultCodeEnum.authFailed);
			return responseBody;
		}

          // 業務邏輯處理,此處需要根據自己公司産品進行相應的代碼編寫





          // 響應
		responseBody.setInstanceId(isvInstance.getBusinessId());
		responseBody.setResultCode(ResultCodeEnum.success);

		AppInfo appInfo = new AppInfo();
		appInfo.setFrontEndUrl("http://front.a.com/b/index.html#/user/login");
		appInfo.setAdminUrl("http://front.a.com/b/index.html#/user/login");
		appInfo.setUserName(SecureUtils.generateSaaSUsernameOrPwd(isvConfig.getAccessKey(),"user1000",256));
		appInfo.setPassword(SecureUtils.generateSaaSUsernameOrPwd(isvConfig.getAccessKey(),"user1000",256));
		appInfo.setMemo("hello world");

		responseBody.setAppInfo(appInfo);
		return responseBody;
	}

	/**
	 * 新購場景
	 */
	private void newInstanceScene(IsvInstance isvInstance) {
		log.info("新購場景:");
	}

	/**
	 * 續費場景
	 */
	private void refreshInstanceScene() {
		log.info("續費場景:");
	}

	/**
	 * 商品過期
	 */
	private void expireInstanceScene() {
		log.info("商品過期場景:");
	}

	/**
	 * 商品資源釋放
	 */
	private void releaseInstanceScene() {
		log.info("商品資源釋放場景:");
	}

	/**
	 * 商品更新
	 */
	private void upgradeScene() {
		log.info("商品更新場景:");
	}

	/**
	 * 執行個體是否已存在,并且有效
	 *
	 * @param orderId
	 * @return
	 */
	private boolean isExist(String orderId) {

		return true;
	}
}
      

IsvConfig

@lombok.Getter
@lombok.Setter
@Configuration
@ConfigurationProperties("huawei.isv")
public class IsvConfig {

/**
 * accessKey 登入華為雲市場賣家中心檢視
 */
private String accessKey;
/**
 * 前台位址,客戶購買商品後,可以通路的網站位址。
 */
private String frontEndUrl;
/**
 * 管理位址,客戶購買商品後,可以通路的管理背景位址。
 */
private String adminUrl;
/**
 * 備注。
 * 說明:
 * 如果備注包含中文内容,請将中文轉換成unicode編碼,例如:“中文”可以轉換成“\u4e2d\u6587”。
 */
private String memo;
}