在小程序开发中,获取陌陌用户绑定的手机号功能,详尽可查看官方文档:陌陌官方文档·小程序获取手机号
获取陌陌用户绑定的手机号,需先调用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值
注意:
签名校准以及数据加揭秘涉及用户的会话秘钥session_key。开发者应当事先通过wx.login登陆流程获取会话秘钥session_key并保存在服务器。为了数据不被篡改微信小程序code无效,开发者不应当把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失效时微信小程序code无效,可以通过重新执行登陆流程获取有效的session_key。使用插口wx.checkSession可以校准session_key是否有效,因而防止小程序反复执行登陆流程。当开发者在实现自定义登陆态时,可以考虑以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));