doc: http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html
demo:http://demo.open.weixin.qq.com/jssdk/
sandbox:http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign
生成签名之前必须先了解一下jsapi_ticket,jsapi_ticket是公众号用于调用微信JS接口的临时票据。正常情况下,jsapi_ticket的有效期为7200秒,通过access_token来获取。由于获取jsapi_ticket的api调用次数非常有限,频繁刷新jsapi_ticket会导致api调用受限,影响自身业务,开发者必须在自己的服务全局缓存jsapi_ticket 。
参考以下文档获取access_token(有效期7200秒,开发者必须在自己的服务全局缓存access_token):
用第一步拿到的access_token 采用http GET方式请求获得jsapi_ticket(有效期7200秒,开发者必须在自己的服务全局缓存jsapi_ticket):
https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi
1.设置JS接口安全域名
2.签名用的noncestr和timestamp必须与wx.config中的nonceStr和timestamp相同。
3.签名用的url必须是调用JS接口页面的完整URL。
4.出于安全考虑,开发者必须在服务器端实现签名的逻辑。
001 | public class WxSign { |
002 | @SuppressWarnings ({ "unchecked" , "unchecked" }) |
003 | public static void main(String[] args) throws Exception { |
004 | /* |
005 | String access_token= WeChat.getAccessToken(); |
006 | String jsapi_ticket = WeChat.getJsApiTicket(access_token); |
007 | */ |
008 | //System.out.println("access_token : "+access_token+ " jsapi_ticket: " +jsapi_ticket); |
009 | String jsapi_ticket="jsapi_ticket"; |
010 | String url = "http://cmsplus.com.cn"; |
011 | Map<String, String> ret = sign(jsapi_ticket, url); |
012 | for (Map.Entry entry : ret.entrySet()) { |
013 | //System.out.println(entry.getKey() + "======== " + entry.getValue()); |
014 | } |
015 | System.out.println("signature: "+ret.get("signature") + ": timestamp " +ret.get("timestamp")); |
016 | System.out.println(createLinkString(ret)); |
017 | }; |
018 |
019 | //对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后 |
020 | /** |
021 | * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串 |
022 | * @param params 需要排序并参与字符拼接的参数组 |
023 | * @return 拼接后字符串 |
024 | */ |
025 | public static String createLinkString(Map<String, String> params) { |
026 | List<String> keys = new ArrayList<String>(params.keySet()); |
027 | Collections.sort(keys); |
028 | String prestr = ""; |
029 | for (int i = 0; i < keys.size(); i++) { |
030 | String key = keys.get(i); |
031 | String value = params.get(key); |
032 | if (i == keys.size() - 1) {//拼接时,不包括最后一个&字符 |
033 | prestr = prestr + key + "=" + value; |
034 | } else { |
035 | prestr = prestr + key + "=" + value + "&"; |
036 | } |
037 | } |
038 | return prestr; |
039 | } |
040 | |
041 | public static Map<String, String> sign(String jsapi_ticket, String url) { |
042 | Map<String, String> ret = new HashMap<String, String>(); |
043 | String nonce_str = create_nonce_str(); |
044 | String timestamp = create_timestamp(); |
045 | String string1; |
046 | String signature = ""; |
047 | // 注意这里参数名必须全部小写,且必须有序 |
048 | string1 = "jsapi_ticket=" + jsapi_ticket + "&noncestr=" + nonce_str + "×tamp=" + timestamp + "&url=" + url; |
049 | //System.out.println(string1); |
050 | try { |
051 | MessageDigest crypt = MessageDigest.getInstance("SHA-1"); |
052 | crypt.reset(); |
053 | crypt.update(string1.getBytes("UTF-8")); |
054 | signature = byteToHex(crypt.digest()); |
055 | } catch (NoSuchAlgorithmException e) { |
056 | e.printStackTrace(); |
057 | } catch (UnsupportedEncodingException e) { |
058 | e.printStackTrace(); |
059 | } |
060 | ret.put("url", url); |
061 | ret.put("jsapi_ticket", jsapi_ticket); |
062 | ret.put("nonceStr", nonce_str); |
063 | ret.put("timestamp", timestamp); |
064 | ret.put("signature", signature); |
065 | return ret; |
066 | } |
067 |
068 | private static String byteToHex(final byte[] hash) { |
069 | Formatter formatter = new Formatter(); |
070 | for (byte b : hash) { |
071 | formatter.format("%02x", b); |
072 | } |
073 | String result = formatter.toString(); |
074 | formatter.close(); |
075 | return result; |
076 | } |
077 |
078 | private static String create_nonce_str() { |
079 | return UUID.randomUUID().toString(); |
080 | } |
081 |
082 | private static String create_timestamp() { |
083 | return Long.toString(System.currentTimeMillis() / 1000); |
084 | } |
085 | } |
086 | public class WeChat { |
087 | private static final String ACCESSTOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"; |
088 | private static final String PAYFEEDBACK_URL = "https://api.weixin.qq.com/payfeedback/update"; |
089 | /** |
090 | * 获取access_token |
091 | * |
092 | * @return |
093 | * @throws Exception |
094 | */ |
095 | public static String getAccessToken() throws Exception { |
096 | String appid = ConfKit.get("AppId"); |
097 | String secret = ConfKit.get("AppSecret"); |
098 | String jsonStr = HttpKit.get(ACCESSTOKEN_URL.concat("&appid=") + appid + "&secret=" + secret); |
099 | Map<String, Object> map = JSONObject.parseObject(jsonStr); |
100 | return map.get("access_token").toString(); |
101 | } |
102 | |
103 | /** |
104 | * 获取jsapi_ticket |
105 | * |
106 | * @return |
107 | * @throws Exception |
108 | */ |
109 | public static String getJsApiTicket(String accessToken) throws Exception { |
110 | String jsonStr = HttpKit.get("https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token="+accessToken+"&type=jsapi"); |
111 | Map<String, Object> map = JSONObject.parseObject(jsonStr); |
112 | return map.get("ticket").toString(); |
113 | } |
114 |
115 | /** |
116 | * 获取access_token |
117 | * |
118 | * @return |
119 | * @throws Exception |
120 | */ |
121 | public static String getAccessToken(String appid, String secret) throws Exception { |
122 | String jsonStr = HttpKit.get(ACCESSTOKEN_URL.concat("&appid=") + appid + "&secret=" + secret); |
123 | Map<String, Object> map = JSONObject.parseObject(jsonStr); |
124 | return map.get("access_token").toString(); |
125 | } |
126 |
127 | /** |
128 | * 支付反馈 |
129 | * |
130 | * @param openid |
131 | * @param feedbackid |
132 | * @return |
133 | * @throws Exception |
134 | */ |
135 | public static boolean payfeedback(String openid, String feedbackid) throws Exception { |
136 | Map<String, String> map = new HashMap<String, String>(); |
137 | String accessToken = getAccessToken(); |
138 | map.put("access_token", accessToken); |
139 | map.put("openid", openid); |
140 | map.put("feedbackid", feedbackid); |
141 | String jsonStr = HttpKit.get(PAYFEEDBACK_URL, map); |
142 | Map<String, Object> jsonMap = JSONObject.parseObject(jsonStr); |
143 | return "0".equals(jsonMap.get("errcode").toString()); |
144 | } |
145 |
146 | /** |
147 | * 判断是否来自微信, 5.0 之后的支持微信支付 |
148 | * |
149 | * @param request |
150 | * @return |
151 | */ |
152 | public static boolean isWeiXin(HttpServletRequest request) { |
153 | String userAgent = request.getHeader( "User-Agent" ); |
154 | if (StringUtils.isNotBlank(userAgent)) { |
155 | Pattern p = Pattern.compile( "MicroMessenger/(\\d+).+" ); |
156 | Matcher m = p.matcher(userAgent); |
157 | String version = null ; |
158 | if (m.find()) { |
159 | version = m.group( 1 ); |
160 | } |
161 | return ( null != version && NumberUtils.toInt(version) >= 5 ); |
162 | } |
163 | return false ; |
164 | } |
javascript
01 | wx.config({ |
02 | debug: false , |
03 | appId: '${appId}' , |
04 | timestamp: '${timestamp}' , |
05 | nonceStr: '${nonceStr}' , |
06 | signature: '${signature}' , |
07 | jsApiList: [ |
08 | 'checkJsApi' , |
09 | 'onMenuShareTimeline' , |
10 | 'onMenuShareAppMessage' , |
11 | 'onMenuShareQQ' , |
12 | 'onMenuShareWeibo' , |
13 | 'hideMenuItems' , |
14 | 'showMenuItems' , |
15 | 'hideAllNonBaseMenuItem' , |
16 | 'showAllNonBaseMenuItem' , |
17 | 'translateVoice' , |
18 | 'startRecord' , |
19 | 'stopRecord' , |
20 | 'onRecordEnd' , |
21 | 'playVoice' , |
22 | 'pauseVoice' , |
23 | 'stopVoice' , |
24 | 'uploadVoice' , |
25 | 'downloadVoice' , |
26 | 'chooseImage' , |
27 | 'previewImage' , |
28 | 'uploadImage' , |
29 | 'downloadImage' , |
30 | 'getNetworkType' , |
31 | 'openLocation' , |
32 | 'getLocation' , |
33 | 'hideOptionMenu' , |
34 | 'showOptionMenu' , |
35 | 'closeWindow' , |
36 | 'scanQRCode' , |
37 | 'chooseWXPay' , |
38 | 'openProductSpecificView' , |
39 | 'addCard' , |
40 | 'chooseCard' , |
41 | 'openCard' |
42 | ] |
43 | }); |
44 | wx.ready(function () { |
45 | var shareData = { |
46 | title: '这是活动的介绍页' , |
47 | desc: '这里是发送给好友的时候的简介' , |
50 | }; |
51 | //wx.onMenuShareAppMessage(shareData); |
52 | wx.onMenuShareAppMessage({ |
53 | title: '互联网之子' , |
54 | desc: '在长大的过程中,我才慢慢发现,我身边的所有事,别人跟我说的所有事,那些所谓本来如此,注定如此的事,它们其实没有非得如此,事情是可以改变的。更重要的是,有些事既然错了,那就该做出改变。' , |
55 | link: 'http://movie.douban.com/subject/25785114/' , |
57 | trigger: function (res) { |
58 | // 不要尝试在trigger中使用ajax异步请求修改本次分享的内容,因为客户端分享操作是一个同步操作,这时候使用ajax的回包会还没有返回 |
59 | alert( '用户点击发送给朋友' ); |
60 | }, |
61 | success: function (res) { |
62 | alert( '已分享' ); |
63 | }, |
64 | cancel: function (res) { |
65 | alert( '已取消' ); |
66 | }, |
67 | fail: function (res) { |
68 | alert(JSON.stringify(res)); |
69 | } |
70 | }); |
71 | wx.onMenuShareTimeline(shareData); |
72 | // 要隐藏的菜单项,只能隐藏“传播类”和“保护类”按钮,所有menu项见附录3 |
73 | wx.hideMenuItems({ |
74 | menuList: [ |
75 | 'menuItem:copyUrl' |
76 | ] |
77 | }); |
78 | //hide |
79 | //wx.hideAllNonBaseMenuItem(); |
80 | //wx.hideOptionMenu(); |
81 | }); |
82 | wx.error(function (res) { |
83 | alert( "error: " + res.errMsg); |
84 | }); |
实现隐藏复制链接,分享的时候改变分析的地址与内容等,其他接口按照文档来吧,没什么复杂滴。
Nodejs实现:http://www.57kan.com/show/index/id/15907
https://github.com/willian12345/wechat-JS-SDK-demo
PHP实现:https://github.com/wjfz/weixin-jssdk
Refer: 微信js sdk invalid signature签名错误 问题解决:http://www.2cto.com/weixin/201503/380936.html