前言--貌似只有认证后的订阅号才可以,且据说仅向政府及媒体开放,我们当时给政府做的...
目前网上大部分的口径都是微信订阅号不支持网页端获取用户信息,只允许微信企业公众号获取,都跟客户说需要他们升级订阅号为企业公众号了,突然又想多试试,最终还真成功了,把成果物记录下来,供大家参考。
官方文档参考地址:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
正文
订阅号网页获取用户信息需要经过以下四步
负一、用户同意授权,获取code
我们做的项目是在公众号内操作,需要先关注公众号再去使用,这时候默认用户是授权过的,所以这一步我是略过的。这样也不会弹出需要用户确认是否授权的提示框(每个code仅能使用一次,5分钟未使用code失效,需重新获取)
/**
* 理论上第一步应该是调用open.weixin.qq.com/connect/oauth2/authorize来获取用户授权,但是一般都是从按钮跳转到本系统
* 所以直接在按钮对应的地址里面调用完整的请求授权地址,把本系统的主入口当做请求授权中的回调地址即可跳过第一步
*
* 测试接口示例: 需要替换url中的appid、redirect_uri appid 测试公众号的 appid redirect_uri
* 系统主入口,例如示例中的主入口是http://yourWebSite.com/projectName/doMain
*
* 如果有多个入口则应该配置多个按钮,不同按钮的回调入口设置为不同的 { "button": [ { "type": "view",
* "name": "测试按钮", "key": "TEST_BTN", "url":
* "https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxa385dca37483e5b3&redirect_uri=http://yourWebSite.com/projectName/doMain&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect"
* } ] }
*
*/
一、通过code获取OPENID
OPENID=用户唯一标识。同一个微信用户在同一个APPID“公众号”里是唯一的(服务号、订阅号、个人号是不同的APPID,所以同一个微信用户在同一个主体注册的不同公众号里面也是不同的)。
// 请求地址 其中APPID和SECRET需要替换成对应订阅号的参数,参数值可以在公众号管理控制台查询,CODE则是从request中获取的code参数值
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
二、通过网页授权令牌再次获取可以得到用户信息的授权令牌
// 通过基础令牌获取一次性临时令牌
// 其中APPID及APPSECRET可以在订阅号管理后台查询
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
注意这里,跟网上给的例子不同,网上的例子只能是企业公众号才可以调用,一定要注意
三、获取用户信息
获取用户信息时,需要用到第一步和第二步的结果
// 调用获取用户的API请求
// 需要替换ACCESS_TOKEN和OPENIDID为第二步中得到的结果
https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID
四、 在公众号配置访问路径
我的项目是直接使用订阅号的自定义按钮,在地址里面输入以下内容即可(记得替换变量APPID及具体的请求回调路径)
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=http://yourWebSite.com/projectName/doMain&response_type=code&scope=snsapi_base&state=1#wechat_redirect
在正式环境上线之前要注意几点
1. 正式的公众号接口需要在后台的网页接口权限处配置好web服务器的域名
2. web项目必须是80端口,并且根路径下会放一个验证文件,若无法访问该验证文件会导致授权失败
3. 想起来再补充
用户信息缓存策略
正常情况下帖子应该到此结束了,但是!这不是一个正常的帖子。
一般情况下我们的微信公众号都不会仅仅使用一个一级界面的,最起码是二级、三级甚至更多,那么如何在非一级界面获取用户信息呢?重新请求?太没有技术含量了。在各个请求中缓存对象?这是不被允许的。那么有没有一种方法可以避免前面的那些问题呢?答案是有的,可能有更好的方式,但我第一个想到的就是把用户信息放到缓存里,尽量在缓存里读取用户信息。
首先在每个一级界面入口处通过常规方法获取用户信息,并把结果以openId(每个微信号都有一个唯一的openId)为key放入map中,在每个子界面之间传递openId,并通过openId去缓存中查找用户对象(增加超时机制,若超时则抛出异常,需要用户重新操作获取用户信息)。
示例代码如下
// XxxService.java
// 用户对象缓存map
private static Map<String, WeChatUserInfo> snsUserMapCache = new ConcurrentHashMap<String, WeChatUserInfo>();
// 环形列表数组,每个元素都是一个openId集合
@SuppressWarnings("unchecked")
private static Set<String>[] slot = new HashSet[60];
// openId所在环上的index位置
private Map<String, Integer> openIdIndexMap = new ConcurrentHashMap<String, Integer>();
// 环形列表中的当前index位置
private static AtomicInteger currentIndex = new AtomicInteger(0);
// ......
// 启动定时器,用于循环清理超时对象
static {
/**
* 这里的理念是参考的架构师之路17年某篇超时业务处理机制文章,感谢沈大大
* 首先创建一个60大小的圆环,每个环里面存放的都是一个openId集合的Set对象 创建定时器,每分钟执行一次
* 缓存当前环形列表下标,并把下标往后移一位 把后移之前的openId集合对象缓存下来,并把对应下标中的指针置空
* 遍历缓存的openId集合对象,并从用户缓存中删掉该集合中的openId对应的key
*/
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
// 获取当前下标
Integer currIndex = currentIndex.get();
// 处理下一个
currIndex++;
// 若下一个为60,则一轮处理完毕,改为处理第一个元素
if (currIndex == 60) {
currIndex = 0;
}
// 获取需要清除的集合
Set<String> openIdSet = slot[currIndex];
// 置空需要清除的集合数组对象
slot[currIndex] = null;
// 清空过期的对象
if (openIdSet != null) {
for (String openId : openIdSet) {
snsUserMapCache.remove(openId);
}
}
// 当前下标位移
if (currIndex == 0) {
currentIndex.set(0);
} else {
currentIndex.incrementAndGet();
}
}
}, 0, 1000 * 60);
}
/**
* 缓存微信weChatUserInfo对象
*
* @param openId
* @param weChatUserInfo
* @return
*/
public boolean cacheSnsUserInfo(String openId, WeChatUserInfo weChatUserInfo) {
// openId不为空,用户合法则进行处理
if (StringUtils.isNotBlank(openId) && WeChatCommonUtil.validUserInfo(weChatUserInfo)) {
logger.info("openIdIndexMap size:" + openIdIndexMap.size());
// 判断当前openId是否已经缓存
Integer openIdIndex = openIdIndexMap.get(openId);
// 存在则在环形列表中删除
if (openIdIndex != null && openIdIndex > 0) {
logger.info("openIdIndex [{}] openId [{}]", openIdIndex, openId);
Set<String> set = slot[openIdIndex];
if (set != null)
set.remove(openId);
}
// 把openId放入环形 列表当前index的前一个位置
// 缓存当前下标
Integer currIndex = currentIndex.get();
logger.info("currIndex:" + currIndex);
// 获取当前下标对应的环形列表openId集合
Set<String> openIdSet = slot[currIndex];
// 若为空则创建一个
if (openIdSet == null) {
openIdSet = new HashSet<String>();
// 放入环形列表
slot[currIndex] = openIdSet;
}
logger.info("openIdSet:" + openIdSet);
// 把当前openId放入环形列表集合中
openIdSet.add(openId);
logger.info("currIndex [{}] openId [{}]", currIndex, openId);
// 缓存openId与环形列表之间的对应关系
openIdIndexMap.put(openId, currIndex);
// 把当前对象放入缓存
snsUserMapCache.put(openId, weChatUserInfo);
return true;
}
return false;
}
/**
* 通过openId从缓存中获取用户实体
*
* @param openId
* @return
*/
public WeChatUserInfo getWeChatUserInfoByOpenId(String openId) {
logger.info("weChatUserMapCache [{}]", openId);
return StringUtils.isBlank(openId) ? null : snsUserMapCache.get(openId);
}
// WeChatCommonUtil.java
// import net.sf.json.JSONArray;
// import net.sf.json.JSONException;
// import net.sf.json.JSONObject;
// import javax.net.ssl.HttpsURLConnection;
// import javax.net.ssl.SSLContext;
// import javax.net.ssl.SSLSocketFactory;
// import javax.net.ssl.TrustManager;
// import javax.servlet.ServletException;
// import java.net.ConnectException;
// import java.net.URL;
// .......
//
//凭证获取(GET)
public final static String token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
//正式微信公众号
private final static String FIN_APPID = "wx1234567890123456";
private final static String FIN_APPSECRET = "xxxxxx27b4160717be43afb031xxxxxx";
/**
* 订阅号通过用户网页授权获取用户信息
* @param request
* @param response
* @return
* @throws ServletException
* @throws IOException
*/
public static WeChatUserInfo getWeChatUserInfoByOAuth2(String code) throws ServletException, IOException {
WeChatUserInfo weChatUserInfo = null;
// 用户同意授权
if (!"authdeny".equals(code)) {
// 获取网页授权access_token
WeChatOauth2Token weixinOauth2Token = WeChatCommonUtil.getOauth2AccessToken(FIN_APPID, FIN_APPSECRET, code);
// 网页授权接口访问凭证
String accessToken = weixinOauth2Token.getAccessToken();
// 用户标识
String openId = weixinOauth2Token.getOpenId();
logger.info("accessToken [{}] openId [{}]", accessToken, openId);
// 重新获取一份token
accessToken = WeChatCommonUtil.getToken(FIN_APPID, FIN_APPSECRET).getAccessToken();
// 获取用户信息
weChatUserInfo = WeChatCommonUtil.getUserInfo(accessToken, openId);
logger.info("nickname:[{}]", weChatUserInfo.getNickname());
//控制台打印用户信息
logger.info("昵称:" + weChatUserInfo.getNickname());
logger.info("性别:" + weChatUserInfo.getSex());
logger.info("国家:" + weChatUserInfo.getCountry());
logger.info("省份:" + weChatUserInfo.getProvince());
logger.info("城市:" + weChatUserInfo.getCity());
}
return weChatUserInfo;
}
/**
* 验证用户对象是否合法
*
* @param weChatUserInfo
* @return 对象为空或openId为空,则为不合法
*/
public static boolean validUserInfo(WeChatUserInfo weChatUserInfo) {
return weChatUserInfo != null && StringUtils.isNotBlank(weChatUserInfo.getOpenId());
}
/**
* 发送https请求
*
* @param requestUrl
* 请求地址
* @param requestMethod
* 请求方式(GET、POST)
* @param outputStr
* 提交的数据
* @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
*/
public static JSONObject httpsRequest(String requestUrl, String requestMethod, String outputStr) {
JSONObject jsonObject = null;
try {
// 创建SSLContext对象,并使用我们指定的信任管理器初始化
TrustManager[] tm = { new MyX509TrustManager() };
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
// 从上述SSLContext对象中得到SSLSocketFactory对象
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(ssf);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(GET/POST)
conn.setRequestMethod(requestMethod);
// 当outputStr不为null时向输出流写数据
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
// 注意编码格式
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 从输入流读取返回内容
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
conn.disconnect();
jsonObject = JSONObject.fromObject(buffer.toString());
} catch (ConnectException ce) {
logger.error("连接超时:{}", ce);
} catch (Exception e) {
logger.error("https请求异常:{}", e);
}
return jsonObject;
}
/**
* 获取网页授权凭证
*
* @param appId 公众账号的唯一标识
* @param appSecret 公众账号的密钥
* @param code
* @return WeixinAouth2Token
*/
public static WeChatOauth2Token getOauth2AccessToken(String appId, String appSecret, String code) {
WeChatOauth2Token wat = null;
// 拼接请求地址
String requestUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
requestUrl = requestUrl.replace("APPID", appId);
requestUrl = requestUrl.replace("SECRET", appSecret);
requestUrl = requestUrl.replace("CODE", code);
// 获取网页授权凭证
JSONObject jsonObject = WeChatCommonUtil.httpsRequest(requestUrl, "GET", null);
if (null != jsonObject) {
try {
wat = new WeChatOauth2Token();
wat.setAccessToken(jsonObject.getString("access_token"));
wat.setExpiresIn(jsonObject.getInt("expires_in"));
wat.setRefreshToken(jsonObject.getString("refresh_token"));
wat.setOpenId(jsonObject.getString("openid"));
wat.setScope(jsonObject.getString("scope"));
} catch (Exception e) {
wat = null;
int errorCode = jsonObject.getInt("errcode");
String errorMsg = jsonObject.getString("errmsg");
logger.error("获取网页授权凭证失败 errcode:{} errmsg:{}", errorCode, errorMsg);
}
}
return wat;
}
/**
* 获取接口访问凭证
*
* @param appid
* 凭证
* @param appsecret
* 密钥
* @return
*/
public static Token getToken(String appid, String appsecret) {
Token token = null;
String requestUrl = token_url.replace("APPID", appid).replace("APPSECRET", appsecret);
// 发起GET请求获取凭证
JSONObject jsonObject = httpsRequest(requestUrl, "GET", null);
if (null != jsonObject) {
try {
token = new Token();
token.setAccessToken(jsonObject.getString("access_token"));
token.setExpiresIn(jsonObject.getInt("expires_in"));
} catch (JSONException e) {
token = null;
// 获取token失败
logger.error("获取token失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
}
}
return token;
}
/**
* 获取用户信息
*
* @param accessToken
* 接口访问凭证
* @param openId
* 用户标识
* @return weChatUserInfo
*/
public static WeChatUserInfo getUserInfo(String accessToken, String openId) {
WeChatUserInfo weChatUserInfo = null;
// 拼接请求地址
String requestUrl = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID";
requestUrl = requestUrl.replace("ACCESS_TOKEN", accessToken).replace("OPENID", openId);
// 获取用户信息
JSONObject jsonObject = WeChatCommonUtil.httpsRequest(requestUrl, "GET", null);
logger.info("result [{}]", jsonObject.toString());
if (null != jsonObject) {
try {
weChatUserInfo = new WeChatUserInfo();
// 用户的标识
weChatUserInfo.setOpenId(jsonObject.getString("openid"));
// 关注状态(1是关注,0是未关注),未关注时获取不到其余信息
weChatUserInfo.setSubscribe(jsonObject.getInt("subscribe"));
// 用户关注时间
weChatUserInfo.setSubscribeTime(jsonObject.getString("subscribe_time"));
// 昵称
weChatUserInfo.setNickname(jsonObject.getString("nickname"));
// 用户的性别(1是男性,2是女性,0是未知)
weChatUserInfo.setSex(jsonObject.getInt("sex"));
// 用户所在国家
weChatUserInfo.setCountry(jsonObject.getString("country"));
// 用户所在省份
weChatUserInfo.setProvince(jsonObject.getString("province"));
// 用户所在城市
weChatUserInfo.setCity(jsonObject.getString("city"));
// 用户的语言,简体中文为zh_CN
weChatUserInfo.setLanguage(jsonObject.getString("language"));
// 用户头像
weChatUserInfo.setHeadImgUrl(jsonObject.getString("headimgurl"));
} catch (Exception e) {
if (0 == weChatUserInfo.getSubscribe()) {
logger.error("用户{}已取消关注", weChatUserInfo.getOpenId());
} else {
int errorCode = jsonObject.getInt("errcode");
String errorMsg = jsonObject.getString("errmsg");
logger.error("获取用户信息失败 errcode:{} errmsg:{}", errorCode, errorMsg);
}
}
}
return weChatUserInfo;
}
// WeChatUserInfo.java
/**
* Title: WeChatUserInfo<br>
* Description: 微信用户的基本信息<br>
* @version 1.0
* @date 2017年8月25日
*/
public class WeChatUserInfo {
// 用户的标识
private String openId;
// 关注状态(1是关注,0是未关注),未关注时获取不到其余信息
private int subscribe;
// 用户关注时间,为时间戳。如果用户曾多次关注,则取最后关注时间
private String subscribeTime;
// 昵称
private String nickname;
// 用户的性别(1是男性,2是女性,0是未知)
private int sex;
// 用户所在国家
private String country;
// 用户所在省份
private String province;
// 用户所在城市
private String city;
// 用户的语言,简体中文为zh_CN
private String language;
// 用户头像
private String headImgUrl;
public String getOpenId() {
return openId;
}
public void setOpenId(String openId) {
this.openId = openId;
}
public int getSubscribe() {
return subscribe;
}
public void setSubscribe(int subscribe) {
this.subscribe = subscribe;
}
public String getSubscribeTime() {
return subscribeTime;
}
public void setSubscribeTime(String subscribeTime) {
this.subscribeTime = subscribeTime;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public String getHeadImgUrl() {
return headImgUrl;
}
public void setHeadImgUrl(String headImgUrl) {
this.headImgUrl = headImgUrl;
}
}
// XxxController.java
// ......
/**
* 通过微信公众号按钮访问的主入口,需要调用此方法对用户信息进行处理
* @param request
* @param mv
* @throws ServletException
* @throws IOException
* @date 2018年1月19日
*/
private void handleUserInfoByFirstRequest(HttpServletRequest request, ModelAndView mv) throws ServletException, IOException {
WeChatUserInfo weChatUserInfo = WeChatCommonUtil.getWeChatUserInfoByOAuth2(request.getParameter("code"));
if (!WeChatCommonUtil.validUserInfo(weChatUserInfo)) {
logger.error("验证用户对象是否合法方法 报错 validUserInfo ");
mv.addObject("errMsg", "获取用户信息失败,请在微信内打开重试!");
mv.setViewName("/WEB-INF/jsp/Error");
} else if (!wxyyService.cacheSnsUserInfo(weChatUserInfo.getOpenId(), weChatUserInfo)) {
//缓存当前用户 只有通过公众号按钮访问的主入口才需要调用此方法
mv.addObject("errMsg", "缓存用户信息失败,请重试!");
logger.error("预约须知缓存当前用户信息失败!");
} else { //缓存openId
mv.addObject("openId", weChatUserInfo.getOpenId());
}
}
/**
* 通过缓存得到并处理用户信息
* @param request
* @param mv
* @date 2018年1月19日
*/
private void handleUserInfoByCache4Api(HttpServletRequest request, ModelAndView mv) {
logger.info("openId [{}]", request.getParameter("openId"));
//除主入口外直接获取缓存中的用户即可
WeChatUserInfo weChatUserInfo = wxyyService.getWeChatUserInfoByOpenId(request.getParameter("openId"));
if (!WeChatCommonUtil.validUserInfo(weChatUserInfo)) {
mv.addObject("errMsg", "获取用户信息失败,请在微信内打开重试!");
mv.setViewName("/WEB-INF/jsp/Error");
} else {
//缓存openId
mv.addObject("openId", weChatUserInfo.getOpenId());
}
}
/**
* 一级界面使用代码示例
* @param request
* @param response
* @return
* @date 2018年1月18日
*/
@RequestMapping("/doMain")
public ModelAndView doMain(HttpServletRequest request, HttpServletResponse response) {
ModelAndView mv = new ModelAndView();
mv.setViewName("/WEB-INF/jsp/doMain");
try {
//通过微信公众号按钮访问的主入口,需要调用此方法对用户信息进行处理
handleUserInfoByFirstRequest(request, mv);
} catch (Exception e) {
try {
//通过缓存得到并处理用户信息
handleUserInfoByCache4Api(request, mv);
} catch (Exception e1) {
logger.error("一级界面,获取用户信息失败!", e);
mv.addObject("errMsg", "获取用户信息失败,请在微信内打开重试!");
mv.setViewName("/WEB-INF/jsp/Error");
}
}
return mv;
}
/**
* 子界面使用代码示例
*
* @param request
* @param response
* @return
*/
@RequestMapping("/childPage")
public ModelAndView yyxz(HttpServletRequest request, HttpServletResponse response) {
ModelAndView mv = new ModelAndView();
mv.setViewName("/WEB-INF/jsp/childPage");
try {
//通过缓存得到并处理用户信息
handleUserInfoByCache4Api(request, mv);
} catch (Exception e) {
logger.error("子界面,获取用户信息失败!", e);
mv.addObject("errMsg", "获取用户信息失败,请在微信内打开重试!");
mv.setViewName("/WEB-INF/jsp/Error");
}
return mv;
}
有什么问题请联系 leandzgc@gmail.com