背景:
微信中打开h5页面,获取用户信息,分享(利用微信jdk,支持分享图片与title的设置)到微信朋友圈与好友。
微信文档
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
1- 接入微信公众平台
登录微信公众平台官网后,在公众平台官网的开发-基本设置页面,勾选协议成为开发者,点击“修改配置”按钮,填写服务器地址(URL)、Token和EncodingAESKey,其中URL是开发者用来接收微信消息和事件的接口URL。Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性)。EncodingAESKey由开发者手动填写或随机生成,将用作消息体加解密密钥。
后台java代码
@RequestMapping("/wechat")
@Controller
public class WechatController {
private static Logger logger = LoggerFactory.getLogger(WechatController.class);
private static String token = "shudanlaile2017";
@RequestMapping(value = "/check")
public void get(HttpServletRequest request, HttpServletResponse response) throws Exception {
logger.info("WechatController check start ");
Enumeration pNames = request.getParameterNames();
while (pNames.hasMoreElements()) {
String name = (String) pNames.nextElement();
String value = request.getParameter(name);
logger.info("name {} value {}",name,value);
}
if (StringUtils.isNotBlank(request.getParameter("signature"))) {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
logger.info("signature{}, timestamp{}, nonce{}, echostr{}", signature, timestamp, nonce, echostr);
if (SignUtil.checkSignature(signature, timestamp, nonce)) {
logger.info("数据源为微信后台,将echostr {} 返回", echostr);
response.getOutputStream().println(echostr);
}
}
}
}
必须把后台的服务搭建起来,开起来,然后再去配置
2- 设置网页授权
在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的“开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息”的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头;
授权回调域名配置规范为全域名,比如需要网页授权的域名为:www.qq.com,配置以后此域名下面的页面http://www.qq.com/music.html 、 http://www.qq.com/login.html 都可以进行OAuth2.0鉴权。但http://pay.qq.com 、 http://music.qq.com 、 http://qq.com无法进行OAuth2.0鉴权
如果是tomcat 容器,把这个文件下载并放在
/home/duke/apache-tomcat-8.5.28/webapps/ROOT
[root@cache ROOT]# ls
MP_verify_6CvHRorvdr0vv2pC.txt
如果是nginx容器
放在
/nginx/html 下面
为了后续开发,可以直接把这些域名授权配置好
至此可以进行获取用户信息功能的开发了,注意网站必须搭建在上面配置的授权域名下才行
前端引导用户授权,授权后把code传给后台服务,后台服务根据code 获取用户信息
后台代码
public class WxService {
private String URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
private String userInfoURL = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";
private static String APPID = "你的appid";
private static String SECRET = "你的secret";
private static Logger logger = LoggerFactory.getLogger(WxService.class);
RedisProxy proxy_h5 = RedisProxy.getRedisClustor(H5Constants.h5_redis_clusterId);
public static void main(String[] args) {
WxService service=new WxService();
// UserInfoModel model= service.getUserInfoModelBYcode("071vnQRP1uYTQ11rcYPP11DyRP1vnQR8");
String s1= service.getRankPersent("ofOVy1L0NTyy4BbPmNv4MP2mW5Sw");
System.out.println("aaa");
}
public UserInfoModel getUserInfoModelBYcode(String code) {
String url = URL.replaceAll("APPID", APPID).replaceAll("CODE", code).replaceAll("SECRET", SECRET);
logger.info("getUserInfoModelBYcode url {} ",url);
String s_token = HttpUtil.doGet(url);
logger.info("getUserInfoModelBYcode s_token {}",s_token);
Object o = JSON.parseObject(s_token);
String ACCESS_TOKEN = ((JSONObject) o).getString("access_token");
String OPENID = ((JSONObject) o).getString("openid");
String userInfourl = userInfoURL.replaceAll("ACCESS_TOKEN", ACCESS_TOKEN).replaceAll("OPENID", OPENID);
String s_user = HttpUtil.doGet(userInfourl);
logger.info("getUserInfoModelBYcode userInfoURL {}",userInfourl);
s_user= changeCharset(s_user,"ISO_8859_1","UTF-8");
logger.info("getUserInfoModelBYcode s_user {}",s_user);
UserInfoModel model = transfer2UserInfoModel(s_user);
return model;
}
private UserInfoModel transfer2UserInfoModel(String userInfoMap) {
logger.info("transfer2UserInfoModel {} ",userInfoMap);
Object o = JSON.parseObject(userInfoMap);
UserInfoModel model = new UserInfoModel();
model.setOpenid(((JSONObject) o).getString("openid"));
model.setCity(((JSONObject) o).getString("city"));
model.setCountry(((JSONObject) o).getString("country"));
model.setHeadimgurl(((JSONObject) o).getString("headimgurl"));
model.setNickname(((JSONObject) o).getString("nickname"));
model.setPrivilege(((JSONObject) o).getString("privilege"));
model.setSex(((JSONObject) o).getString("sex"));
model.setUnionid(((JSONObject) o).getString("unionid"));
model.setLanguage(((JSONObject) o).getString("language"));
model.setProvince(((JSONObject) o).getString("province"));
return model;
}
public String getRankPersent(String openid) {
long all = proxy_h5.zcount(H5Constants.h5_all_user_map_list, 0, 999999999);
long rank = all;
try {
rank = proxy_h5.zrevrank(H5Constants.h5_all_user_map_list, openid);
} catch (Exception e) {
rank = all;
e.printStackTrace();
}
if (rank == 0) {
return "100";
}
if (all == 1) {
return "100";
}
if (rank>=all){
return "10";
}
long res = (all - rank-1) * 100 / (all);
return res + "";
}
public String changeCharset(String str, String sourceCharset, String targetCharset) {
if (str == null) {
return null;
}
//用旧的字符编码解码字符串。解码可能会出现异常。
byte[] bs = new byte[0]; //用新的字符编码生成字符串
try {
bs = str.getBytes(sourceCharset);
return new String(bs, targetCharset);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
logger.error("changeCharset {} ",e.toString());
return null;
}
}
}
public class UserInfoModel {
// openid,nickname,sex,province,city,country,headimgurl,privilege,unionid,language,persent
@RedisColumn(isRowkey =true,isInversion = true)
private String openid; //用户的唯一标识
@RedisColumn
private String nickname; //用户昵称
@RedisColumn
private String sex; //用户的性别,值为1时是男性,值为2时是女性,值为0时是未知
@RedisColumn
private String province; //用户个人资料填写的省份
@RedisColumn
private String city; //普通用户个人资料填写的城市
@RedisColumn
private String country; //国家,如中国为CN
@RedisColumn
private String headimgurl;// 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。
@RedisColumn
private String privilege; //用户特权信息,json 数组,如微信沃卡用户为(chinaunicom)
@RedisColumn
private String unionid; //只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。
@RedisColumn
private String language; //
@RedisColumn
private String persent; //
public UserInfoModel(){}
public UserInfoModel(String openid, String nickname, String sex, String province, String city, String country, String headimgurl, String privilege, String unionid) {
this.openid = openid;
this.nickname = nickname;
this.sex = sex;
this.province = province;
this.city = city;
this.country = country;
this.headimgurl = headimgurl;
this.privilege = privilege;
this.unionid = unionid;
}
@Override
public String toString() {
return "UserInfoModel{" +
"openid='" + openid + '\'' +
", nickname='" + nickname + '\'' +
", sex='" + sex + '\'' +
", province='" + province + '\'' +
", city='" + city + '\'' +
", country='" + country + '\'' +
", headimgurl='" + headimgurl + '\'' +
", privilege='" + privilege + '\'' +
", unionid='" + unionid + '\'' +
'}';
}
public String getPersent() {
return persent;
}
public void setPersent(String persent) {
this.persent = persent;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public String getOpenid() {
return openid;
}
public void setOpenid(String openid) {
this.openid = openid;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
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 getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getHeadimgurl() {
return headimgurl;
}
public void setHeadimgurl(String headimgurl) {
this.headimgurl = headimgurl;
}
public String getPrivilege() {
return privilege;
}
public void setPrivilege(String privilege) {
this.privilege = privilege;
}
public String getUnionid() {
return unionid;
}
public void setUnionid(String unionid) {
this.unionid = unionid;
}
}
3- 调用微信的分享功能
微信文档
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
在微信内打开H5,可以直接调用微信浏览器的的分享按钮,不过分享出去的标题/图片是默认抓取的,效果很差,需要调用其分享朋友圈、好友的功能设置title、图片,则需要以下操作(这是坑最多的地方)
步骤一:绑定域名
先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。
备注:登录后可在“开发者中心”查看对应的接口权限。
前端的代码是
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: '', // 必填,公众号的唯一标识
timestamp: , // 必填,生成签名的时间戳
nonceStr: '', // 必填,生成签名的随机串
signature: '',// 必填,签名
jsApiList: [] // 必填,需要使用的JS接口列表
});
后台代码,问题最多的地方就是这里
config:fail,Error: invalid url domain
等错误是因为 上面的 安全域名 js域名没有设置或错误,请检查一下
最蛋疼的错误是这个
config:fail,Error: invalid signature
签名非法,但是并不提示哪里非法, 以下 是常规的网上看到的解决方案,可是我全部试了一遍,还是报错
如果是invalid signature签名错误。建议按如下顺序检查:
1.确认签名算法正确,可用 http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign 页面工具进行校验。
2.确认config中nonceStr(js中驼峰标准大写S), timestamp与用以签名中的对应noncestr, timestamp一致。
3.确认url是页面完整的url(请在当前页面alert(location.href.split('#')[0])确认),包括'http(s)://'部分,以及'?'后面的GET参数部分,但不包括'#'hash后面的部分。
4.确认 config 中的 appid 与用来获取 jsapi_ticket 的 appid 一致。
5.确保一定缓存access_token和jsapi_ticket。
这个是重点:
确保你获取用来签名的url是动态获取的,动态页面可参见实例代码中php的实现方式。如果是html的静态页面在前端通过ajax将url传到后台签名,前端需要用js获取当前页面除去'#'hash部分的链接(可用location.href.split('#')[0]获取,而且需要encodeURIComponent),因为页面一旦分享,微信客户端会在你的链接末尾加入其它参数,如果不是动态获取当前链接,将导致分享后的页面签名失败。
教如何验证是否正确方法:
url动态获取的方法是:
$protocol = (!empty($_SERVER[HTTPS]) && $_SERVER[HTTPS] !== off || $_SERVER[SERVER_PORT] == 443) ? "https://" : "http://";
$url = $protocol.$_SERVER[HTTP_HOST].$_SERVER[REQUEST_URI];
复制代码
步骤:
首先你在页面alert(location.href.split('#')[0]);
然后你再打印出动态获取的url是否和你alert的地址是否一样。一定要一模一样,包括大小写。
如果发现不一样,那就按照自己的需求改。反正要一样。验证签名一定可以通过的。
在解决这个问题的过程中,因为我搭建的是nginx 的负载均衡,整个集群有三台机器,之前以为是集群的问题,研究了很久,并且尝试过切到单点模式,不过最终确定,nginx 集群并不影响这里的签名校验。
我这里签名校验失败是因为两个问题,第一,通过
access_token 获取
jsapi_ticket 过程中,服务器没有设置白名单,获取失败获取错误的数据
需要在安全中心-白名单,添加后台服务器的外网ip
第二,是前端传入的url 错误,前端调用微信sdk时候,微信后台会根据其标准获取url计算签名,与你传入的签名比对,两处的url需要一致,下面贴出后台与前端的调试通过的代码
前端代码
这里的 时间戳、随机串需要与后台保持一致,签名需要后台获取
function wxShare() {
let data = {
url:"https://域名/booklist/index.html"
}
axios.post("https://域名/h5/wechat/signature",qs.stringify(data))
.then(res =>{
wx.config({
debug: false, // 开启调试模式,
appId: res.data.appid, // 必填,企业号的唯一标识,此处填写企业号corpid
timestamp: res.data.timestamp, // 必填,生成签名的时间戳
nonceStr: res.data.noncestr, // 必填,生成签名的随机串
signature: res.data.sgture, // 必填,签名,见附录1
jsApiList: [
"checkJsApi",
"onMenuShareTimeline",
"onMenuShareAppMessage",
"onMenuShareQQ",
"onMenuShareWeibo",
"onMenuShareQZone",
"hideMenuItems",
"showMenuItems",
"hideAllNonBaseMenuItem",
"showAllNonBaseMenuItem",
"translateVoice",
"startRecord",
"stopRecord",
"onVoiceRecordEnd",
"playVoice",
"onVoicePlayEnd",
"pauseVoice",
"stopVoice",
"uploadVoice",
"downloadVoice",
"chooseImage",
"previewImage",
"uploadImage",
"downloadImage",
"getNetworkType",
"openLocation",
"getLocation",
"hideOptionMenu",
"showOptionMenu",
"closeWindow",
"scanQRCode",
"chooseWXPay",
"openProductSpecificView",
"addCard",
"chooseCard",
"openCard"
] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
});
//处理验证失败的信息
wx.error(function(res) {
// alert('验证失败返回的信息:',res)
console.log("验证失败返回的信息:", res);
// alert(res)
});
wx.ready(function() {
// alert('签名成功')
//需在用户可能点击分享按钮前就先调用
// 自定义“分享给朋友”及“分享到QQ”按钮的分享内容
wx.onMenuShareAppMessage({
title: "2019年你打算读多少书?列出你的阅读计划吧!", // 分享标题
desc: "生成你的2019年阅读计划", // 分享描述
link: window.location.href, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl:
"https://域名/images/logo2.png", // 分享图标
success: function(res) {
// 设置成功
console.log('分享给朋友成功')
},
cancel: function(res) {
// alert('用户分享失败',res)
console.log('用户分享失败')
}
});
// 分享到朋友圈
wx.onMenuShareTimeline({
title: "2019年你打算读多少书?列出你的阅读计划吧", // 分享标题
desc: "生成你的2019年阅读计划", // 分享描述
link: window.location.href, // 分享链接
imgUrl:"https://域名/images/logo2.png",
success: function() {
console.log("已分享到朋友圈");
},
cancel: function() {
console.log('用户取消分享')
},
fail: function(res) {
console.log(JSON.stringify(res));
}
});
});
})
.catch(err => {
})
}
后台java代码
@RequestMapping("/signature")
@ResponseBody
public Map<String, Object> signature(HttpServletRequest request) {
String strUrl=request.getParameter("url");
// String strUrl="https://域名/index.html";
WinXinEntity wx = WeiXinUnitl.getWinXinEntity(strUrl);
logger.info("signature url {} wx {}",strUrl,wx.toString());
// 将wx的信息到给页面
Map<String, Object> map = new HashMap<String, Object>();
String sgture = WXUnitl.getSignature(wx.getTicket(), wx.getNoncestr(), wx.getTimestamp(), strUrl);
map.put("sgture", sgture.trim());//签名
map.put("timestamp", wx.getTimestamp().trim());//时间戳
map.put("noncestr", wx.getNoncestr().trim());//随即串
map.put("appid","appID");//appID
logger.info("signature map {} ",map);
return map;
public class WeiXinUnitl {
private static Logger logger = LoggerFactory.getLogger(WeiXinUnitl.class);
public static void main(String[] args) {
WinXinEntity wx= getWinXinEntity("https://域名/index.html");
System.out.println("111");
WinXinEntity wx2= getWinXinEntity("https://域名/index.html");
System.out.println("111");
}
public static WinXinEntity getWinXinEntity(String url) {
WinXinEntity wx = new WinXinEntity();
String access_token ="";
String jsapi_ticket=(String) H5GuavaCache.get(Constant.ticket_guava_key);
if (jsapi_ticket==null || jsapi_ticket.equals("") || jsapi_ticket.length()<20){
access_token = getAccessToken();
jsapi_ticket = getTicket(access_token);
H5GuavaCache.put(Constant.ticket_guava_key,jsapi_ticket);
logger.info("getWinXinEntity access_token {} jsapi_ticket {} ",access_token,jsapi_ticket);
}else {
logger.info("getWinXinEntity ticket {} ",jsapi_ticket);
}
Map<String, String> ret = Sign.sign(jsapi_ticket, url);
wx.setTicket(ret.get("jsapi_ticket"));
wx.setSignature(ret.get("signature"));
wx.setNoncestr(ret.get("nonceStr"));
wx.setTimestamp(ret.get("timestamp"));
wx.setAccess_token(access_token);
//
return wx;
}
// 获取token
private static String getAccessToken() {
String access_token = "";
String grant_type = "client_credential";//获取access_token填写client_credential
String AppId="appid";//第三方用户唯一凭证
String secret="secret";//第三方用户唯一凭证密钥,即appsecret
//这个url链接地址和参数皆不能变
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type="+grant_type+"&appid="+AppId+"&secret="+secret; //访问链接
try {
URL urlGet = new URL(url);
HttpURLConnection http = (HttpURLConnection) urlGet.openConnection();
http.setRequestMethod("GET"); // 必须是get方式请求
http.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
http.setDoOutput(true);
http.setDoInput(true);
http.connect();
InputStream is = http.getInputStream();
int size = is.available();
byte[] jsonBytes = new byte[size];
is.read(jsonBytes);
String message = new String(jsonBytes);
JSONObject demoJson = JSONObject.parseObject(message);
access_token = demoJson.getString("access_token");
is.close();
} catch (Exception e) {
e.printStackTrace();
}
return access_token;
}
// 获取ticket
private static String getTicket(String access_token) {
String ticket = null;
String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + access_token + "&type=jsapi";// 这个url链接和参数不能变
try {
URL urlGet = new URL(url);
HttpURLConnection http = (HttpURLConnection) urlGet.openConnection();
http.setRequestMethod("GET"); // 必须是get方式请求
http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
http.setDoOutput(true);
http.setDoInput(true);
System.setProperty("sun.net.client.defaultConnectTimeout", "30000");// 连接超时30秒
System.setProperty("sun.net.client.defaultReadTimeout", "30000"); // 读取超时30秒
http.connect();
InputStream is = http.getInputStream();
int size = is.available();
byte[] jsonBytes = new byte[size];
is.read(jsonBytes);
String message = new String(jsonBytes, "UTF-8");
JSONObject demoJson = JSONObject.parseObject(message);
ticket = demoJson.getString("ticket");
is.close();
} catch (Exception e) {
e.printStackTrace();
}
return ticket;
}
}
public class Sign {
public static Map<String, String> sign(String jsapi_ticket, String url) {
Map<String, String> ret = new HashMap<String, String>();
String nonce_str = create_nonce_str();
String timestamp = create_timestamp();
String string1;
String signature = "";
// 注意这里参数名必须全部小写,且必须有序
string1 = "jsapi_ticket=" + jsapi_ticket + "&noncestr=" + nonce_str + "×tamp=" + timestamp + "&url=" + url;
try {
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
crypt.reset();
crypt.update(string1.getBytes("UTF-8"));
signature= WXUnitl.getSignature(jsapi_ticket, nonce_str, timestamp, url);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
ret.put("url", url);
ret.put("jsapi_ticket", jsapi_ticket);
ret.put("nonceStr", nonce_str);
ret.put("timestamp", timestamp);
ret.put("signature", signature);
return ret;
}
// 生成签名
private static String byteToHex(final byte[] hash) {
Formatter formatter = new Formatter();
for (byte b : hash) {
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
// 生成nonce_str
private static String create_nonce_str() {
return UUID.randomUUID().toString();
}
// 生成timestamp
private static String create_timestamp() {
return Long.toString(System.currentTimeMillis() / 1000);
}
}
public class WXUnitl {
public static void main(String[] args) {
//String s="jsapi_ticket=HoagFKDcsGMVCIY2vOjf9qACR0jcC9h3eaF16vvg8i76KDwtaulU67WbpFYyDgJL72UZZLn8JwBuKDwExrQgBQ&noncestr=359ba6f4-35e8-48d8-9d0e-d72175239174×tamp=1551669217&url=https%3A%2F%2Factivity.dookbook.com%2Fbooklist%2F";
String s= getSignature("","","","");
System.out.println();
}
public static String getSignature(String jsapi_ticket, String nonce_str, String timestamp, String url) {
// 注意这里参数名必须全部小写,且必须有序
// String string1 = "jsapi_ticket=" + jsapi_ticket + "&noncestr=" + nonce_str + "×tamp=" + timestamp + "&url=" + url;
String string1 = "jsapi_ticket=" + jsapi_ticket + "&noncestr=" + nonce_str + "×tamp=" + timestamp + "&url=" + url;
/
String signature = "";
try {
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
crypt.reset();
crypt.update(string1.getBytes("UTF-8"));
signature = byteToHex(crypt.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return signature;
}
private static String byteToHex(final byte[] hash) {
Formatter formatter = new Formatter();
for (byte b : hash) {
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
}
public class WinXinEntity {
private String access_token;
private String ticket;
private String noncestr;
private String timestamp;
private String str;
private String signature;
get set
}