search
分发文档
分发文档/应用分发/应用服务/测试版本分发/直投2.0测试版本分发技术接入文档
直投2.0测试版本分发技术接入文档更新时间:2025-07-28 14:08:00

一、接入指南

1、直投流程

为了保障安全性,直投2.0针对开放能力做了校验。直投2.0流程如下:

2、直投2.0 Deeplink 参数介绍

2.1 deeplink必要参数

直投的deeplink都是以mimarket://details开头:

参数名参数类型参数含义
idstring跳转的具体应用包名
appClientIdstring小米应用商店为您分配的业务编码
noncestring时间戳参数(生成算法见附录),默认24小时后过期
signstringdeeplink参数加密后的签名
senderPackageNamestring应用跳转来源,即媒体应用包名,可用于后续定向回传下载状态
detailStyleint详情页交互形式 1:全屏
releaseTypeint应用版本类型 1:内测,2:公测
targetVersionCodestring测试应用的版本号

2.2 公测应用自更新

针对公测应用的自更新场景,可以不需要进行后续的签名生成流程。
直投的deeplink格式:mimarket://details?detailStyle=1&id=被分发应用包名&releaseType=应用类型&targetVersionCode=版本号

2.3 获取和使用deeplink的时序图

2.4 直投2.0 deeplink 的生成和使用

每次deeplink都需要重新生成

  • 前提:生成密钥
    • 密钥A:商店生成
    • 密钥B:调用方生成

1、使用keytool工具生成非对称密钥B和公钥证书,其中密钥B要求长度是RSA2(1024位)、格式是PKCS12,公钥证书要求是.cer格式。
参考步骤:

// 1、生成test.keystore文件  
keytool --genkeypair -alias serverkey -sigalg SHA256withRSA -keyalg RSA -keysize 1024 -keystore test.keystore
// 2、生成test.cer文件
keytool -exportcert -keystore test.keystore -storepass xxxx -file test.cer -alias serverkey -rfc
// 3、转换test.p12文件
keytool -importkeystore -srckeystore test.keystore -srcstoretype jks -srcstorepass xxx -srcalias serverkey -deststoretype pkcs12 -deststorepass xxxx -destalias serverkey -destkeystore test.p12

2、将生成好的*.cer公钥证书打包成zip文件, 找销售同学协助邮件(或参考能力申请文档申请密钥A
3、小米应用商店审核通过后,会分配业务编码(appClientId)和密钥A给到申请业务

第一步:deeplink参数拼接
时间戳参数nonce的值按以下算法生成:

import java.security.SecureRandom;  
public static String generateNonce() {
Random random = new SecureRandom();
long n = random.nextLong();
int m = (int) (System.currentTimeMillis() / (1000 * 60));
return n + ":" + m; }

将这些deeplink参数按照参数名的字典序进行升序排序,再将这些参数名及其对应的参数值拼接起来,得到一个字符串,示例如下:

appClientId=test&detailStyle=1&id=com.miui.fm&nonce=-8162956393368422160:27234970&releaseType=1&senderPackageName=com.android.browser&targetVersionCode=88

第二步:deeplink参数签名
为了保证deeplink的安全性,您需要使用商店分配的密钥A与您自行生成的密钥B对第一步拼接得到的字符串进行签名加密操作,加密整体流程如下图所示:
(1)采用HmacSHA256算法对第二步生成的字符串进行加密,加密时需要使用应用商店分配的密钥A,加密得到的字符串即为原始签名,示例如下:56d826a985ff0c7ad58c541130cf436289effd96388f25174290a7c2cfb89153。加密的示例代码如下:

String originalSign = sha256Hmac("appClientId=test&detailStyle=1&id=com.miui.fm&nonce=-8162956393368422160:27234970&releaseType=1&senderPackageName=com.android.browser&targetVersionCode=88", "${商店分配的密钥A}");

(2)采用RSA加密算法对原始签名进行加密,加密时需要使用您自己生成的密钥B。示例代码如下,其中用到的getPrivateKeyByPKCS12和encryptByPrivateKey方法:

//获取私钥  
PrivateKey privateKey = getPrivateKeyByPKCS12("${密钥B对应的p12格式文件路径}", "${密钥B对应的密码}");
//RSA加密
byteresult = encryptByPrivateKey(originalSign.getBytes(), privateKey);

(3)对RSA加密的结果进行base64编码,示例代码如下:

String strBase64 = Base64.encodeBase64String(result);

得到的base64字符串如下:

T2Zps5mRzdvzoXIuBha5em2R6RnmCO2OOXs5LpkHiJrSaOMddTnNC6vWaE1WGNmvCc+9vyzfXrZGgiGYQ3k+St4OQfK2omrDJt0xu8oe3mITFTON31C/3HQ7kU47tCdcJa6iKaWGULE+rgXUbYTUQY/qjaiY8OOGx1ZDBsA85Ts=

url编码后得到的是最终的签名,即为参数sign的值,形如:

T2Zps5mRzdvzoXIuBha5em2R6RnmCO2OOXs5LpkHiJrSaOMddTnNC6vWaE1WGNmvCc%2B9vyzfXrZGgiGYQ3k%2BSt4OQfK2omrDJt0xu8oe3mITFTON31C%2F3HQ7kU47tCdcJa6iKaWGULE%2BrgXUbYTUQY%2FqjaiY8OOGx1ZDBsA85Ts%3D

(5)对于生成的签名sign,建议您使用公钥对其进行解密,验证结果是否与原始签名一致。示例代码如下:

//url解码  
String enResult = URLDecoder.decode(sign, "utf-8");
//获取公钥
PublicKey publicKey = getPublicKeyByX509Cer("${密钥B对应的公钥证书的文件路径}");
//使用公钥解密
bytedecodedResult = decryptByPublicKey(Base64.decodeBase64(enResult), publicKey);
//判断解密结果与原始签名是否一致
System.out.printlin(decodedResult.equals(originalSign));

第三步:deeplink生成
将第二步得到的签名作为参数sign的值拼接到第一步生成的字符串之后,然后在该字符串的开头拼上mimarket://details?前缀,生成最终完整的deeplink,示例如下:

mimarket://details?appClientId=test&detailStyle=1&id=com.miui.fm&nonce=-2062162848931642628:29212297&releaseType=1&senderPackageName=com.android.browser&sign=T2Zps5mRzdvzoXIuBha5em2R6RnmCO2OOXs5LpkHiJrSaOMddTnNC6vWaE1WGNmvCc%2B9vyzfXrZGgiGYQ3k%2BSt4OQfK2omrDJt0xu8oe3mITFTON31C%2F3HQ7kU47tCdcJa6iKaWGULE%2BrgXUbYTUQY%2FqjaiY8OOGx1ZDBsA85Ts%3D&targetVersionCode=88

第四步: deeplink下发和跳转
调用方的服务端生成deeplink后下发到调用方App,在App上通过deeplink跳转应用商店,如果商店验签通过,则会调起指定样式的详情页。客户端调用请在自身的任务栈中发起下载,不要设置intent的 FLAG_ACTIVITY_NEW_TASK,这样不会跳转到商店而中断用户行为。示例代码如下:

var intent = Intent(Intent.ACTION_VIEW);  
intent.setData(Uri.parse("${生成的deeplink}"));
startActivity(intent);

二、注意事项

  • 接入完成后,小米应用商店为您分配的业务编码(appClientId)和密钥请注意妥善保存
  • 本文使用的deeplink必须由广告服务端生成然后下发到广告sdk,一定不要在客户端生成
  • 一个deeplink只能单台设备使用
  • 生成deeplink时建议实时生成,目前默认24小时后deeplink会过期,如果nonce中的时间戳过期会鉴权失败

三、附录

deeplink完整参数

参数名称参数类型是否必须是否参与加签字段说明
idstring需要推广的目标应用包名
appClientIdstring业务编码是广告平台/媒体接入直投的唯一标识
noncelong时间戳参数(生成算法见下),默认24小时后过期
signstring签名
senderPackageNamestring应用跳转来源,即媒体应用包名,可用于后续定向回传下载状态
detailStyleint详情页交互形式,1:全屏
sdkVersionstring有值时参与加签广告sdk版本号,用于后续排查问题
refstring商店打点透传参数,用于标识被点击广告投放的位置,如广告投放在banner或者其他组件,由合作方自己定义
referrerstring有值时参与加签归因参数,标准的归因数据格式,需要进行url编码(具体格式要求参考虚拟归因部分)
urlstring有值时参与加签H5页面地址
ext_apkChannelstring渠道号
releaseTypeint应用的测试类型,1:内测;2:公测
targetVersionCodestring测试应用的版本号

时间戳参数nonce生成算法(java)

import java.security.SecureRandom; 
public static String generateNonce() {
Random random = new SecureRandom();
long n = random.nextLong();
int m = (int) (System.currentTimeMillis() / (100060));
return n + ":"m;
}

deeplink生成示例(java)

package rsa;

import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher; import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.Random;
public class Example {
private static final int KEY_SIZE = 2048;
private static final int GROUP_SIZE = KEY_SIZE / 8;
private static final int ENCRYPT_GROUP_SIZE = GROUP_SIZE - 11;
/**
* 生成时间戳参数nocne,格式为 随机long型整数:当前时间戳转换为分钟单位
*
* @return
* @throws Exception
*/
public static String generateNonce() throws Exception {
Random random = new SecureRandom();
long n = random.nextLong();
int m = (int) (System.currentTimeMillis() / (1000 * 60));
return n + ":" + m;
}
/**
* 私钥加密
*
* @param data
* @return
* @throws Exception
*/
public static byte[] encryptByPrivateKey(byte[] data, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return codeImpl(data, cipher, ENCRYPT_GROUP_SIZE);
}
/**
* 公钥解密
*
* @param data
* @return
* @throws Exception
*/
public static byte[] decryptByPublicKey(byte[] data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return codeImpl(data, cipher, GROUP_SIZE);
}
private static byte[] codeImpl(byte[] data, Cipher cipher, int groupSize) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int idx = 0;
while (idx < data.length) {
int remain = data.length - idx;
int segsize = remain > groupSize ? groupSize : remain;
baos.write(cipher.doFinal(data, idx, segsize));
idx += segsize;
}
return baos.toByteArray();
}
/**
* 获取私钥
*
* @param p12Is
* @param password
* @return * @throws Exception
*/
public static PrivateKey getPrivateKeyByPKCS12(InputStream p12Is, String password) throws Exception {
try {
KeyStore ks = KeyStore.getInstance("PKCS12");
char[] pwd = null;
if ((password == null) || password.trim().equals("")) {
pwd = null;
}
else {
pwd = password.toCharArray();
}
ks.load(p12Is, pwd);
Enumeration<String> aliases = ks.aliases();
String keyAlias = null;
if (aliases.hasMoreElements()) {
keyAlias = aliases.nextElement();
}
return (PrivateKey) ks.getKey(keyAlias, pwd);
} finally {
if (p12Is != null) {
try {
p12Is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static PrivateKey getPrivateKeyByPKCS12(String p12File, String password) throws Exception {
return getPrivateKeyByPKCS12(new FileInputStream(p12File), password);
}
/**
* 获取公钥
*
* @param cerfile
* @return * @throws Exception
*/
public static PublicKey getPublicKeyByX509Cer(String cerfile) throws Exception {
return getPublicKeyByX509Cer(new FileInputStream(cerfile));
}
private static PublicKey getPublicKeyByX509Cer(InputStream x509Is) throws Exception {
try {
CertificateFactory certificatefactory = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) certificatefactory.generateCertificate(x509Is);
System.out.println(JSONObject.toJSONString(cert));
System.out.println(cert.getIssuerDN().getName());
System.out.println(cert.getIssuerX500Principal().getName());
return cert.getPublicKey();
} finally {
if (x509Is != null) {
try {
x509Is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* sha256_HMAC加密
*
* @param message 消息
* @return 加密后字符串
*/
public static String sha256Hmac(String message, String serverkey) {
String hash = "";
try {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(serverkey.getBytes(), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] bytes = sha256_HMAC.doFinal(message.getBytes());
hash = byteArrayToHexString(bytes);
} catch (Exception e) {
System.out.println("Error HmacSHA256 ===========" + e.getMessage());
}
return hash;
}
/**
* 将加密后的字节数组转换成字符串
*
* @param b 字节数组
* @return 字符串
*/
public static String byteArrayToHexString(byte[] b) {
StringBuilder hs = new StringBuilder();
String stmp;
for (int n = 0; b != null && n < b.length; n++) {
stmp = Integer.toHexString(b[n] & 0XFF);
if (stmp.length() == 1)
hs.append('0');
hs.append(stmp);
}
return hs.toString().toLowerCase();
}
public static void main(String[] args) throws Exception {
// 原始数据(所有按照按照ascii升序排列),nonce采用generateNonce()方法生成
String info = "appClientId=test&detailStyle=1&id=com.miui.fm&nonce=-2062162848931642628:29212297&releaseType=1&senderPackageName=com.android.browser&targetVersionCode=88";
// 加签
String originalSign = sha256Hmac(info, "${商店分配的密钥A}");
System.out.println("生成的签名:" + originalSign);
// 对签名利用私钥加密保护
// 获取私钥
PrivateKey privateKey = getPrivateKeyByPKCS12("${密钥B对应的p12格式文件路径}", "${密钥B的密码}");
// RSA加密
byte[] result = encryptByPrivateKey(originalSign.getBytes(), privateKey);
// base64编码
String strBase64 = Base64.encodeBase64String(result);
// url编码
String sign = URLEncoder.encode(strBase64, "utf-8");
System.out.println("最终签名:" + sign);
// 传输 到商店。。。。。。。。。。。。。。。。。。。经过网络传输
// url解码
String enResult = URLDecoder.decode(sign, "utf-8");
// 获取公钥
PublicKey publicKey = getPublicKeyByX509Cer("${密钥B对应的公钥证书的文件路径}");
// 使用公钥解密
byte[] decodedResult = decryptByPublicKey(Base64.decodeBase64(enResult), publicKey);
String decodedSign = new String(decodedResult, "utf-8");
System.out.println("解密后的签名:" + decodedSign);
//判断解密结果与原始签名是否一致
System.out.println(decodedSign.equals(originalSign));
}
}

测试用例
注:仅用于本地代码测试验证
测试密钥数据
用来测试的私钥(密钥B)
名称:test.p12
地址:https://kpan.mioffice.cn/webfolder/ext/K8Lz8GJFMuw%40?n=0.9659223941950801
密码:dm11
https://xiaomi.f.mioffice.cn/docx/doxk4D1TsYWSmpFdP8jfLV4Q3vb#doxk4DMAFvuiOi9ytNqzp6wTdgg
密码:123456
用来测试的公钥(密钥B对应的公钥证书)
名称:test.cer
地址:https://kpan.mioffice.cn/webfolder/ext/7k5T4od%23yx4%40?n=0.5354798806971743
密码:dm11
https://xiaomi.f.mioffice.cn/docx/doxk4D1TsYWSmpFdP8jfLV4Q3vb#doxk4G3B6qOsQppYGLY6e65ei9e

-----BEGIN CERTIFICATE-----
MIICcjCCAdugAwIBAgIEOTfYLDANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdV
bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD
VQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3du
MB4XDTIzMTExNDEyMTgwNVoXDTI0MDIxMjEyMTgwNVowbDEQMA4GA1UEBhMHVW5r
bm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UE
ChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCB
nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAh6xO38mlOEzEKmzun5of8I3Fojgc
VdW4vv32ZcDFmyUMum9wG7R5Tny4C1RWp/HCmlGEE3TizxNpqpE1x7dliUP32l0E
SkMUWlIKpWXAEBJgtRmcoW0lG3yZjN+ytfpRDHV+GWathEBKyJsPZbGg5U1X72hR
7VS7NU0D9N3qFk8CAwEAAaMhMB8wHQYDVR0OBBYEFBJTHr3IcPrfYPKnrZIUCzi9
wpHGMA0GCSqGSIb3DQEBCwUAA4GBAELvU1FNIrjJw/DhlowZk4SLkSXZYpRGKsdT
boY1tpwzOPci0c0vBghhTrD6QPMFGbRZaOgr4MrgOQPL/ndqYX7EPypXImdyyHgO
l6l2qG4WNHhJHb3HjgWWtZTTDh2vm7U79A8GUdDUiOSCeAEsb64X007TE76kXGYn
wZoAbC0L
-----END CERTIFICATE-----

用来测试的商店分配的密钥A

wcEvOZqgNtOURyNMdOm8Rg==

测试输出数据

  • 原始数据:

appClientId=test&detailStyle=1&id=com.miui.fm&nonce=-2062162848931642628:29212297&releaseType=1&senderPackageName=com.android.browser&targetVersionCode=88

  • 原始签名:

56d826a985ff0c7ad58c541130cf436289effd96388f25174290a7c2cfb89153

  • 私钥加密后Base64编码后的签名:

T2Zps5mRzdvzoXIuBha5em2R6RnmCO2OOXs5LpkHiJrSaOMddTnNC6vWaE1WGNmvCc+9vyzfXrZGgiGYQ3k+St4OQfK2omrDJt0xu8oe3mITFTON31C/3HQ7kU47tCdcJa6iKaWGULE+rgXUbYTUQY/qjaiY8OOGx1ZDBsA85Ts=

  • URL编码后的签名(即DP上的签名):

T2Zps5mRzdvzoXIuBha5em2R6RnmCO2OOXs5LpkHiJrSaOMddTnNC6vWaE1WGNmvCc%2B9vyzfXrZGgiGYQ3k%2BSt4OQfK2omrDJt0xu8oe3mITFTON31C%2F3HQ7kU47tCdcJa6iKaWGULE%2BrgXUbYTUQY%2FqjaiY8OOGx1ZDBsA85Ts%3D

常见直投错误码

错误码问题说明
11、16参数错误。请检查deeplink必须参数是否完整。
12链接过期。生成deeplink时建议实时生成,目前默认24小时后deeplink会过期,如果nonce中的时间戳过期会鉴权失败。
13签名无效。一般是由于加解密或编码错误导致的,可以参考附录2的测试用例进行排查。
14无效设备。一个deeplink只能单台设备使用。
15链接使用超限。单个Deeplink在单台设备上30min中内的请求次数,默认上限为30次。
18调起媒体错误。deeplink上的senderPackageName参数与实际调起deeplink的媒体包名不一致。
19直投编码错误。可以联系商店侧进行直投配置排查。
24媒体不在白名单。
如果未配置媒体白名单,则可能是以上各种错误导致鉴权失败,然后触发兜底鉴权逻辑从而返回该错误码,需要联系商店侧技术人员进行进一步排查。
28直投当前阶段仅开放应用品类,对游戏品类不适用

四、常见问题QA

Q1: “客户端接入时,客户端调用请在自身的任务栈中发起下载,不要设置intent的 FLAG_ACTIVITY_NEW_TASK”,这个是不是必须的?
A1: 非必须,客户端调用请在自身的任务栈中发起下载,不要设置intent的 FLAG_ACTIVITY_NEW_TASK,目的是这样不会跳转到商店而中断用户行为,提高下载率。当未设置FLAG_ACTIVITY_NEW_TASK时,startActivity 时,不要使用application/service的context,需要使用activity的context。


Q2:senderPackageName是什么?有没有什么限制?
A2:senderPackageName是媒体APP包名或宿主APP包名,比如从小米浏览器跳转商店详情页,senderPackageName就是小米浏览器的包名。senderPackageName有安全性限制,如果广告平台/媒体有诉求的话,可以将指定的宿主APP包名加入安全授权白名单


Q3:证书、密钥B、appClientId和密钥A有没有过期时间?
A3:都是一次性生成好的,没有过期时间


Q4:如果宿主APP没有广告SDK,sdkVersion该如何填?
A4:可以填宿主APP的版本号


Q5:referrer如何填写?source, medium, campaign, term, content 中哪些是必须参数?
A5:source, medium, campaign, term, content是归因参数的模板规范,根据合作方需求可以选择其中任何一个或几个,非必填


content://com.xiaomi.marked.provider.DirectMailProvider 接口

Q6:此接口是客户端需要调用的接口吗?
A6:是的,调用provider标准接口查询商店是否支持相应功能


Q7:此接口的调用是否会影响性能?
A7:不会,本地测试的查询时间平均5ms左右


Q8:调起Deeplink展示页面错误文案为「页面无法打开,您可联系应用开发者以解决此问题。(链接参数格式有误)」
A8:可能的原因如下:

  • 内测应用的自升级缺失签名等参数
  • 缺少必备参数detailStyle、releaseType、targetVersionCode
  • releaseType填写了非法值,目前仅支持值1、2,请检查应用类型是否正确
  • targetVersionCode填写了非法值,请检查版本号是否正确
  • 不支持直投映射

上一篇:内测/公测分发统一链接构建指南
下一篇:卸载数据服务使用说明
文档内容是否有帮助?
有帮助
无帮助