在使用微信小程序有许多时候需要使用到申请用户手机号码的功能。我们可以通过微信小程序的getPhoneNumber组件快速获取用户手机号。
以下为根据官方文档的简要介绍,想要查看完整内容可点击完整官方文档。
使用方法
需要将 button 组件 open-type 的值设置为 getPhoneNumber,当用户点击并同意之后,可以通过 bindgetphonenumber 事件回调获取到微信服务器返回的加密数据(encryptedData、iv), 然后在第三方服务端结合 session_key 以及 app_id 进行解密获取手机号。
小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系。
auth.code2Session 接口请求地址:
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
auth.code2Session 接口请求参数:
auth.code2Session 接口返回值:
注意:
会话密钥session_key是对用户数据进行加密签名的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。临时登录凭证 code 只能使用一次。
开发者后台校验与解密开放数据
微信会对这些开放数据(即微信用户私人信息)做签名和加密处理。开发者后台拿到开放数据后可以对数据进行校验签名和解密,来保证数据不被篡改。
签名校验以及数据加解密涉及用户的会话密钥 session_key。 开发者应该事先通过 wx.login 登录流程获取会话密钥 session_key 并保存在服务器。为了数据不被篡改,开发者不应该把 session_key 传到小程序客户端等服务器外的环境。
数据签名校验
加密数据解密算法
接口如果涉及敏感数据(如wx.getUserInfo当中的 openId 和 unionId),接口的明文内容将不包含这些敏感数据。开发者如需要获取敏感数据,需要对接口返回的加密数据(encryptedData) 进行对称解密。 解密算法如下:
会话密钥 session_key 有效性
开发者如果遇到因为 session_key 不正确而校验签名失败或解密失败,请关注下面几个与 session_key 有关的注意事项。
package demowechatgetphonenumber.controller;
import demowechatgetphonenumber.controller.dto.WeChatLoginDTO;
import demowechatgetphonenumber.entity.WeChatEntity;
import demowechatgetphonenumber.util.WechatApiProxy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
@RestController
@CrossOrigin
public class DemoGetPhoneController {
private static final Logger logger= LoggerFactory.getLogger(DemoGetPhoneController.class);
/**
* 获取当前微信用户电话号码。
* @param wxLoginDTO
*
*/
@PostMapping("get-phonenumber")
public void getPhoneNumber(@RequestBody WeChatLoginDTO wxLoginDTO) {
//获取用户登录凭证code(有效期五分钟)。
String code = wxLoginDTO.getCode();
//通过code等条件请求得到sessionKey。
WeChatEntity entity = WechatApiProxy.getWXEntityByCode(code);
logger.info("WxEntity is {}", entity.toString());
//通过sessionKey、iv解密encryptedData得到电话号码。
WechatApiProxy.WxEncryptedPhoneNumber info =
WechatApiProxy.decrypt(
wxLoginDTO.getEncryptedData(), entity.getSessionKey(), wxLoginDTO.
getIv(), WechatApiProxy.WxEncryptedPhoneNumber.class);
logger.info("PhoneNumber is {}", info.getPhoneNumber());
}
}
package demowechatgetphonenumber.controller.dto;
/**
* @author Chained1001
* @date 2020-07-02 13:25
*/
public class WeChatLoginDTO {
//encryptedData:用户信息的加密数据(如果用户没有同意授权同样返回undefined)。
private String encryptedData;
//iv:加密算法的初始向量(如果用户没有同意授权则为undefined)。
private String iv;
//code:用户登录凭证(有效期五分钟)。
private String code;
public String getEncryptedData() {
return encryptedData;
}
public void setEncryptedData(String encryptedData) {
this.encryptedData = encryptedData;
}
public String getIv() {
return iv;
}
public void setIv(String iv) {
this.iv = iv;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
package demowechatgetphonenumber.entity;
import com.alibaba.fastjson.JSON;
/**
* 微信的一些信息,事例中主要使用sessionKey。
* @author Chained1001
*/
public class WeChatEntity {
/**
* 微信会话密钥。
*/
private String sessionKey;
/**
* 微信用户在小程序内的唯一标志。
*/
private String openId;
/**
* 用户在开放平台的唯一标识符。
*/
private String unionId;
private String errcode;
private String errmsg;
public String getErrcode() {
return errcode;
}
public void setErrcode(String errcode) {
this.errcode = errcode;
}
public String getErrmsg() {
return errmsg;
}
public void setErrmsg(String errmsg) {
this.errmsg = errmsg;
}
public String getSessionKey() {
return sessionKey;
}
public void setSessionKey(String sessionKey) {
this.sessionKey = sessionKey;
}
public String getOpenId() {
return openId;
}
public void setOpenId(String openId) {
this.openId = openId;
}
public String getUnionId() {
return unionId;
}
public void setUnionId(String unionId) {
this.unionId = unionId;
}
@Override
public String toString() {
return JSON.toJSONString(this);
}
}
package demowechatgetphonenumber.util;
import com.alibaba.fastjson.JSON;
import demowechatgetphonenumber.entity.WeChatEntity;
import org.apache.xmlbeans.impl.util.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.AlgorithmParameters;
import java.security.Security;
import java.util.Arrays;
import java.util.Date;
/**
* 与微信交互的一些api。
* @author Chained1001
*/
@Component
@Import(value = {WeChatConfig.class})
public class WechatApiProxy {
// 算法名。
private static final String KEY_NAME = "AES";
// 加解密算法/模式/填充方式。
// ECB模式只用密钥即可对数据进行加密解密,CBC模式需要添加一个iv。
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding";
private static WechatApiProxy wechatApiProxy;
private static final Integer INVALID_ACCESS_TOEKN = 40001;
@Autowired
private WeChatConfig weChatConfig;
@PostConstruct
public void init() {
wechatApiProxy = this;
wechatApiProxy.weChatConfig = this.weChatConfig;
}
private static Logger logger = LoggerFactory.getLogger(WechatApiProxy.class);
/**
*
*
* @param code
*/
public static WeChatEntity getWXEntityByCode(String code){
//通过code、appid、appsecret构建访问微信服务器的地址。
String url=getJscode2sessionUrl(code);
//发送请求到微信服务器,得到sessionKey。
String str = HttpRequester.doGet(url, null);
//将sessionKey放入到WeChatEntity对象中。
WeChatEntity entity = JSON.parseObject(str, WeChatEntity.class);
return entity;
}
/**
* 构建实际访问的微信服务器url。
* @param code
* @return url
*/
private static String getJscode2sessionUrl(String code) {
WeChatConfig config = wechatApiProxy.weChatConfig;
if (null == config.getJscode2sessionUrl()) {
return null;
}
//url=profile.wechat.jscode2session=
// https://api.weixin.qq.com/sns/jscode2session?
// appid=APPID&secret=SECRET&js_code=JSCODE
// &grant_type=authorization_code
String url = config.getJscode2sessionUrl()
.replace("APPID", config.getAppid())
.replace("SECRET", config.getAppsecret())
.replace("JSCODE", code);
logger.info("wx jscode2session url is {}", url);
return url;
}
/**
* 解密方法。
* @param encryptedData
* @param sessionKey
* @param iv
* @param t
* @param
* @return
*/
public static <T> T decrypt(String encryptedData, String sessionKey, String iv, Class<T> t) {
byte[] dataByte = Base64.decode(encryptedData.getBytes(StandardCharsets.UTF_8));
// 加密秘钥
byte[] keyByte = Base64.decode(sessionKey.getBytes(StandardCharsets.UTF_8));
// 偏移量
byte[] ivByte = Base64.decode(iv.getBytes(StandardCharsets.UTF_8));
try {
// 如果密钥不足16位,那么就补足,这个if 中的内容很重要。
int base = 16;
if (keyByte.length % base != 0) {
int groups = keyByte.length / base + 1;
byte[] temp = new byte[groups * base];
Arrays.fill(temp, (byte) 0);
System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
keyByte = temp;
}
// 初始化
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
SecretKeySpec spec = new SecretKeySpec(keyByte, KEY_NAME);
AlgorithmParameters parameters = AlgorithmParameters.getInstance(KEY_NAME);
parameters.init(new IvParameterSpec(ivByte));
// 初始化
cipher.init(Cipher.DECRYPT_MODE, spec, parameters);
byte[] resultByte = cipher.doFinal(dataByte);
if (null != resultByte && resultByte.length > 0) {
String result = new String(resultByte, StandardCharsets.UTF_8);
return JSON.parseObject(result, t);
}
} catch (Exception e) {
logger.error("cipher error: {}, {}, {}", encryptedData, sessionKey, iv, e);
}
return null;
}
public static class WxEncryptedPhoneNumber {
private String phoneNumber;
private String purePhoneNumber;
private String countryCode;
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public String getPurePhoneNumber() {
return purePhoneNumber;
}
public void setPurePhoneNumber(String purePhoneNumber) {
this.purePhoneNumber = purePhoneNumber;
}
public String getCountryCode() {
return countryCode;
}
public void setCountryCode(String countryCode) {
this.countryCode = countryCode;
}
}
}
package demowechatgetphonenumber.util;
import com.alibaba.fastjson.JSON;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MapUtils {
public static <K, V> Map<K, V> of(K k1, V v1) {
Map<K, V> map = newHashMap();
map.put(k1, v1);
return map;
}
public static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2) {
Map<K, V> map = of(k1, v1);
map.put(k2, v2);
return map;
}
public static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3) {
Map<K, V> map = of(k1, v1, k2, v2);
map.put(k3, v3);
return map;
}
public static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4) {
Map<K, V> map = of(k1, v1, k2, v2, k3, v3);
map.put(k4, v4);
return map;
}
public static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5) {
Map<K, V> map = of(k1, v1, k2, v2, k3, v3, k4, v4);
map.put(k5, v5);
return map;
}
public static <T> Map<T, T> of(T... keyAndValues) {
Map<T, T> map = newHashMap();
for (int i = 0; i < keyAndValues.length; i += 2) {
T key = keyAndValues[i];
T value = i + 1 < keyAndValues.length ? keyAndValues[i + 1] : null;
map.put(key, value);
}
return map;
}
public static Map<Object, Object> asMap(Object... keyAndValues) {
Map<Object, Object> map = newHashMap();
for (int i = 0; i < keyAndValues.length; i += 2) {
Object key = keyAndValues[i];
Object value = i + 1 < keyAndValues.length ? keyAndValues[i + 1] : null;
map.put(key, value);
}
return map;
}
public static <K, V> Map<K, V> newHashMap() {
return new HashMap<K, V>();
}
public static boolean isEmpty(Map<?, ?> map) {
return map == null || map.isEmpty();
}
public static String getStr(Map m, Object key) {
return getStr(m, key, null);
}
public static String getStr(Map m, Object key, String defaultValue) {
if (m == null) return defaultValue;
Object value = m.get(key);
if (value == null) return defaultValue;
return value.toString();
}
public static Number getNum(Map m, Object key) {
if (m == null) return null;
Object value = m.get(key);
if (value == null) return null;
if (value instanceof Number) return (Number) value;
if (!(value instanceof String)) return null;
try {
return NumberFormat.getInstance().parse((String) value);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
public static Integer getInt(Map m, Object key) {
Number value = getNum(m, key);
if (value == null) return null;
return value instanceof Integer ? (Integer) value : new Integer(value.intValue());
}
public static Map toMap(Object bean) {
if (bean == null) {
return of();
}
return JSON.parseObject(JSON.toJSONString(bean), Map.class);
}
public static <K> List<K> getListForce(Map m, Object key) {
if (m == null) return null;
Object value = m.get(key);
if (value == null) {
List<K> list = new ArrayList<K>();
m.put(key, list);
return list;
}
if (value instanceof List)
return (List<K>) value;
if (!(value instanceof List))
throw new RuntimeException("Cannot Parse Object To List");
return null;
}
public static <K, V> Map<K, V> getMapForce(Map m, Object key) {
if (m == null) return null;
Object value = m.get(key);
if (value == null) {
Map<K, V> ret = new HashMap<K, V>();
m.put(key, ret);
return ret;
}
if (value instanceof Map)
return (Map<K, V>) value;
if (!(value instanceof Map))
throw new RuntimeException("Cannot Parse Object To Map");
return null;
}
}
package demowechatgetphonenumber.util;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Map;
/**
* @author Chained1001
*/
public class HttpRequester {
private static Logger logger = LoggerFactory.getLogger(HttpRequester.class);
public static String doGet(String url, Map<String, String> getParams) {
CloseableHttpClient client = null;
try {
StringBuilder sb = new StringBuilder(url);
if (!MapUtils.isEmpty(getParams)) {
for (Map.Entry<String, String> entry : getParams.entrySet()) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
}
logger.info("doGet() params is {}", sb.toString());
client = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(sb.toString());
CloseableHttpResponse response = client.execute(httpGet);
String result = EntityUtils.toString(response.getEntity(), Charset.forName("UTF-8"));
return result;
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
IOUtils.closeQuietly(client);
}
}
}
package demowechatgetphonenumber.util;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
/**
* @author Chained1001
*/
@Configuration
public class WeChatConfig {
@Value("${profile.wechat.appid}")
private String appid;
@Value("${profile.wechat.appsecret}")
private String appsecret;
@Value("${profile.wechat.jscode2session}")
private String jscode2sessionUrl;
@Value("${profile.wechat.accessToken}")
private String accessTokenUrl;
public String getAppid() {
return appid;
}
public void setAppid(String appid) {
this.appid = appid;
}
public String getAppsecret() {
return appsecret;
}
public void setAppsecret(String appsecret) {
this.appsecret = appsecret;
}
public String getJscode2sessionUrl() {
return jscode2sessionUrl;
}
public void setJscode2sessionUrl(String getJscode2sessionUrl) {
this.jscode2sessionUrl = getJscode2sessionUrl;
}
public String getAccessTokenUrl() {
return accessTokenUrl;
}
public void setAccessTokenUrl(String accessTokenUrl) {
this.accessTokenUrl = accessTokenUrl;
}
}