微信小程序获取手机号流程及后端java解密

在小程序开发中,获取微信用户绑定的手机号功能,详细可查看官方文档:微信官方文档 · 小程序 获取手机号

获取微信用户绑定的手机号,需先调用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 刷新登录态。


以上基本都是官网内容,接下来咱们分析一下获取流程。

  1. 前端 wx.login() 用户授权 -> 获取登录用户code值 -> 发送给后端
  2. 后端根据code和小程序的appId和appSecret调用微信官方接口获取用户的session_key和openId 微信官方文档 · 小程序 服务端 code2Session
  3. 前端通过button调用微信的获取手机号功能
  4. 微信根据当前用户的session_key(也就是刚刚后端通过code获取的code值)和用户手机号等敏感数据 进行签名/加密
  5. 微信将数据+签名+密文串 发送给小程序,因为没有发送session_key,因此无法伪造签名,无法解密
  6. 前端小程序将数据+签名+密文串 发给后端,后端进行解密得到想要的数据。
    微信小程序获取手机号流程及后端java解密_第1张图片
    微信官方文档 · 小程序 解密

其中有疑问的点是,第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来解密。

结论:
  1. 前端 wx.login() 用户授权 -> 获取登录用户code值 -> 发送给后端 并将code缓存一份
  2. 后端根据code和小程序的appId和appSecret调用微信官方接口获取用户的session_key和openId ,并将code为key,session_key为value缓存一份
  3. 前端通过button调用微信的获取手机号功能
  4. 微信根据当前用户的session_key(也就是刚刚后端通过code获取的code值)和用户手机号等敏感数据 进行签名/加密
  5. 微信将数据+签名+密文串 发送给小程序,因为没有发送session_key,因此无法伪造签名,无法解密
  6. 前端小程序将数据+签名+密文串 +缓存中的code 发给后端,后端 根据code去缓存中获得session_key 进行解密得到想要的数据。
细节:
缓存中的值我设置的3天的过期时间

微信开放社区 小程序登录session_key的有效期问题 2018年
微信小程序获取手机号流程及后端java解密_第2张图片
注意:这里即使是微信开放社区,但是也是2018年的回复,所以是否为3天这里不做证明与验证,至于过期时间需按照公司实际业务需求确定:如有用户量较大,服务器内存不足,redis占用过大等问题。可适当调整,因为每个用户退出小程序后,过段时间在进入时,可能需要再次授权获取code值,所以也会导致之前的code失效,redis存入过期数据。而且微信官方一直强调,不提供session_key的过期时间,微信不会把 session_key 的有效期告知开发者。我们会根据用户使用小程序的行为对 session_key 进行续期。用户越频繁使用小程序,session_key 有效期越长。

前端获取手机号时 取code值的逻辑:
  • 判断缓存中是否有code值
  1. 如果缓存中有code值

    • 根据 wx.checkSession 判断session_key是否有效
      • 如果有效,则将当前code传递给后端
      • 如果无效,则调用wx.login(后端会在wx.login的时候将code和session_key存入缓存),之后将code传递给后端
  2. 如果缓存中没有code值

    • 则调用wx.login(后端会在wx.login的时候将code和session_key存入缓存)
注意:

签名校验以及数据加解密涉及用户的会话密钥 session_key。 开发者应该事先通过 wx.login 登录流程获取会话密钥 session_key 并保存在服务器。为了数据不被篡改,开发者不应该把 session_key 传到小程序客户端等服务器外的环境。

会话密钥 session_key 有效性

开发者如果遇到因为 session_key 不正确而校验签名失败或解密失败,请关注下面几个与 session_key 有关的注意事项。

  1. wx.login 调用时,用户的 session_key 可能会被更新而致使旧 session_key 失效(刷新机制存在最短周期,如果同一个用户短时间内多次调用 wx.login,并非每次调用都导致 session_key 刷新)。开发者应该在明确需要重新登录时才调用 wx.login,及时通过 auth.code2Session 接口更新服务器存储的 session_key。
  2. 微信不会把 session_key 的有效期告知开发者。我们会根据用户使用小程序的行为对 session_key 进行续期。用户越频繁使用小程序,session_key 有效期越长。
  3. 开发者在 session_key 失效时,可以通过重新执行登录流程获取有效的 session_key。使用接口 wx.checkSession可以校验 session_key 是否有效,从而避免小程序反复执行登录流程。
  4. 当开发者在实现自定义登录态时,可以考虑以 session_key 有效期作为自身登录态有效期,也可以实现自定义的时效性策略。
附:解密代码

需要提前导入的maven依赖

<dependency>
    <groupId>org.bouncycastlegroupId>
    <artifactId>bcprov-jdk16artifactId>
    <version>1.46version>
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));

你可能感兴趣的:(微信,微信小程序,微信获取手机号)