同一个微信开放平台下的相同主体的App、公众号、小程序的unionid是相同的,这样可判断是否同一用户
微信针对不同的用户在不同的应用下都有唯一的一个openId
每个用户对每个公众号的OpenID是唯一的,对于不同公众号,同一用户的openid不同
例如:
同一用户在移动应用(网页web授权),APP,公众号,小程序,商户等授权或关注后,openid是不同的
因此获取在每种应用底下获取到openid的url地址和请求参考也是不一样的
应用名称 URL 参数
小程序 https://api.weixin.qq.com/sns/jscode2session appid、
secret、
js_code=WebAuthCode、
grant_type=authorization_code
微信网页 https://api.weixin.qq.com/sns/oauth2/access_token appid、
secret、
code=WebAuthCode、
grant_type=authorization_code
注意:
1.这里得到的返回结果:除了openid,还有access_token
2.除了小程序外,其它应用开发(如:公众号、web应用、商户)获取openid的url地址是一 样,只是appid和secret参数不同,如果是公众号获取openid,则参数填写公众号的openid和secret,同理商户和其它应用,appid和secret不同也就决定了在这个应用下的用户对应的openid是不同的。
东湖网上支付,采用了两种支付方式:H5支付和公众号支付,都是采用“微信网页”的同一URL来获取webauthcode来交换openid。这里面涉及到3个openid(用户关注的公众号openid,微信网页移动应用openid,商户openid),那不管是哪种支付,获取的Openid都是由商户的appid和secret参数来获得这个用户在商户应用中的openid。问题来了,参见下面的"问题二"。
问题一:如果我们项目中有了个UserId,要能关联同一微信平台下的全部应用(移动应用、网站应用、公众帐号、小程序),怎么关联呢?
问题二:在公众号内移动应用进行H5支付,怎么确定是同一个用户呢?
open.weixin.qq.com的unionid机制。开放平台和UnionId机制参考说明
使用unionid机制前提:open.weixin.qq.com中注册为开发者且认证,绑定开发者账号下的所有应用
如果不需要确定区分每种应用中的用户是不是同一用户,则不用理会unionid机制。
小程序中获取openId方法:
1.调用wx.login的时候会返回一个web authorize code
2.通过这个web authorize code去微信后台获取
后台地址:https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
传入appid,secret,JSCODE(web authorize code)去换取到openid,session_key以及unionid等信息
方式一:直接在小程序中调用(问题:https://api.weixin.qq.com 不在以下 request 合法域名列表中)
在调试阶段,我们可以在微信开发者工具中禁用这个警告,考虑到小程序的发布版本,
小程序中我们要设置自己的后台合法域名,又不能同时设置两个request合法域名
方式二:通过我们自己的后台去微信平台URL获取
小程序获取到web authorize code传入到我们自己的后台去获取openId
官方:
1.小程序调用wx.login,wx.getUserInfo获取临时登录凭证code,并回传到开发者服务器
2.开发者服务器以code换取用户唯一标识openid和会话密钥session_key
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。具体实现流程如下:
官方提醒:
//正常返回的JSON数据包
{"openid":"OPENID","session_key":"SESSIONKEY",}
//满足UnionID返回条件时,返回的JSON数据包
//满足前提:open.weixin.qq.com中注册为开发者且认证,绑定开发者账号下的所有应用
{"openid":"OPENID","session_key":"SESSIONKEY","unionid":"UNIONID"}
//错误时返回JSON数据包(示例为Code无效)
{"errcode":40029,"errmsg":"invalid code"}
3.获取加密信息
如果只是拿小程序的openid和unionid,则不用考虑这步,
因为上面第2步就可拿到openid和unionid(满足unionid机制)
在调用 wx.login,wx.getUserInfo都会返回一个对象,这个对象有一个属性叫encryptedData
这个字段的数据是加密的,所以我们可以通过解密的方式来得到原文信息,解密后为json,结构如下:
{"openId":"OPENID","nickName":"NICKNAME","gender": GENDER,"city":"CITY","province":"PROVINCE","country":"COUNTRY","avatarUrl":"AVATARURL","unionId":"UNIONID","watermark": {"appid":"APPID","timestamp":TIMESTAMP }}
新的数据解密方法
总的来说还是原来的算法,还是原来的逻辑结构,不同的是解密方式,
以前通过session_key得到iv,新方法是直接从前台接口处得到iv来解密,所改变的也只是传输的数据
1.对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充。
2.对称解密的目标密文为 Base64_Decode(encryptedData)。
3.对称解密秘钥 aeskey = Base64_Decode(session_key), aeskey 是16字节。
4.对称解密算法初始向量 为Base64_Decode(iv),其中iv由数据接口返回。
详细流程:
1.小程序客户端调用wx.login,回调里面包含js_code。
2.然后将js_code发送到服务器A(开发者服务器),服务器A向微信服务器发起请求附带js_code、appId、secretkey和grant_type参数,以换取用户的openid和session_key(会话密钥)。
3.服务器A拿到session_key后,生成一个随机数我们叫3rd_session,以3rdSessionId为key,以session_key + openid为value缓存到redis或memcached中;因为微信团队不建议直接将session_key在网络上传输,由开发者自行生成唯一键与session_key关联。其作用是:
将3rdSessionId返回给客户端,维护小程序登录态。
通过3rdSessionId找到用户session_key和openid。
4.客户端拿到3rdSessionId后缓存到storage,
5.通过wx.getUserIinfo可以获取到用户敏感数据encryptedData 。
6.客户端将encryptedData、3rdSessionId和偏移量一起发送到服务器A
7.服务器A根据3rdSessionId从缓存中获取session_key
8.在服务器A使用AES解密encryptedData,从而实现用户敏感数据解密
重点在6、7、8三个环节。
服务器端解密代码示例
package com.iups.wx.wxservice;
importjava.io.UnsupportedEncodingException;
import java.security.AlgorithmParameters;
importjava.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
importjava.security.NoSuchAlgorithmException;
importjava.security.NoSuchProviderException;
import java.security.Security;
importjava.security.spec.InvalidParameterSpecException;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
importjavax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import net.sf.json.JSONObject;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.Arrays;
import org.codehaus.xfire.util.Base64;
importcom.iups.wx.common.RemoteInterfaceAddress;
import com.iups.wx.util.HttpsClientUtil;
/**
*微信小程序信息获取
*@author Administrator
*@Date 2017年2月16日11:56:08
*/
public class WXAppletUserInfo {
/**
*获取微信小程序 session_key 和openid
* @param code
* @return
*/
public JSONObject getSessionKeyOropenid(String code){
//微信端登录code值
String wxCode = code;
String requestUrl = RemoteInterfaceAddress.GETSESSIONKEYOROPENID;
Map requestUrlParam = newHashMap();
requestUrlParam.put("appid", RemoteInterfaceAddress.AppletAPPID);
requestUrlParam.put("secret",RemoteInterfaceAddress.AppletAppSecret);
requestUrlParam.put("js_code", wxCode);
requestUrlParam.put("grant_type","authorization_code");
JSONObject jsonObject = HttpsClientUtil.getInstance().sendGetRequest(requestUrl,requestUrlParam);
return jsonObject;
}
/**
*解密用户敏感数据获取用户信息
* @param sessionKey数据进行加密签名的密钥
* @param encryptedData包括敏感数据在内的完整用户信息的加密数据
* @param iv加密算法的初始向量
* @return
*/
public JSONObject getUserInfo(String encryptedData,StringsessionKey,String iv){
//被加密的数据
byte[] dataByte = Base64.decode(encryptedData);
//加密秘钥
byte[] keyByte = Base64.decode(sessionKey);
//偏移量
byte[] ivByte = Base64.decode(iv);
try {
//如果密钥不足16位,那么就补足. 这个if 中的内容很重要
int base = 16;
if (keyByte.length % base != 0) {
int groups = keyByte.length /base + (keyByte.length % base != 0 ? 1 : 0);
byte[] temp = new byte[groups *base];
Arrays.fill(temp, (byte) 0);
System.arraycopy(keyByte, 0,temp, 0, keyByte.length);
keyByte = temp;
}
//初始化
Security.addProvider(newBouncyCastleProvider());
Cipher cipher =Cipher.getInstance("AES/CBC/PKCS7Padding","BC");
SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
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 = newString(resultByte, "UTF-8");
returnJSONObject.fromObject(result);
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidParameterSpecException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
}
return null;
}
}
: