0x01 分析
同一主体下微信小程序和微信公众号下,同一个用户在不同的公众平台下openid是不同的,但是unionid是相同的,因此若需要创建同主体跨公众平台的系统时候,用户的unionid一定要记录下来。我遇到的场景是小程序已经上线,但是公众号还没有推广,但是未来的运营计划是根据小程序的用户行为在微信公众号进行模板消息推送,因此需要首先收集微信小程序用户的unionid。
要收集unionid,就要首先知道怎么在小程序中获取unionid,微信小程序的官方文档中给出以下三种方式
- 调用接口wx.getUserInfo,从解密数据中获取UnionID。注意本接口需要用户授权,请开发者妥善处理用户拒绝授权后的情况。
- 如果开发者帐号下存在同主体的公众号,并且该用户已经关注了该公众号。开发者可以直接通过wx.login获取到该用户UnionID,无须用户再次授权。
- 如果开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用。开发者也可以直接通过wx.login获取到该用户UnionID,无须用户再次授权。
- 文档地址:https://developers.weixin.qq.com/miniprogram/dev/api/uinionID.html
后两种我们不符合,因为不能保证我们的用户已经关注授权了我们的公众号,所以只能使用第一种方法。
0x02 开始折腾
根据微信小程序的login文档和getUserInfo文档进行操作,我们拿到了如下三项需要用到的数据:
encryptedData = +6fr9M+bNeRk8LQOOuxqBLuzsydnIh7D2UvImuWicfzJsbrRFptPr7yXHubgTdRd6JHwvjDXD+Q9L0oeTjXlBZilfipjRZJSV7nODB5wQr6hKAPsvLmUjOpTPocpVrRDbkXRQKAgl6uTXkR8SdUL3j0zihANr3ANaz2kgg8X+iCJKS7lOns3ZW8V4KxfYntvLBN7LJEfOEfYcyemNYtVt3ALDE2sOxI4pb8XxUUO3zn+Yt7e5xWqmfqFj1YK3CCl0ILHle5JlZxGz178PsztDoCnOMoA4NwEoGfqCsUH1pM7AsyfcOh27yqqk=
sessionKey = 2gCRTCyEp5F89yg==
iv = XBzf8VMLBIKSOWNg==
微信官方文档链接顺便附上,方便小伙伴们直接查看:
https://developers.weixin.qq.com/miniprogram/dev/api/api-login.html
https://developers.weixin.qq.com/miniprogram/dev/api/open.html#wxgetuserinfoobject
这三个字段,其中encryptedData是密文,sessionKey是密钥,iv是偏移量,我们需要根据密钥和偏移量对密文进行解密,微信官方给出如下算法说明:
- 对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充。
- 对称解密的目标密文为 Base64_Decode(encryptedData)。
- 对称解密秘钥 aeskey = Base64_Decode(session_key), aeskey 是16字节。
- 对称解密算法初始向量 为Base64_Decode(iv),其中iv由数据接口返回。
- 解密相关文档地址:
https://developers.weixin.qq.com/miniprogram/dev/api/signature.html#wxchecksessionobject
翻阅文档,发现文档中有这么一段话:
微信官方提供了多种编程语言的示例代码(点击下载)。每种语言类型的接口名字均一致。调用方式可以参照示例。
然后我点击下载链接下载实例代码后,发现有C++,NodeJS,PHP,Python,并没有Java版本,真是醉了,Java在腾讯眼里这么不重视吗?看来只能自己搞了。
查阅不少资料,发现JDK中的库不能完成微信用到的PKCS#7加密方式,因此只能引用第三方库来进行解密,这里我采用的是org.bouncycastle.bcprov-jdk15on
,可以使用maven的方式直接引入:
org.bouncycastle
bcprov-jdk15on
1.57
我用的是1.57版本,最新的是1.59,但是担心稳定性问题并没有使用,读者可以自行下载尝试,我就直接用这个了。如果你没有使用maven,可以直接从这里下载jar包:
http://central.maven.org/maven2/org/bouncycastle/bcprov-jdk15on/1.57/bcprov-jdk15on-1.57.jar
接下来,我们使用该类库直接进行数据解密,代码如下:
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.Security;
/**
* 微信工具类
*/
public class WechatUtil {
public static void main(String[] args) {
String result = decryptData(
"+6fr9M+bNeRk8LQOOuxqBLuzsydnIh7D2UvImuWicfzJsbrRFptPr7yXHubgTdRd6JHwvjDXD+Q9L0oeTjXlBZilfipjRZJSV7nOpaq++DB5wQr6hKAPsvLmUjOpTPocpVrRDbkXRQKAgl6uTXkR8SdUL3j0zihANr3ANaz2kgg8X+iCJKSxmCuxwPswFCrYaXih2Z7+s/EqWU0ACFgaoZMNkliYBy9mF/pwzCfzsDVx+eJu1eG2UmU6e0e8rUOcEGv4KxfYntvLBN7LJEfOEfYcyemNYtVt3ALDE2sOxI4pb8XxUUO3zn+Yt7e5xWqmfqFj1YK3CCl0ILHle5JlZxGz178PsztDoCnOMoA4NwEoGfqCsUH1pM7AsyfcOh27yqqk=",
"2gCRTCyEpjQTW5Fc89yg==",
"XBzf8VMLBIsFBZvjOWNg=="
);
System.out.println("result = " + result);
}
public static String decryptData(String encryptDataB64, String sessionKeyB64, String ivB64) {
return new String(
decryptOfDiyIV(
Base64.decode(encryptDataB64),
Base64.decode(sessionKeyB64),
Base64.decode(ivB64)
)
);
}
private static final String KEY_ALGORITHM = "AES";
private static final String ALGORITHM_STR = "AES/CBC/PKCS7Padding";
private static Key key;
private static Cipher cipher;
private static void init(byte[] keyBytes) {
// 如果密钥不足16位,那么就补足. 这个if 中的内容很重要
int base = 16;
if (keyBytes.length % base != 0) {
int groups = keyBytes.length / base + (keyBytes.length % base != 0 ? 1 : 0);
byte[] temp = new byte[groups * base];
Arrays.fill(temp, (byte) 0);
System.arraycopy(keyBytes, 0, temp, 0, keyBytes.length);
keyBytes = temp;
}
// 初始化
Security.addProvider(new BouncyCastleProvider());
// 转化成JAVA的密钥格式
key = new SecretKeySpec(keyBytes, KEY_ALGORITHM);
try {
// 初始化cipher
cipher = Cipher.getInstance(ALGORITHM_STR, "BC");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 解密方法
*
* @param encryptedData 要解密的字符串
* @param keyBytes 解密密钥
* @param ivs 自定义对称解密算法初始向量 iv
* @return 解密后的字节数组
*/
private static byte[] decryptOfDiyIV(byte[] encryptedData, byte[] keyBytes, byte[] ivs) {
byte[] encryptedText = null;
init(keyBytes);
try {
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ivs));
encryptedText = cipher.doFinal(encryptedData);
} catch (Exception e) {
e.printStackTrace();
}
return encryptedText;
}
}
控制台输出如下:
Connected to the target VM, address: '127.0.0.1:58393', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:58393', transport: 'socket'
result = {"openId":"o5IPx0AnPfqqSxjH3kw","unionId":"XXXXXXXX","nickName":"LemonIT.CN","gender":1,"language":"zh_CN","city":"Dalian","province":"Liaoning","country":"China","avatarUrl":"https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTIEzBD4x5Ot6ERHPEUTOcsdff8XkExqUt5l7nPxAlS7E6hrH8yibxmKAHZqA1JyNI4Is7fBSmINVuw/0","watermark":{"timestamp":1524314705,"appid":"XXXXXXXXXXXXX"}}
Process finished with exit code 0
0x03 说明,注意!
- 第二部中涉及到的密文和敏感信息我都进行了处理,因此你从我的密文和密钥无法直接解密测试,还烦请读者自己亲自申请相关数据进行解密测试。不过可以放心的是WechatUtil这个类可以直接使用
- 如果出现方法报错,请读者注意一下与我提供的代码中的import是否匹配,是否引错了包。
- 如果你解密出来的数据没有unionid,请您确认您的微信小程序是否已经绑定了微信开放平台!!一定要注意,因为我已经踩坑了。只有绑定之后才能获取到unionid!!!
- 微信开放平台地址:https://open.weixin.qq.com
- 微信开放平台的账号和现有的公众平台账号不能共用同一邮箱等账号信息,需要重新注册一个新账号,重新走一遍认证流程,重新交300块钱进行一次审核!!!坑...