Java: 小程序获取用户开放信息(OpenId,UnionID)

关于OpenId和UnionID

OpenId是用户唯一标识.也就是用户信息的主键.

UnionID 机制说明
如果开发者拥有多个移动应用、网站应用、和公众帐号(包括小程序),可通过 UnionID 来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的 UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,UnionID是相同的。

获取流程

UnionID只有满足条件时才会返回.

优化后流程(小程序减少请求一次后台)

在小程序启动时,会运行app.js里面的onLaunch方法.获取信息从onLaunch方法开始:

1. 在调用 wx.login时将res.code缓存到this.globalData.privateUserCode中,在后面使用;
2. 获取用户信息的时候: wx.getSetting中调用wx.getUserInfo(需要附加参数: withCredentials: true,)时,将res.code,encryptedData,iv这三个数据发送给后台,用来换取OpenId和UnionID().

未优化流程

1. 用wx.login中的res.code换取session_key,OpenId;
2. 用session_key,iv,encryptedData解密出UnionID

微信小程序中的修改

app.js

返回结果信息格式(仅供参考):


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
    "code":"0",
    "data":{
        "city":"xxxxxxxxxxxx",
        "province":"xxxxxxxxxxxx",
        "country":"China",
        "watermark":{
            "appid":"xxxxxxxxxxxx",
            "timestamp":"xxxxxxxxxxxx"
        },
        "openId":"xxxxxxxxxxxx",
        "nickName":"xxxxxxxxxxxx",
        "avatarUrl":"xxxxxxxxxxxx",
        "unionId":null
    },
    "message":""
}

修改参考:


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
App({
  onLaunch: function () {

    wx.getSystemInfo({
      success: e => {
      }
    })
    // 展示本地存储能力
    var logs = wx.getStorageSync('logs') || []
    logs.unshift(Date.now())
    wx.setStorageSync('logs', logs)

    // 登录
    wx.login({
      success: res => {
        console.log(res);
        if (res && res.code) {
          this.globalData.privateUserCode = res.code;
        }
        // 发送 res.code 到后台换取 openId, sessionKey, unionId
      }
    })
    // 获取用户信息
    wx.getSetting({
      success: res => {
        if (res.authSetting['scope.userInfo']) {
          // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
          wx.getUserInfo({
            withCredentials: true,
            success: res => {
              // 可以将 res 发送给后台解码出 unionId
              this.globalData.userInfo = res.userInfo
              if (res && res.encryptedData && res.iv && this.globalData.privateUserCode) {
                const postData = {
                  openCode: this.globalData.privateUserCode,
                  encryptedData: res.encryptedData,
                  iv: res.iv
                }
                wx.request({
                  url:'http://localhost:8080/getWeChatUnionIdAndOpenId',
                  data: postData,
                  method: "POST",
                  header: {
                    'content-type': 'application/x-www-form-urlencoded' //修改此处即可
                  },
                  success: decodeData => {
                      try {
                        const objInfo = decodeData.data;
                        if (objInfo.code == "0") {
                          this.globalData.constWxUserOpenId = objInfo.data.openId;
                          this.globalData.constWxUserUnionId=objInfo.data.unionId;
                        } else {
                          console.error(`获取微信UNIONID返回结果处理错误,错误信息: ${objInfo.message}`);
                        }
                      } catch (err) {
                        console.error(`获取微信UNIONID错误,错误信息: ${err}`);
                      }
                  }
                })
              }
              // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
              // 所以此处加入 callback 以防止这种情况
              if (this.userInfoReadyCallback) {
                this.userInfoReadyCallback(res)
              }
            }
          })
        }
      }
    })
  },
  globalData: {
    userInfo: null,
    extUserInfo: null,
    privateUserCode: null,
  }
})

java标志
image-3175

Java服务端

此处需使用Java JDK 14或高于14的版本,采用Spring Boot搭建.

在application.properties中配置小程序相关信息

application.properties文件中修改小程序appid和小程序秘钥:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
#################################### common config : ####################################
spring.application.name=wxopenid
# 应用服务web访问端口
server.port=8080
# ActuatorWeb访问端口
management.server.port=8081
management.endpoints.jmx.exposure.include=*
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
#************************************************
# 微信小程序-配置信息
cn.bckf.wechat.appid=配置你自己的小程序appid
cn.bckf.wechat.secret=配置你自己的小程序秘钥
logging.level.cn.bckf=debug

将配置文件中的信息注入到Java Bean


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 读取application.properties文件中的微信小程序相关信息.
 *
 */
@Component
@ConfigurationProperties(prefix = "cn.bckf.wechat")
@Data
@NoArgsConstructor
public class SystemInfo {

    String appId;

    String secret;
}

JSON数据处理

访问微信的接口,微信会返回json数据,在Java中,需要对json数据进行解析处理.


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
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;


