关于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
返回结果信息格式(仅供参考):
{
"code":"0",
"data":{
"city":"xxxxxxxxxxxx",
"province":"xxxxxxxxxxxx",
"country":"China",
"watermark":{
"appid":"xxxxxxxxxxxx",
"timestamp":"xxxxxxxxxxxx"
},
"openId":"xxxxxxxxxxxx",
"nickName":"xxxxxxxxxxxx",
"avatarUrl":"xxxxxxxxxxxx",
"unionId":null
},
"message":""
}
修改参考:
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服务端
此处需使用Java JDK 14或高于14的版本,采用Spring Boot搭建.
在application.properties中配置小程序相关信息
application.properties文件中修改小程序appid和小程序秘钥:
#################################### 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
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数据进行解析处理.
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
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的限制请参考官方文档).
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