微信JSSDK签名
微信JS-SDK说明文档
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
生成签名
1.签名规则
参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。
对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。
这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义。
2.注意事项
1.签名用的noncestr和timestamp必须与wx.config中的nonceStr和timestamp相同。
2.签名用的url必须是调用JS接口页面的完整URL。
3.出于安全考虑,开发者必须在服务器端实现签名的逻辑。
4.调用接口时,请登录“微信公众平台-开发-基本配置”提前将服务器IP地址添加到IP白名单中,点击查看设置方法,否则将无法调用成功。小程序无需配置IP白名单。
3.签名逻辑
所知,签名字段有noncestr,jsapi_ticket,timestamp,url。那这四个值怎么来呢?
noncestr:随机字符串,可以直接生成。
string nonceStr=Guid.NewGuid().ToString("N");
jsapi_ticket:公众号用于调用微信JS接口的临时票据(签名密钥)。正常情况下,jsapi_ticket的有效期为7200秒,通过access_token来获取。由于获取jsapi_ticket的api调用次数非常有限,频繁刷新jsapi_ticket会导致api调用受限,影响自身业务,开发者必须在自己的服务全局缓存jsapi_ticket 。
获取jsapi_ticket时就要用到access_token了,access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。
我们可以通过下面的接口取得access_token
https请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
这里要用到3个参数(grant_type,appid,secret);
参数 | 是否必须 | 说明 |
grant_type | 是 | 获取access_token填写client_credential |
appid | 是 | 第三方用户唯一凭证 |
secret | 是 | 第三方用户唯一凭证密钥,即appsecret |
其中,grant_type的值即为client_credential,AppID和AppSecret可在“微信公众平台-开发-基本配置”页中获得(需要已经成为开发者,且帐号没有异常状态)。
我在上篇随笔记录了AppID和AppSecret的获取方式,链接如下:
https://www.cnblogs.com/p1024q/p/11321864.html
正常情况下,微信会返回如下JSON数据
{"access_token":"ACCESS_TOKEN","expires_in":7200}
其中, access_token的值就是我们要用的值,expires_in 是凭证有效时间(7200秒)
取到access_token后,要将值存到缓存不大于7200秒,再调用下面的接口
http请求方式: GET https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi
成功则返回如下JSON
{ "errcode":0, "errmsg":"ok", "ticket":"bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA", "expires_in":7200 }
ticket值就是要用的jsapi_ticket。
timestamp:时间戳.。
url:当前网页的URL,不包含#及其后面部分。作为接口请求参数通过前端传过来。
有了这四个值,就能根据签名规则进行签名,得到签名值signature。
签名成功,需要返回下面几个参数给前端作验签使用。
参数名 | 类型 | 说明 |
timestamp | int | 生成签名的时间戳 |
noncestr | string | 生成签名的随机串 |
signature | string | 签名 |
废话不多说,直接上后端签名代码:
1 static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();//日志 2 3 public WXShare GetWxShareInfo(string url) 4 { 5 DateTime now = DateTime.Now; 6 var timestamp = DateTimeHelper.GetTimeStamp(now);//取十位时间戳 7 var guid = Guid.NewGuid().ToString("N");//随机串 8 var ticket = "";//签名密钥 9 try { 10 WXShare s= new WXShare(); 11 //取缓存中的Ticket,没有则重新生成Ticket值(也可以将Ticket值保存到文件中,此时从文件中读取Ticket) 12 WxShareCache Cache = new WxShareCache(Key).GetCache<WxShareCache>(); 13 if (Cache == null || string.IsNullOrWhiteSpace(Cache.Ticket)) { 14 Cache = new WxShareCache(Key); 15 Cache.Ticket = GetTicket();//获取Ticket值 16 Cache.SetCache(Cache);//添加缓存 17 ticket = Cache.Ticket; 18 } else { 19 ticket = Cache.Ticket; 20 } 21 url = HttpUtility.UrlDecode(url);//url解码 22 string sign = GetSignature(ticket,guid,timestamp,url); 23 s.noncestr = guid; 24 s.timestamp = timestamp; 25 s.sign = sign; 26 logger.Warn($"url:{url},时间戳:{timestamp},随机数:{guid},ticket:{ticket},sign值:{sign}");//记录日志 27 return s; 28 } catch (Exception ex) { 29 logger.Warn(ex); 30 throw ex; 31 } 32 }
返回给前端的对象
/// <summary> /// 返回实体 /// </summary> public class WXShare { /// <summary> /// 随机码 /// </summary> public string noncestr { get; set; } /// <summary> /// 时间戳 /// </summary> public int timestamp { get; set; } /// <summary> /// 签名值 /// </summary> public string signature { get; set; } }
时间戳
/// <summary> /// 十位时间戳 /// </summary> /// <param name="dt"></param> /// <returns></returns> public static int GetTimeStamp(DateTime dt) { DateTime dateStart = new DateTime(1970, 1, 1, 8, 0, 0); int timeStamp = Convert.ToInt32((dt - dateStart).TotalSeconds); return timeStamp; }
请求方法
//请求基类 private static HttpClient _client = null; public static HttpClient Client { get { if (_client == null) { var handler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip, AllowAutoRedirect = false, UseCookies = false, }; _client = new HttpClient(handler); _client.Timeout = TimeSpan.FromSeconds(5); _client.DefaultRequestHeaders.Add("Accept","application/json"); } return _client; } }
签名密钥
/// <summary> /// GetTicket /// </summary> /// <returns></returns> public static string GetTicket() { string token = GetAccessToken();//获取AccessToken IDictionary<string,string> dic = new Dictionary<string,string>(); dic["access_token"] = token; dic["type"] = "jsapi"; FormUrlEncodedContent content = new FormUrlEncodedContent(dic); var response = Client.PostAsync("https://api.weixin.qq.com/cgi-bin/ticket/getticket",content).Result; if (response.StatusCode != HttpStatusCode.OK) return ""; var result = response.Content.ReadAsStringAsync().Result; JObject obj = JObject.Parse(result); string ticket = obj["ticket"]?.ToString()??""; return ticket; }
AccessToken
/// <summary> /// GetAccessToken /// </summary> /// <returns></returns> public static string GetAccessToken() { IDictionary<string,string> dic = new Dictionary<string,string>(); dic["grant_type"] = "client_credential"; dic["appid"] = "";//自己的appid dic["secret"] = "";//自己的appsecret FormUrlEncodedContent content = new FormUrlEncodedContent(dic); var response = Client.PostAsync("https://api.weixin.qq.com/cgi-bin/token",content).Result; if (response.StatusCode != HttpStatusCode.OK) return ""; var result = response.Content.ReadAsStringAsync().Result; JObject obj = JObject.Parse(result); string token = obj["access_token"]?.ToString()??""; return token; }
签名算法
/// <summary> /// 签名算法 /// </summary> /// <param name="ticket">ticket</param> /// <param name="noncestr">随机字符串</param> /// <param name="timestamp">时间戳</param> /// <param name="url"></param> /// <returns></returns> public static string GetSignature(string ticket,string noncestr,long timestamp,string url) { var string1Builder = new StringBuilder(); //拼接字符串 string1Builder.Append("jsapi_ticket=").Append(ticket).Append("&") .Append("noncestr=").Append(noncestr).Append("&") .Append("timestamp=").Append(timestamp).Append("&") .Append("url=").Append(url.IndexOf("#") >= 0 ? url.Substring(0,url.IndexOf("#")) : url); string str = string1Builder.ToString(); return SHA1(str);//加密 }
SHA1加密
public static string SHA1(string content) { return SHA1(content,Encoding.UTF8); } /// <summary> /// SHA1 加密 /// </summary> /// <param name="content">需要加密字符串</param> /// <param name="encode">指定加密编码</param> /// <returns>返回40位小写字符串</returns> public static string SHA1(string content,Encoding encode) { try { SHA1 sha1 = new SHA1CryptoServiceProvider(); byte[] bytes_in = encode.GetBytes(content); byte[] bytes_out = sha1.ComputeHash(bytes_in); sha1.Dispose(); string result = BitConverter.ToString(bytes_out); result = result.Replace("-","").ToLower();//转小写 return result; } catch (Exception ex) { throw new Exception("SHA1加密出错:" + ex.Message); } }
前端验签
1.引入JS文件
在需要调用JS接口的页面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.4.0.js
2.调用后端接口并注入权限验证
$(function(){ var url=encodeURIComponent(location.href.split('#')[0]); //对当前url编码 //ajax注入权限验证 $.ajax({ url:"ajax", type:'GET', data: {url:url}, error: function(XMLHttpRequest, textStatus, errorThrown){ alert("发生错误:"+errorThrown); }, success: function(res){ var appId = "";//与后端的appid相同 var noncestr = res.noncestr; var timestamp = res.timestamp; var signature = res.signature; wx.config({ debug: true, //开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: appId, //必填,公众号的唯一标识 timestamp: timestamp, // 必填,生成签名的时间戳 nonceStr: noncestr, //必填,生成签名的随机串 signature: signature,// 必填,签名 jsApiList: ['onMenuShareTimeline','onMenuShareAppMessage','onMenuShareQQ', 'onMenuShareWeibo','onMenuShareQZone','chooseImage', 'uploadImage','downloadImage','startRecord','stopRecord', 'onVoiceRecordEnd','playVoice','pauseVoice','stopVoice', 'translateVoice','openLocation','getLocation','hideOptionMenu', 'showOptionMenu','closeWindow','hideMenuItems','showMenuItems', 'showAllNonBaseMenuItem','hideAllNonBaseMenuItem','scanQRCode'] //必填,需要使用的JS接口列表,所有JS接口列表 }); } }); });
至此,后端签名,前端验签过程结束。
在这过程中,掉过几次坑。最让我印象深刻的是测试的时候怎么样都是签名错误(返回invalid signature)。考虑到可能是url的问题,所以在前端做了编码,后端做了解码,然后验签成功。
测试签名正确与否,微信有个校验工具,如下:
https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign
将签名的四个参数输入,生成的签名与后端生成的签名作对比,sign值一致说明签名是正确的,不一致就需要检查程序逻辑问题了。