/**
 * 微信解密数据实体类.(字段可能会增加)
 * (参考地址: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html)
 *
 * @author prd
 * @version 2020-05-16
 */
@AllArgsConstructor
@NoArgsConstructor
@Data
public class WeChatDecryptData {


    @JsonProperty("openId")
    private String openid;
    @JsonProperty("nickName")
    private String nickname;
    private String gender;
    private String city;
    private String province;
    private String country;
    @JsonProperty("avatarUrl")
    private String avatarUrl;
    @JsonProperty("unionId")
    private String unionId;
    private Watermark watermark;

    public class Watermark {
        private String appid;
        private String timestamp;

        public void setAppid(String appid) {
            this.appid = appid;
        }

        public String getAppid() {
            return appid;
        }

        public void setTimestamp(String timestamp) {
            this.timestamp = timestamp;
        }

        public String getTimestamp() {
            return timestamp;
        }
    }
}




import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class WeChatOpenIdRes {

    @JsonProperty("session_key")
    private String sessionKey;
    private String openid;

}

解密及调用微信接口

该类中主要定义了解密方法,请求微信接口.

关于解密,也可以参考微信官方文档: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html


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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;

/**
 * 解密及调用微信接口工具
 *
 *
 */
@Slf4j
public class WebUtil {
    /**
     * 解密微信加密信息获取UnionId.
     */
    private static int AES_128 = 128;
    private static String ALGORITHM = "AES";
    private static String AES_CBS_PADDING = "AES/CBC/PKCS5Padding";

    public static byte[] encrypt(final byte[] key, final byte[] IV, final byte[] message) throws Exception {
        return encryptDecrypt(Cipher.ENCRYPT_MODE, key, IV, message);
    }

    public static byte[] decrypt(final byte[] key, final byte[] IV, final byte[] message) throws Exception {
        return encryptDecrypt(Cipher.DECRYPT_MODE, key, IV, message);
    }

    private static byte[] encryptDecrypt(final int mode, final byte[] key, final byte[] IV, final byte[] message)
            throws Exception {
        final Cipher cipher = Cipher.getInstance(AES_CBS_PADDING);
        final SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM);
        final IvParameterSpec ivSpec = new IvParameterSpec(IV);
        cipher.init(mode, keySpec, ivSpec);
        return cipher.doFinal(message);
    }

    private static String getWeChatAPI() {
        return "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";
    }


    /**
     * 解密微信加密的数据(获取UnionId及其余相关信息).
     *
     * @param sessionKey
     * @param encryptedData
     * @param iv
     * @return
     */
    public static String decryptWeChatInfo(String sessionKey, String encryptedData, String iv) {
        if (StringUtils.hasText(sessionKey) && StringUtils.hasText(encryptedData) && StringUtils.hasText(iv)) {
            try {
                var decodeSessionKey = Base64.getDecoder().decode(sessionKey);
                var decodeEncryptedData = Base64.getDecoder().decode(encryptedData);
                var deCodeIV = Base64.getDecoder().decode(iv);
                var keyGenerator = KeyGenerator.getInstance(ALGORITHM);
                keyGenerator.init(AES_128);
                var decryptedString = decrypt(decodeSessionKey, deCodeIV, decodeEncryptedData);
                var resStr = new String(decryptedString);
                log.debug("解密结果 : {}",resStr);
                return resStr;
            } catch (Exception ex) {
                log.debug("________________________________decryptWeChatInfo: sessionKey:{},encryptedData: {} , iv: {}", sessionKey, encryptedData, iv);
            }
        }
        return null;
    }


    /**
     * 用微信小程序获得的code换取微信用户的openId.
     *
     * @param weChatOpenCode 微信小程序获得的code.
     * @return
     */
    public static String getWeChatOpenId(String weChatOpenCode, String appId, String secret) {
        log.debug("________________________________wxOpenCode:{}", weChatOpenCode);
        if (!StringUtils.hasLength(weChatOpenCode)) {
            return "";
        }
        try {
            var client = HttpClient.newHttpClient();
            var url = getWeChatAPI().formatted(appId, secret, weChatOpenCode);
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .build();
            var response = client.send(request, HttpResponse.BodyHandlers.ofString());
            log.info(response.body());
            return response.body();
        } catch (IOException e) {
            log.error("getWeChatOpenId:{}", e.getMessage());
        } catch (InterruptedException e) {
            log.error("getWeChatOpenId:{}", e.getMessage());
        }
        return "";
    }


}

对外提供服务

对外提供访问服务:

/appid 可以访问当前application.properties配置的小程序相关信息.
/getWeChatOpenId 微信loginCode获取微信用户的openId
/getWeChatUnionId 解密微信加密的数据(获取UnionId)(关于UnionId的限制请参考官方文档).
(推荐,只需调用一次即可同时获得UnionId和OpenId) /getWeChatUnionIdAndOpenId 解密微信加密的数据(获取UnionId和OpenId).(用loginCode换取OpenId然后换取UnionId)(关于UnionId的限制请参考官方文档).


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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;


/**
 * 微信小程序服务端解密用户的OpenId和UnionId.
 *
 * 基本参考: JDK>=14
 *
 * 参考文档:
 *
 * 服务端获取开放数据:
 *
 * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html
 *
 * UnionID 机制说明:
 *
 * https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html
 *
 * @author prd
 * @version 2020-05-16
 */
@SpringBootApplication
@RestController
@RequestMapping("/")
@Slf4j
public class WeChatOpenDataDecryptApplication {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SystemInfo systemInfo;

    @RequestMapping("/appid")
    public SystemInfo getAppId() {
        return systemInfo;
    }


    /**
     * 用微信loginCode获取微信用户的openId.
     *
     * @param loginCode
     * @return
     */
    @RequestMapping("getWeChatOpenId")
    public String getWeChatOpenId(String loginCode) {
        return WebUtil.getWeChatOpenId(loginCode, systemInfo.getAppId(), systemInfo.getSecret());
    }


    /**
     * 返回UnionId,通用方法.
     *
     * @param resStr
     * @return
     * @throws JsonProcessingException
     */
    private Map getUnionIdCommon(String resStr) throws JsonProcessingException {
        if (StringUtils.hasText(resStr)) {
            var resObj = objectMapper.readValue(resStr, WeChatDecryptData.class);
            var waterMarkAppId = resObj.getWatermark().getAppid();
            var currentWxAppId = systemInfo.getAppId();
            if (waterMarkAppId.equalsIgnoreCase(currentWxAppId)) {
                return Map.of("code", "0", "message", "", "data", resObj);
            } else {
                var resMessage = String.format("解密微信加密的数据(获取UnionId).发生错误,错误原因: 解密后的APPID与当前小程序的APPID不一致!解密后的APPID: %s,当前APPID: %s.", waterMarkAppId, currentWxAppId);
                log.error("解密微信加密的数据(获取UnionId).发生错误,错误原因: 解密后的APPID与当前小程序的APPID不一致!解密后的APPID: {},当前APPID: {}.", waterMarkAppId, currentWxAppId);
                return Map.of("code", "-1", "message", resMessage);
            }
        }
        return Map.of("code", "-1", "message", "返回数据为空,或处理出现错误!");
    }

    /**
     * 解密微信加密的数据(获取UnionId)(关于UnionId的限制请参考官方文档).
     *
     * @param encryptedData
     * @param sessionKey
     * @param iv
     * @return
     */
    @RequestMapping("getWeChatUnionId")
    public Map getWeChatUnionId(String sessionKey, String encryptedData, String iv) {
        try {
            var resStr = WebUtil.decryptWeChatInfo(sessionKey, encryptedData, iv);
            return getUnionIdCommon(resStr);
        } catch (Exception ex) {
            return Map.of("code", "-1", "message", "处理出现错误!错误信息: " + ex.getMessage());
        }
    }


    /**
     * 解密微信加密的数据(获取UnionId和OpenId).(用loginCode换取OpenId然后换取UnionId)(关于UnionId的限制请参考官方文档).
     *
     * @param encryptedData
     * @param iv
     * @param loginCode
     * @return
     */
    @RequestMapping("getWeChatUnionIdAndOpenId")
    public Map getWeChatUnionIdAndOpenId(String encryptedData, String iv, String loginCode) {
        try {
            if (!StringUtils.hasText(loginCode) || !StringUtils.hasText(encryptedData) || !StringUtils.hasText(iv)) {
                return Map.of("code", "-1", "message", "必填参数为空!");
            }
            var openIdRes = WebUtil.getWeChatOpenId(loginCode, systemInfo.getAppId(), systemInfo.getSecret());
            if (StringUtils.hasText(openIdRes)) {
                var wxOpenIdResObj = objectMapper.readValue(openIdRes, WeChatOpenIdRes.class);
                if (wxOpenIdResObj != null) {
                    var resStr1 = WebUtil.decryptWeChatInfo(wxOpenIdResObj.getSessionKey(), encryptedData, iv);
                    return getUnionIdCommon(resStr1);
                }
            }
        } catch (Exception ex) {
            return Map.of("code", "-1", "message", "处理出现错误!错误信息: " + ex.getMessage());
        }
        return Map.of("code", "-1", "message", "返回数据为空,或处理出现错误!");
    }


    public static void main(String[] args) {
        SpringApplication.run(WeChatOpenDataDecryptApplication.class, args);
    }

}

项目地址

完整代码可以访问: https://gitee.com/pruidong/SpringBootDemoProjects/tree/master/WeChatGetOpenIdDemo