在小程序开发中,获取微信用户绑定的手机号功能,详细可查看官方文档:微信官方文档 · 小程序 获取手机号
获取微信用户绑定的手机号,需先调用wx.login接口。
因为需要用户主动触发才能发起获取手机号接口,所以该功能不由 API 来调用,需用 button 组件的点击来触发。
注意:目前该接口针对非个人开发者,且完成了认证的小程序开放(不包含海外主体)。需谨慎使用,若用户举报较多或被发现在不必要场景下使用,微信有权永久回收该小程序的该接口权限。
前端
需要将 button 组件 open-type 的值设置为 getPhoneNumber,当用户点击并同意之后,可以通过 bindgetphonenumber 事件回调获取到微信服务器返回的加密数据, 然后在第三方服务端结合 session_key 以及 app_id 进行解密获取手机号。
<button open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">获取手机号
methods: {
getPhoneNumber(e){
if(e.detail.errMsg == "getPhoneNumber:ok"){
console.log('用户同意提供手机号');
console.log(JSON.stringify(e.detail.encryptedData));
console.log(JSON.stringify(e.detail.iv));
var encryptedData = e.detail.encryptedData;
var iv = e.detail.iv;
}
}
},
注意:
在回调中调用 wx.login 登录,可能会刷新登录态。此时服务器使用 code 换取的 sessionKey 不是加密时使用的 sessionKey,导致解密失败。建议开发者提前进行 login;或者在回调中先使用 checkSession 进行登录态检查,避免 login 刷新登录态。
以上基本都是官网内容,接下来咱们分析一下获取流程。
- 前端 wx.login() 用户授权 -> 获取登录用户code值 -> 发送给后端
- 后端根据code和小程序的appId和appSecret调用微信官方接口获取用户的session_key和openId 微信官方文档 · 小程序 服务端 code2Session
- 前端通过button调用微信的获取手机号功能
- 微信根据当前用户的session_key(也就是刚刚后端通过code获取的code值)和用户手机号等敏感数据 进行签名/加密
- 微信将数据+签名+密文串 发送给小程序,因为没有发送session_key,因此无法伪造签名,无法解密
- 前端小程序将数据+签名+密文串 发给后端,后端进行解密得到想要的数据。
微信官方文档 · 小程序 解密
其中有疑问的点是,第2步、第6步中,后端用到的session_key。sessionKey是用户的标识,也就是说每个用户同一时间点击进入的时候是不一致的。而且每个用户在wx.login之后,也是会刷新sessionKey的。
据此,我的想法是:因为sessionKey是根据code换来的,每次调用wx.login之后,code也会跟sessionKey一起变。也就是说code跟sessionKey可以认为是同一个东西,至少同一个作用。
所以,后端在第2步根据code获取sessionKey的时候,将code和sessionKey存到了缓存中。也让前端将用户的code标识存在客户端缓存中。
第5步调用的时候也将缓存中的code传递给后端,后端通过当前code去缓存里取出sessionKey来解密。
结论:
- 前端 wx.login() 用户授权 -> 获取登录用户code值 -> 发送给后端 并将code缓存一份
- 后端根据code和小程序的appId和appSecret调用微信官方接口获取用户的session_key和openId ,并将code为key,session_key为value缓存一份
- 前端通过button调用微信的获取手机号功能
- 微信根据当前用户的session_key(也就是刚刚后端通过code获取的code值)和用户手机号等敏感数据 进行签名/加密
- 微信将数据+签名+密文串 发送给小程序,因为没有发送session_key,因此无法伪造签名,无法解密
- 前端小程序将数据+签名+密文串 +缓存中的code 发给后端,后端 根据code去缓存中获得session_key 进行解密得到想要的数据。
细节:
缓存中的值我设置的3天的过期时间
微信开放社区 小程序登录session_key的有效期问题 2018年
注意:这里即使是微信开放社区,但是也是2018年的回复,所以是否为3天这里不做证明与验证,至于过期时间需按照公司实际业务需求确定:如有用户量较大,服务器内存不足,redis占用过大等问题。可适当调整,因为每个用户退出小程序后,过段时间在进入时,可能需要再次授权获取code值,所以也会导致之前的code失效,redis存入过期数据。而且微信官方一直强调,不提供session_key的过期时间,微信不会把 session_key 的有效期告知开发者。我们会根据用户使用小程序的行为对 session_key 进行续期。用户越频繁使用小程序,session_key 有效期越长。
前端获取手机号时 取code值的逻辑:
- 判断缓存中是否有code值
-
如果缓存中有code值
- 根据 wx.checkSession 判断session_key是否有效
- 如果有效,则将当前code传递给后端
- 如果无效,则调用wx.login(后端会在wx.login的时候将code和session_key存入缓存),之后将code传递给后端
- 根据 wx.checkSession 判断session_key是否有效
-
如果缓存中没有code值
- 则调用wx.login(后端会在wx.login的时候将code和session_key存入缓存)
注意:
签名校验以及数据加解密涉及用户的会话密钥 session_key。 开发者应该事先通过 wx.login 登录流程获取会话密钥 session_key 并保存在服务器。为了数据不被篡改,开发者不应该把 session_key 传到小程序客户端等服务器外的环境。
会话密钥 session_key 有效性
开发者如果遇到因为 session_key 不正确而校验签名失败或解密失败,请关注下面几个与 session_key 有关的注意事项。
- wx.login 调用时,用户的 session_key 可能会被更新而致使旧 session_key 失效(刷新机制存在最短周期,如果同一个用户短时间内多次调用 wx.login,并非每次调用都导致 session_key 刷新)。开发者应该在明确需要重新登录时才调用 wx.login,及时通过 auth.code2Session 接口更新服务器存储的 session_key。
- 微信不会把 session_key 的有效期告知开发者。我们会根据用户使用小程序的行为对 session_key 进行续期。用户越频繁使用小程序,session_key 有效期越长。
- 开发者在 session_key 失效时,可以通过重新执行登录流程获取有效的 session_key。使用接口 wx.checkSession可以校验 session_key 是否有效,从而避免小程序反复执行登录流程。
- 当开发者在实现自定义登录态时,可以考虑以 session_key 有效期作为自身登录态有效期,也可以实现自定义的时效性策略。
附:解密代码
需要提前导入的maven依赖
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.46</version>
</dependency>
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.validation.constraints.NotNull;
import java.security.Security;
import java.security.spec.AlgorithmParameterSpec;
@Slf4j
public class AESUtils {
// 加密模式
private static final String ALGORITHM = "AES/CBC/PKCS7Padding";
private static final String CHARSET_NAME = "UTF-8";
private static final String AES_NAME = "AES";
//解决java.security.NoSuchAlgorithmException: Cannot find any provider supporting AES/CBC/PKCS7Padding
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* 解密
*
* @param content 目标密文
* @param key 秘钥
* @param iv 偏移量
* @return
*/
public static String decrypt(@NotNull String content, @NotNull String key, @NotNull String iv) {
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
byte[] sessionKey = java.util.Base64.getDecoder().decode(key);
SecretKeySpec keySpec = new SecretKeySpec(sessionKey, AES_NAME);
byte[] ivByte = java.util.Base64.getDecoder().decode(iv);
AlgorithmParameterSpec paramSpec = new IvParameterSpec(ivByte);
cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec);
return new String(cipher.doFinal(Base64.decodeBase64(content)), CHARSET_NAME);
} catch (Exception e) {
log.error("解密失败:{}", e);
e.printStackTrace();
}
return StringUtils.EMPTY;
}
}
测试:
public static void main(String[] args) {
String sessionKey = "zF6lhhRqTdWJ8sb45RTxsw==";
String encryptedData = "JXZ5dxBn7EqgRWTbqt50rxrN69Y9okDdL0YzvrSwNjKA9blYJagZbhovcwbhFy8vVaqjVVEjIl451JOCXIB2fpNpq0sbIxV+B28pKWLA8y2jn7R1iTE7O7k/tW1yVDMZwqRQyTw9lV/qlISw+HX887DeVWCfem6lx8jZ/C+kshJdig4Li06AIA9A9smToZYI";
String iv = "CO5eq/F5TTv9SuwiMLDNaA==";
String decrypt = AESUtils.decrypt(encryptedData, sessionKey, iv);
System.out.println(decrypt);
}
将json转成map:
Gson gson = new Gson();
Map<String,Object> stringList = gson.fromJson(decrypt, new TypeToken<Map<String,Object>>() {}.getType());
stringList.forEach((k,v)-> System.out.println(k+":"+v));
gson需注意时间戳处理
或者
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> userMap = objectMapper.readValue(decrypt, Map.class);
userMap.forEach((k,v)-> System.out.println(k+":"+v));