前提

  • 获取用户信息调整, 详见官方公告

  • 目前尝试获取的用户信息只有头像和昵称,其他信息都是空的

  • 个人开发者不能获取用户手机号,需要企业开发者才能获取

前端获取用户信息示例代码

三种方式获取用户手机号

  1. 通过微信小程序的云开发能力获取手机号 cloudID. (未使用过 x.x, 本文没有介绍)

  2. 通过后端向微信服务端换取真实手机号的动态令牌 code

  3. 通过 wx.login 获取的 code 以及 加密数据 encryptedData, iv.
    注意: 不要在 button 回调里写 wx.login, 否则请求微信服务端会出现 pad block corrupted 问题

1
2
3
4
5
6
7
8
9
const getUserPhone = (e: any) => {
// e.detail = {
// cloudID: "xxx", // 云ID (方式1)
// code: "xxx", // 后端向微信服务端换取 真实手机号 的 code, 不是 wx.login 的 code (方式2)
// encryptedData: "xxx", // 加密数据 (方式3)
// iv: "xxx" // 加密算法的初始向量 (方式3)
// }
console.log('getUserPhone', e.detail);
};

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
let code = ''; // 保存 wx.login 的 code

function Index() {
const [useInfo, setUserInfo] = useState({
nickName: '微信用户',
avatarUrl: 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0',
});

useReady(async () => {
const resp = await Taro.login();
code = resp.code; // 获取 code (示例)
});

/**
* 获取用户名
*/
const getUserName = (e: any) => {
console.log('getUserName', e.detail.value); // 微信用户
};

/**
* 获取用户头像 (无法捕获用户取消选择的事件)
*/
const getUserAvatar = (e: any) => {
console.log('getUserAvatar', e.detail.avatarUrl);
// TODO: 上传图片
};

/**
* 获取用户手机号 (个人开发者不能获取用户手机号)
*
* 三种方式拿到真实的手机号:
* 1. 通过微信小程序的云开发能力获取手机号 [cloudID]
* 2. 通过后端向微信服务端换取真实手机号的动态令牌 [code]
* 3. 通过 wx.login 获取的 code 以及 加密数据 [encryptedData, iv]
* 注意: 不要在 button 回调里写 wx.login, 否则请求微信服务端会出现 pad block corrupted 问题
*/
const getUserPhone = (e: any) => {
// e.detail = {
// cloudID: "xxx", // 云ID (方式1)
// code: "xxx", // 后端向微信服务端换取 真实手机号 的 code, 不是 wx.login 的 code (方式2)
// encryptedData: "xxx", // 加密数据 (方式3)
// iv: "xxx" // 加密算法的初始向量 (方式3)
// }
console.log('getUserPhone', e.detail);

// ---------------- 发起请求并携带对应的参数 ----------------

// eg: 方式2: 通过后端向微信服务端换取真实手机号的动态令牌 [code]
// Taro.request({
// url: "...",
// method: "POST",
// data: {
// code: e.detail.code, // 后端向微信服务端换取 真实手机号 的 code, 不是 wx.login 的 code (方式2)
// },
// success: (res) => {
// console.log("getUserPhone", res.data);
// },
// });

// eg: 方式3: 通过 wx.login 获取的 code 以及 加密数据 [encryptedData, iv]
// Taro.request({
// url: "...",
// method: "POST",
// data: {
// code: code, // wx.login 的 code
// encryptedData: e.detail.encryptedData, // 加密数据 (方式3)
// iv: e.detail.iv, // 加密算法的初始向量 (方式3)
// },
// success: (res) => {
// console.log("getUserPhone", res.data);
// },
// });
};

return (
<View className={styles.pages}>
{/* 获取用户名 */}
<Input type="nickname" placeholder="输入或更换用户名" onBlur={getUserName} value={useInfo.nickName} />

{/* 获取用户头像 */}
<Button openType="chooseAvatar" onChooseAvatar={getUserAvatar}>
<Image src={useInfo.avatarUrl} />
</Button>

{/* 获取用户手机号 */}
<Button type="primary" openType="getPhoneNumber" onGetPhoneNumber={getUserPhone}>
获取手机号
</Button>
</View>
);
}

后端获取用户信息示例代码

所需的包

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>

<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.15</version>
</dependency>

AesCbUtil 解密工具类 [AesCbUtil.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.symmetric.AES;

import java.util.Base64;

/**
* AES 解密工具类
*/
public class AesCbUtil {
/*
<a href="https://doc.hutool.cn/pages/SymmetricCrypto/#aes%E5%B0%81%E8%A3%85">AES封装</a>
<a href="https://github.com/dromara/hutool/issues/2661#issuecomment-1280567307">AES解密 - Github示例</a>

注意:
button 触发获取手机号的那个按钮里不要写 wx.login!!!
在 onLoad 里写 wx.login 保存在本地后登录的时候带上这个 code 参数就行了.
否则会出现 pad block corrupted 问题。在 button 里写 wx.login 会导致 session_key 变化,导致解密失败.
<a href="https://blog.csdn.net/zhanglf02/article/details/100124091">pad block corrupted 问题</a>
*/

/**
* 1. 可解密微信小程序的加密的用户信息, 解密出来也是默认的信息. (nickName、avatarUrl)
* <p>
* 2. 可解密微信小程序的加密的用户手机号. (phoneNumber、purePhoneNumber、countryCode、watermark)
* <p>
*
* @param encryptedData 密文,被加密的数据
* @param session_key 秘钥 (code 换取的 session_key)
* @param iv 偏移量
* @return 解密后的数据
*/
public static String decrypt(String encryptedData, String session_key, String iv) {
byte[] encryptedDataByte = Base64.getDecoder().decode(encryptedData);
byte[] sessionKeyByte = Base64.getDecoder().decode(session_key);
byte[] ivByte = Base64.getDecoder().decode(iv);

AES aes = new AES(Mode.CBC, Padding.ISO10126Padding, sessionKeyByte, ivByte);
String decrypt = aes.decryptStr(encryptedDataByte, CharsetUtil.CHARSET_UTF_8);

return decrypt;
}
}

方式一: 请求微信服务器获取用户手机号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 方式一: 请求微信服务器获取用户手机号 (新方式)
* <p>
* <a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html">
* 1.1 根据 appid 和 appSecret 获取能够解密手机号的 Token
* </a>
*
* @return 能够解密手机号的 Token
*/
String getPhoneTokenService();

/**
* 方式一: 请求微信服务器获取用户手机号 (新方式)
* <p>
* <a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/phonenumber/phonenumber.getPhoneNumber.html">
* 1.2 根据 Token 和 Code 获取解密后的手机号
* </a>
*
* @param accessToken 能够获取解密手机号的 access_token
* @param code 前端 getPhoneNumber 获取的动态令牌 code
* @return 解密后的手机号
*/
JSONObject byTokenCodeGetPhoneService(String accessToken, String code);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Override
public String getPhoneTokenService() {
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
String requestUrl = url.replace("{0}", appid).replace("{1}", secret);
String res = HttpUtil.get(requestUrl);

String accessToken = JSON.parseObject(res).getString("access_token");
if (StrUtil.isEmpty(accessToken)) {
throw new CustomException("向微信服务器发送请求: 获取解密手机号的token失败!");
}

return accessToken;
}

@Override
public JSONObject byTokenCodeGetPhoneService(String accessToken, String code) {
try {
String url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={0}";
String requestUrl = url.replace("{0}", accessToken);

HashMap<String, Object> params = new HashMap<>();
params.put("code", code);
String requestParams = JSON.toJSONString(params);

String result = HttpUtil.post(requestUrl, requestParams);

JSONObject object = JSONObject.parseObject(result);
Integer errcode = object.getInteger("errcode");

if (errcode != 0) {
throw new CustomException("向微信服务器发送请求: 格式错误, 解密手机号失败!");
}

return object.getJSONObject("phone_info");
} catch (HttpException e) {
e.printStackTrace();
throw new CustomException("向微信服务器发送请求: 解密手机号请求出错!");
}
}

方式二: 使用微信小程序提供的解密方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 /**
* 方式二: 解密加密的用户手机号 (旧方式)
* <p>
* <a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html">
* 2.1 根据 appid 和 appSecret 获取 openid 和 session_key
* </a>
*
* @param code 前端 wx.login 获取的动态令牌 code
* @return {openid, session_key}
*/
JSONObject getOpenidSessionKeyService(String code);

/**
* 方式二: 解密加密的用户手机号 (旧方式)
* <p>
* <<a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html">
* 2.2 根据 session_key、encryptedData、iv 解密手机号
* </a>
*
* @param session_key 会话密钥
* @param encryptedData 加密手机号的数据
* @param iv 加密算法的初始向量
* @return 解密后的手机号
*/
String decryptPhoneService(String session_key, String encryptedData, String iv);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public JSONObject getOpenidSessionKeyService(String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
String replaceUrl = url.replace("{0}", appid).replace("{1}", secret).replace("{2}", code);
String result = HttpUtil.get(replaceUrl);

JSONObject object = JSON.parseObject(result);

String openId = object.getString("openid");
if (StrUtil.isEmpty(openId)) {
throw new CustomException("向微信服务器发送请求: code换取openId请求错误!");
}

return object;
}

@Override
public String decryptPhoneService(String session_key, String encryptedData, String iv) {
String decrypt = AesCbUtil.decrypt(encryptedData, session_key, iv);
JSONObject object = JSON.parseObject(decrypt);
System.out.println("decrypt: " + object);

return object.getString("phone_info");
}

用户服务类完整代码 [UserService.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import com.alibaba.fastjson2.JSONObject;

public interface UserService {

/*
注意:个人开发者获取不到用户手机号, 只有企业开发者才能获取到用户手机号
<p>
<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html#%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95">获取用户手机号</a>
<p>
获取用户信息的两种方式:
方式一: 请求微信服务器获取用户手机号 (新方式)
1.1 根据 appid 和 appSecret 获取能够解密手机号的 Token (getPhoneTokenService)
1.2 根据 Token 和 Code 获取解密后的手机号 (byTokenCodeGetPhoneService)
方式二: 解密加密的用户手机号 (旧方式)
2.1 根据 appid 和 appSecret 获取 openid 和 session_key (getOpenidSessionKeyService)
2.2 根据 session_key、encryptedData、iv 解密手机号 (decryptPhoneService)
*/

/**
* 方式一: 请求微信服务器获取用户手机号 (新方式)
* <p>
* <a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html">
* 1.1 根据 appid 和 appSecret 获取能够解密手机号的 Token
* </a>
*
* @return 能够解密手机号的 Token
*/
String getPhoneTokenService();

/**
* 方式一: 请求微信服务器获取用户手机号 (新方式)
* <p>
* <a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/phonenumber/phonenumber.getPhoneNumber.html">
* 1.2 根据 Token 和 Code 获取解密后的手机号
* </a>
*
* @param accessToken 能够获取解密手机号的 access_token
* @param code 前端 getPhoneNumber 获取的动态令牌 code
* @return 解密后的手机号
*/
JSONObject byTokenCodeGetPhoneService(String accessToken, String code);

/**
* 方式二: 解密加密的用户手机号 (旧方式)
* <p>
* <a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html">
* 2.1 根据 appid 和 appSecret 获取 openid 和 session_key
* </a>
*
* @param code 前端 wx.login 获取的动态令牌 code
* @return {openid, session_key}
*/
JSONObject getOpenidSessionKeyService(String code);

/**
* 方式二: 解密加密的用户手机号 (旧方式)
* <p>
* <<a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html">
* 2.2 根据 session_key、encryptedData、iv 解密手机号
* </a>
*
* @param session_key 会话密钥
* @param encryptedData 加密手机号的数据
* @param iv 加密算法的初始向量
* @return 解密后的手机号
*/
String decryptPhoneService(String session_key, String encryptedData, String iv);

}

用户服务实现类 [UserServiceImpl.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.*;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.zxiaosi.common.exception.CustomException;
import com.zxiaosi.common.utils.AesCbUtil;
import com.zxiaosi.weapp.service.UserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.HashMap;

@Service
public class UserServiceImpl implements UserService {

@Value("${config.weixin.appid}")
private String appid;

@Value("${config.weixin.secret}")
private String secret;

@Override
public String getPhoneTokenService() {
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
String requestUrl = url.replace("{0}", appid).replace("{1}", secret);
String res = HttpUtil.get(requestUrl);

String accessToken = JSON.parseObject(res).getString("access_token");
if (StrUtil.isEmpty(accessToken)) {
throw new CustomException("向微信服务器发送请求: 获取解密手机号的token失败!");
}

return accessToken;
}

@Override
public JSONObject byTokenCodeGetPhoneService(String accessToken, String code) {
try {
String url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={0}";
String requestUrl = url.replace("{0}", accessToken);

HashMap<String, Object> params = new HashMap<>();
params.put("code", code);
String requestParams = JSON.toJSONString(params);

String result = HttpUtil.post(requestUrl, requestParams);

JSONObject object = JSONObject.parseObject(result);
Integer errcode = object.getInteger("errcode");

if (errcode != 0) {
throw new CustomException("向微信服务器发送请求: 格式错误, 解密手机号失败!");
}

return object.getJSONObject("phone_info");
} catch (HttpException e) {
e.printStackTrace();
throw new CustomException("向微信服务器发送请求: 解密手机号请求出错!");
}
}

@Override
public JSONObject getOpenidSessionKeyService(String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
String replaceUrl = url.replace("{0}", appid).replace("{1}", secret).replace("{2}", code);
String result = HttpUtil.get(replaceUrl);

JSONObject object = JSON.parseObject(result);

String openId = object.getString("openid");
if (StrUtil.isEmpty(openId)) {
throw new CustomException("向微信服务器发送请求: code换取openId请求错误!");
}

return object;
}

@Override
public String decryptPhoneService(String session_key, String encryptedData, String iv) {
String decrypt = AesCbUtil.decrypt(encryptedData, session_key, iv);
JSONObject object = JSON.parseObject(decrypt);
System.out.println("decrypt: " + object);

return object.getString("phone_info");
}
}

关于小程序获取手机号收费