本文档服务于第三方用户小程序接入兔展作品授权,授权动作为兔展玩法类作品运行、客户档案录入等一系列功能的必要条件。

我需要准备什么?

  1. 本授权功能需接入方企业先在兔展营销云开通 “数据集成” 模块,并获得 “自建应用” 的使用权限。

  2. 自建应用内申请开通 编辑器 → 小程序授权,后续联系管理员审核通过。应用配置界面如下

    此处标记的应用id、授权秘钥请务必妥善保管,需要用于授权信息的加密、解密等操作。

  3. 配置好对应配置项授权appid,授权页路径,点击保存即可。

  4. 为数据安全起见,用户信息mp_userinfo必须使用AES方式进行加密(加密方式见下文),此授权方式不接受任何明文方式传入。

  5. 如果接入方采用接入方式二(见下方授权流程介绍),除接入方需自行实现小程序授权页面外,还需在兔展活动编辑器内为作品添加登录授权按钮,具体配置如下


授权流程介绍:

小程序webview加载兔展作品时候携带加密后的(加密方式见下文)用户信息参数:mp_userinfo & app_id(详见参数说明表)方可自动授权。

注意:为防止作品本身发生默认的公众号授权,除字段 mp_userinfo & app_id 外任何时候都必须要传递 stopAuth=1 以及 previewer=mp 来阻止默认的公众号授权以及标明此作品访问环境为小程序

两种接入方式:

  1. 接入方小程序webview加载兔展作品时在作品链接上直接传入对应授权所需信息字段进行授权。
    优点:类似微信的静默授权,用户无感知。
    缺点:用户的登录状态需接入方自行管理。

  2. C端用户点击作品内部的授权按钮,嵌套在小程序webview的作品会跳转小程序原生授权页(自建应用内 “小程序授权” 配置的授权页路径),并携带参数:redirect_url & app_id(详见参数说明表)进入第三方自行实现的授权页,当授权结束后,小程序需重定向回先前传递的redirect_url(作品地址),并携带加密后的用户信息参数mp_userinfo & app_id 注意别忘记携带 stopAuth=1&previewer=mp 来阻止默认授权行为
    优点:可以在不登录的情况下,通过用户行为主动发起登录授权。
    缺点:需要接入方开发授权页。

参数说明表:

参数

类型

说明

redirect_url
string
当前H5页面地址,包含已有的其他字段
app_id
string
自建应用id

mp_userinfo

string加密后的用户信息
stopAuthstring停止公众号授权(此处永远指定为1
previewerstring标明访问环境为小程序(此处永远指定为mp


mp_userinfo的数据结构(加密前,加密时需要通过JSON.stringify转换成字符串内容):

注意:不管是微信环境下的openid、unionid,还是其他的用户身份标识(audienceUserInfo下的用户识别字段),每个用户每次最多可传入5组身份标识。比如,可以从openid、unionid、会员号、手机号、身份证号、车牌号和社保账号等信息中选择5个,将openid、unionid放到wechatUserInfo下传入,另外再选择3个身份放到audienceUserInfo下传入。

const userInfo = {
  // 传入用户授权信息,包括但不限于昵称、头像及其他用户信息,以及openid、unionid或自定义身份标识等
  wechatUserInfo: {
    nickname: "",
    headimgurl: "",
    sex: 0,
    city: "",
    province: "",
    country: "",
    // 必传字段,值为自建应用id,表明数据来源于某自建应用
    platform: app_id,
    customFields: [
      {
	    fieldValue: ["",""],
	    fieldId: ""
	  }
    ] // 用户自定义字段,对象数组的形式,fieldValue为字符串数组类型,可传入多条数据,fieldId为工作台客户自定义字段的id
  },
  audienceUserInfo: {
    // 使用key value形式传入,key为识别字段的类型(对应工作台-【用户】-【用户身份管理】中的用户身份id),value为对应值,如果需使用微信openid和unionid作为用户身份标识,也许按照此格式在此处传递
    // 如身份证,可传入"idCardNo":"xxxxxxxxxxxxxxx", 此处仅为举例,具体字段类型请根据接入方自身的用户身份识别标识传入
    key: value,
  },
};

Java版AES(ECB)加密demo:

以下为Java版本的AES加密demo。

// 加密方法实现如下(加密的key:自建应用的授权密钥secretkey)

/**
  * AES加密 (默认加密模式: AES/ECB/PKCS5Padding)
  *
  * @param secretKey: 用于加密的key
  * @param content:   待加密明文字符串
  * @return 加密后密文
  */
public static String encrypt(String secretKey, String content) throws Exception {
    // 1.构造密钥生成器,指定为AES算法,不区分大小写
    KeyGenerator keygen = KeyGenerator.getInstance("AES");
    // 2.根据encodeRules规则初始化密钥生成器,生成一个128位的随机源,根据传入的字节数组
    SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
    secureRandom.setSeed(secretKey.getBytes(StandardCharsets.UTF_8));
    keygen.init(128, secureRandom);
    // 3.产生原始对称密钥
    SecretKey originalKey = keygen.generateKey();
    // 4.获得原始对称密钥的字节数组
    byte[] raw = originalKey.getEncoded();
    // 5.根据字节数组生成AES密钥
    SecretKey key = new SecretKeySpec(raw, "AES");
    // 6.根据指定算法AES生成密码器
    Cipher cipher = Cipher.getInstance("AES");
    // 7.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密(Decrypt_mode)操作,第二个参数为使用的KEY
    cipher.init(Cipher.ENCRYPT_MODE, key);
    // 8.获取加密内容的字节数组(这里要设置为utf-8)不然内容中如果有中文和英文混合中文就会解密为乱码
    byte[] byteContent = content.getBytes(StandardCharsets.UTF_8);
    // 9.根据密码器的初始化方式--加密:将数据加密
    byte[] byteAES = cipher.doFinal(byteContent);
    // 10.将加密后的数据转换为字符串并返回
    return parseByte2HexStr(byteAES);
}


/**
  * 将二进制转换为16进制
  *
  * @param buf 待转换二进制数组
  * @return 16进制字符串
  */
private static String parseByte2HexStr(byte[] buf) {
    StringBuilder sb = new StringBuilder();
    for (byte b : buf) {
        String hex = Integer.toHexString(b & 0xFF);
        if (hex.length() == 1) {
            hex = '0' + hex;
        }
        sb.append(hex.toUpperCase());
    }
    return sb.toString();
}

Node.js版AES(ECB)加密demo:

以下为Node.js版本的AES加密demo。

// 加密方法实现如下(加密的key:自建应用的授权密钥secretkey)

const crypto = require("crypto");

/**
 * SHA1PRNG算法
 * @param {string} secretKey
 * @returns {Buffer}
 */
function sha1prng(secretKey) {
  const sha = crypto.createHash("sha1");
  sha.update(secretKey, "utf8");
  let state = sha.digest("buffer");
  const buffer = Buffer.alloc(16);

  const getInt8 = (num) => {
    return num <= 127 ? num : num - 256;
  };

  const updateState = (state, output) => {
    let last = 1;
    let v = 0;
    let t = 0;
    let zf = false;
    for (let i = 0; i < state.length; i++) {
      v = getInt8(state[i]) + getInt8(output[i]) + last;
      t = v & 255;
      zf = zf | (state[i] != t);
      state[i] = t;
      last = v >> 8;
    }
    if (!zf) state[0]++;
    return state;
  };

  let index = 0;
  while (index < 16) {
    const sha = crypto.createHash("sha1");
    sha.update(state);
    let output = sha.digest("buffer");
    state = updateState(state, output);
    const todo = 16 - index > 20 ? 20 : 16 - index;
    for (var i = 0; i < todo; i++) {
      buffer[index++] = output[i];
      output[i] = 0;
    }
  }
  return buffer;
}

/**
 * AES加密 (默认加密模式: AES/ECB/PKCS5Padding)
 *
 * @param {string} secretKey 用于加密的key
 * @param {string} content 待加密明文字符串
 * @returns {string} 加密后的密文
 */
function encrypt(secretKey, content) {
  // 使用128位ECB模式的`AES`加密算法
  const algorithm = "aes-128-ecb";
  // 对自建应用密钥进行`SHA2PRNG`计算,得出加密密钥
  const key = sha1prng(secretKey);
  const cipher = crypto.createCipheriv(algorithm, key, "");
  cipher.setAutoPadding(true);
  // 使用`utf8`字符集,保证中文不是乱码,并输出16进制的字符串
  let encrypted = cipher.update(content, "utf8", "hex");
  encrypted += cipher.final("hex");
  return encrypted.toUpperCase();
}


如果接入方使用非Java或Node.js语言作为服务端语言,请按照AES(ECB)标准自行实现(注意秘钥需使用自建应用的授权秘钥secretkey)。