公司需要做一个H5手机端投票活动,涉及到分享授权登陆和微信分享,这里就只讲微信分享。废话不多说了。欢迎指出文中不足。
一.线下测试(测试公众号)
1.申请一个微信测试公众号
网址:http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login&token=68030871&lang=zh_CN
2.微信官方的JS-SDK使用步骤
网址:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
测试公众号的域名可以是IP也可以域名,注意的是不要使用http://或者https://;因为这里的域名不是网址,而是一个字符串。我也被坑了一下
在需要调用JS接口的页面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.4.0.js
如需进一步提升服务稳定性,当上述资源不可访问时,可改访问:http://res2.wx.qq.com/open/js/jweixin-1.4.0.js (支持https)。
备注:支持使用 AMD/CMD 标准模块加载方法加载
步骤三:通过config接口注入权限验证配置(请看官方文档)
步骤四:通过ready接口处理成功验证(请看官方文档)
步骤五:通过error接口处理失败验证(请看官方文档)
3.通过java后台获取 微信 JS 接口签名
微信 JS 接口签名校验工具:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign
上代码前发下效验工具,在后台打印下jsapi_ticket、noncestr、timestamp、url
- 控制层
WXLoginController:
@Controller @RequestMapping("/wx") public class WXLoginController { private static final Logger logger = Logger.getLogger(WXLoginController.class); @RequestMapping("/sgture.do") @ResponseBody public Map<String, Object> sgture(HttpServletRequest request) { String strUrl=request.getParameter("url"); WinXinEntity wx = WeiXinUnitl.getWinXinEntity(strUrl); // 将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 return map; } }
2.获取token 获取ticket 工具类(WeiXinUnitl)
package com.dny_inside.util; import com.dny_inside.entity.WinXinEntity; import net.sf.json.JSONObject; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.Map; public class WeiXinUnitl { public static WinXinEntity getWinXinEntity(String url) { WinXinEntity wx = new WinXinEntity(); String access_token = getAccessToken(); String ticket = getTicket(access_token); Map<String, String> ret = Sign.sign(ticket, url); wx.setTicket(ret.get("jsapi_ticket")); wx.setSignature(ret.get("signature")); wx.setNoncestr(ret.get("nonceStr")); wx.setTimestamp(ret.get("timestamp")); 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.fromObject(message); access_token = demoJson.getString("access_token"); is.close(); } catch (Exception e) { e.printStackTrace(); } System.out.println("access_token:"+access_token); return access_token; //return "24_gcxt36ZpYuFN7B-kGzNFe3HseuYbVIDZpcQhBc6r29sA3fQ6C7Gff14iY-CFQKjHczlCh3ElJvI_NhI0BJtsJM6EG577dN8U61x1GuOjlHviiLoll9nQQhTRy28ySRDwv4bB0G_s0vQ26VaQTLKbADAZRQ"; } // 获取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.fromObject(message); ticket = demoJson.getString("ticket"); is.close(); } catch (Exception e) { e.printStackTrace(); } System.out.println("ticket:"+ticket); return ticket; } }
3.工具类Sign
package com.dny_inside.util; import java.util.UUID; import java.util.Map; import java.util.HashMap; import java.util.Formatter; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.io.UnsupportedEncodingException; 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; System.out.println("string1:"+string1); 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); } }
4.实体类
package com.dny_inside.entity; public class WinXinEntity { private String access_token; private String ticket; private String noncestr; private String timestamp; private String str; private String signature; public String getAccess_token() { return access_token; } public void setAccess_token(String access_token) { this.access_token = access_token; } public String getTicket() { return ticket; } public void setTicket(String ticket) { this.ticket = ticket; } public String getNoncestr() { return noncestr; } public void setNoncestr(String noncestr) { this.noncestr = noncestr; } public String getTimestamp() { return timestamp; } public void setTimestamp(String timestamp) { this.timestamp = timestamp; } public String getStr() { return str; } public void setStr(String str) { this.str = str; } public String getSignature() { return signature; } public void setSignature(String signature) { this.signature = signature; } }
5.签名校验工具类
package com.dny_inside.util; import java.security.MessageDigest; import java.util.Arrays; import java.util.Date; /** * 签名校验工具类 * @author xiaodong * */ public class SignUtil { //校验签名的token 事先与app约定 private static String token="..."; /** * 校验签名 * @param signature 微信加密签名 * @param timestamp 时间戳 * @param nonce 随机数 * @return */ public static boolean checkSignature(String signature,String timestamp,String nonce){ if(signature==null || timestamp == null || nonce == null){ return false; } //对token,timestamp nonce 按字典排序 String[] paramArr=new String[]{token,timestamp,nonce}; Arrays.sort(paramArr); //将排序后的结果拼接成一个字符串 String content=paramArr[0].concat(paramArr[1]).concat(paramArr[2]); String ciphertext=null; try { MessageDigest md=MessageDigest.getInstance("SHA-1"); //对拼接后的字符串进行sha1加密 byte[] digest=md.digest(content.toString().getBytes()); ciphertext=byteToStr(digest); } catch (Exception e) { // TODO: handle exception } //将sha1加密后的字符串与signature进行对比 return ciphertext!=null?ciphertext.equals(signature.toUpperCase()):false; } /** * 生成签名 android使用 * @param timestamp 时间戳 * @param nonce 随机数 * @return */ public static String getSignature(String timestamp,String nonce){ //对token,timestamp nonce 按字典排序 String[] paramArr=new String[]{token,timestamp,nonce}; Arrays.sort(paramArr); //将排序后的结果拼接成一个字符串 String content=paramArr[0].concat(paramArr[1]).concat(paramArr[2]); String ciphertext=null; try { MessageDigest md=MessageDigest.getInstance("SHA-1"); //对拼接后的字符串进行sha1加密 update// 使用指定的字节数组对摘要进行最后更新 byte[] digest=md.digest(content.toString().getBytes());//完成摘要计算 ciphertext=byteToStr(digest); } catch (Exception e) { e.printStackTrace(); } //将sha1加密后的字符串与signature进行对比 return ciphertext; } /** * 将字节数组转换成十六进制字符串 * @param byteArray * @return */ private static String byteToStr(byte[] byteArray){ String strDigest=""; for (int i = 0; i < byteArray.length; i++) { strDigest+=byteToHexStr(byteArray[i]); } return strDigest; } private static String byteToHexStr(byte mByte){ char[] Digit={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; char[] tempArr=new char[2]; tempArr[0]=Digit[(mByte >>> 4) & 0X0F]; tempArr[1]=Digit[mByte & 0X0F]; String s=new String(tempArr); return s; } /** * 生成随机数 */ /*public static String generateVerificationCode() { return RandomStringUtils.random(2, "123456789"); }*/ /** * 当前时间 * 获取精确到秒的时间戳 * @return */ public static String getSecondTimestamp(){ String timestamp = String.valueOf(new Date().getTime()/1000); return timestamp; } }
4.WXUnitl 工具类
package com.dny_inside.util; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Formatter; public class WXUnitl { 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 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; } }
4.页面代码(页面代码我遇到的坑最多,主要是比较难懂,自己也不够细心)
坑一.调用控制层接口时传的url需要特别注意。
确认url是页面完整的url(请在当前页面alert(location.href.split('#')[0])确认),包括'http(s)://'部分,以及'?'后面的GET参数部分,但不包括'#'hash后面的部分。
这个是重点:
确保你获取用来签名的url是动态获取的,动态页面可参见实例代码中php的实现方式。如果是html的静态页面在前端通过ajax将url传到后台签名,前端需要用js获取当前页面除去'#'hash部分的链接(可用location.href.split('#')[0]获取,而且需要encodeURIComponent),因为页面一旦分享,微信客户端会在你的链接末尾加入其它参数,如果不是动态获取当前链接,将导致分享后的页面签名失败。
特别的说下我遇到坑;线下测试用的是下面的页面代码 完全没问题。但是上线测试的时候出问题了 一直报错config:invalid signature。最后查了好久发现是用来签名的url用问题。因为我们做的前后端分离项目,前端代码使用vue.js写的前端加了encodeURIComponent 导致url中的符号变成乱码了 导致获取的js签名出错了。最后不加就ok了。到底加不加还是要在后台打印下url看下是否有乱码。
坑二.官方文档:请注意,原有的 wx.onMenuShareTimeline、wx.onMenuShareAppMessage、wx.onMenuShareQQ、wx.onMenuShareQZone 接口,即将废弃。请尽快迁移使用客户端6.7.2及JSSDK 1.4.0以上版本支持的 wx.updateAppMessageShareData、updateTimelineShareData 接口。
我用的是JSSDK 1.4.0;实际上我用 updateAppMessageShareData、updateTimelineShareData接口并没用成功,最后只有使用wx.onMenuShareTimeline、wx.onMenuShareAppMessage。所以这里最后自己两个都试试。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head>
<body>
<div>
<h1>微信分享</h1>
</div>
</body>
<script src="js/jquery-1.9.1.min.js"></script>
<script src="js/swiper.min.js"></script>
<script src="http://res.wx.qq.com/open/js/jweixin-1.4.0.js"></script>
<!-- 分享配置 -->
<script>
$(function () {
getSgt(window.location.href.split('#')[0]);
})
//-------------begin
//设置分享内容
var title="分享标题";
var desc="分享内容";
var indexHref = window.location.href;
var shareLink=indexHref.toString();//分享链接
var shareImgUrl="http://localhost:8080/Inside/image/meet-home/other/03e73ad6-2de1-45ed-a1d8-8083947ee8e5.JPG";//分享图片
//-------------end
function getSgt(currUrl) {
$.ajax({
type: "POST",
url: "http://localhost:8080/Inside/wx/sgture.do?url="+encodeURIComponent(window.location.href.split('#')[0]),//注意此处有坑,
dataType: "json",
success: function (response) {
initWeChat(response.sgture,response.appid,response.timestamp,response.noncestr);
wx.ready(function() {
console.log(title,desc,shareLink,shareImgUrl);
//分享到---朋友圈,微信好友
wx.onMenuShareAppMessage({
title: title,
desc: desc,
link: shareLink,
imgUrl: shareImgUrl,
trigger: function (res) {
console.log(title,desc,shareLink,shareImgUrl);
// 不要尝试在trigger中使用ajax异步请求修改本次分享的内容,因为客户端分享操作是一个同步操作,这时候使用ajax的回包会还没有返回
alert('用户点击发送给朋友');
},
success: function (res) {
alert('已分享');
},
cancel: function (res) {
alert('已取消');
},
fail: function (res) {
alert(JSON.stringify(res));
}
});
wx.onMenuShareTimeline({
title: title, // 分享标题
link: shareLink,// 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: shareImgUrl,// 分享图标
desc: desc,
trigger: function (res) {
// 不要尝试在trigger中使用ajax异步请求修改本次分享的内容,因为客户端分享操作是一个同步操作,这时候使用ajax的回包会还没有返回
alert('用户点击发送给朋友');
},
success: function (res) {
alert('已分享');
},
cancel: function (res) {
alert('已取消');
},
fail: function (res) {
alert(JSON.stringify(res));
}
});
});
}
});
}
function initWeChat(signature, appId,timestamp,noncestr) {
wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: appId, // 必填,公众号的唯一标识
timestamp: timestamp, // 必填,生成签名的时间戳
nonceStr: noncestr, // 必填,生成签名的随机串
signature:signature,//必填,签名
jsApiList: [// 必填,需要使用的JS接口列表,所有JS接口列表见附录2
'onMenuShareAppMessage',//-----------------我们这里用了分享朋友圈
'onMenuShareTimeline',//----------------好友
]
});
}
//window.location.href="http://localhost:8111";
//console.log(decodeURIComponent(window.document.URL))
</script>
</html>
二.正式公众号配置问题
好记性不如烂笔头,赘述下。
1.设置服务器白名单 开发-->基本配置-->公众号开发信息-->IP白名单。我用的阿里云服务器
2.JS接口安全域名 开发-->接口权限-->网页服务-->页面授权-->功能设置-->JS接口安全域名。
最后展示下成功效果: