项目背景:
前后端分离:
后端: C# 开发
前端: 就是微信中打开的 H5页面 纯 H5
业务流程:
因为在 h5 中实现 卡片分享 的一个字段, 需要后端 访问 腾讯API 生成,所以整个分享结构和流程就比较长!同时 这样也可以避免这个被滥用
效果展示:
触发分享的位置就在这里:
前期准备:
- 微信公众号/微信小程序的 AppID 和 AppSecret
- 配置IP白名单 (api 服务器IP,网站托管服务器 IP 我这里有两个)
- js 接口白名单 (api 服务器域名)
- 分享功能是否开放 (默认是开放的)
服务端 API 实现:
流程步骤:
- 通过 appid 和 appsecret 获取一个访问接口的凭证 access_token 7200秒 时效
private static string getAccessToken() { // access_token 应该全局存储与更新,以下代码以写入到文件中做示例 //string appid = System.Configuration.ConfigurationManager.AppSettings["appid"]; //string appsecret = System.Configuration.ConfigurationManager.AppSettings["appsecret"]; string appid = "wx65b113f9********"; string appsecret = "4e2e8960***********************"; //请求接口获取 string _url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appid + "&secret=" + appsecret; string method = "GET"; HttpWebRequest request = WebRequest.Create(_url) as HttpWebRequest; CookieContainer cookieContainer = new CookieContainer(); request.CookieContainer = cookieContainer; request.AllowAutoRedirect = true; request.Method = method; request.ContentType = "text/html"; request.Headers.Add("charset", "utf-8"); //发送请求并获取响应数据 HttpWebResponse response = request.GetResponse() as HttpWebResponse; Stream responseStream = response.GetResponseStream(); StreamReader sr = new StreamReader(responseStream, Encoding.UTF8); //获取返回过来的结果 string content = sr.ReadToEnd(); dynamic resultContent = JsonConvert.DeserializeObject(content, new { access_token = "", expires_in = "", errcode = "", errmsg = "" }.GetType()); if (resultContent != null && !string.IsNullOrWhiteSpace(resultContent.access_token)) //注意:请求成功时是不会有errcode=0返回,判断access_token是否有值即可 { return resultContent.access_token;//返回请求唯一凭证 } else { //请求失败,返回为空 return ""; } }
- 通过 access_token 获取一个 jsTicket 用于 前端页面 7200秒 时效
public static JSTicket getJsApiTicket() { long timeStamp = DateTimeOffset.Now.ToUnixTimeSeconds(); // 相差秒数 if (resultStr == null || timeStamp - timeMark > 7200-200) { string accessToken = getAccessToken(); try { //TODO:注意api_ticket 是用于调用微信卡券JS API的临时票据,有效期为7200 秒,通过access_token 来获取。 string url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + accessToken + "&type=jsapi"; string method = "GET"; HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest; CookieContainer cookieContainer = new CookieContainer(); request.CookieContainer = cookieContainer; request.AllowAutoRedirect = true; request.Method = method; request.ContentType = "text/html"; request.Headers.Add("charset", "utf-8"); //发送请求并获取响应数据 HttpWebResponse response = request.GetResponse() as HttpWebResponse; Stream responseStream = response.GetResponseStream(); StreamReader sr = new StreamReader(responseStream, Encoding.UTF8); //获取返回过来的结果 string content = sr.ReadToEnd(); //dynamic resultStr = JsonConvert.DeserializeObject(content, new { errcode = "", errmsg = "", ticket = "", expires_in = "" }.GetType()); resultStr = JsonConvert.DeserializeObject<JSTicket>(content); timeMark = DateTimeOffset.Now.ToUnixTimeSeconds(); // 相差秒数 //请求成功 if (resultStr.errcode == "0" && resultStr.errmsg == "ok") { return resultStr; } else { return resultStr; } } catch (Exception ex) { return new JSTicket { errcode = "-11" }; } } else { return resultStr; } }
- 把 jsTicket 返回给前端
[HttpGet] public MiniToken Get() { try { string appid = "wx65b113f*********"; JSTicket jsapiTicket = GetAccessTokenUtil.getJsApiTicket(); //string url = HttpContext.Current.Request.Url.ToString(); long timestamp = GetAccessTokenUtil.ConvertDateTimeInt(); string nonceStr = GetAccessTokenUtil.createNonceStr(); // 这里参数的顺序要按照 key 值 ASCII 码升序排序 //string rawstring = "jsapi_ticket=" + jsapiTicket + "&noncestr=" + nonceStr + "×tamp=" + timestamp + "&url=" + url + ""; string signature = GetAccessTokenUtil.GetSignature(jsapiTicket.ticket, nonceStr, timestamp, "http://pt.thingeasy.cn/KEUploads/vr/aolin_webgl_local/index.html"); 拼接json串返回前台 //string rtn = "{\"appid\":\"" + appid + "\",\"jsapi_ticket\":\"" + jsapiTicket + "\",\"noncestr\":\"" + nonceStr + "\",\"timestamp\":\"" + timestamp + "\",\"signature\":\"" + signature + "\"}"; //return rtn; MiniToken token = new MiniToken { appid = appid, jsapi_ticket = jsapiTicket.ticket, noncestr = nonceStr, timestamp = timestamp, signature = signature, }; return token; } catch (Exception e) { return new MiniToken { timestamp = -1, }; } }
应为时效性,腾讯建议不要频繁调用接口。每日有访问上限,可以做个缓存 块超时了再调用腾讯服务器拿新的;
完整脚本:GetAccessTokenUtil.CS
using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; namespace miniProgram.Controllers { public class GetAccessTokenUtil { private static JSTicket resultStr; //获取成功的时间 private static long timeMark; private static string getAccessToken() { // access_token 应该全局存储与更新,以下代码以写入到文件中做示例 //string appid = System.Configuration.ConfigurationManager.AppSettings["appid"]; //string appsecret = System.Configuration.ConfigurationManager.AppSettings["appsecret"]; string appid = "wx65b113**********"; string appsecret = "4e2e89609ae6********************"; //请求接口获取 string _url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appid + "&secret=" + appsecret; string method = "GET"; HttpWebRequest request = WebRequest.Create(_url) as HttpWebRequest; CookieContainer cookieContainer = new CookieContainer(); request.CookieContainer = cookieContainer; request.AllowAutoRedirect = true; request.Method = method; request.ContentType = "text/html"; request.Headers.Add("charset", "utf-8"); //发送请求并获取响应数据 HttpWebResponse response = request.GetResponse() as HttpWebResponse; Stream responseStream = response.GetResponseStream(); StreamReader sr = new StreamReader(responseStream, Encoding.UTF8); //获取返回过来的结果 string content = sr.ReadToEnd(); dynamic resultContent = JsonConvert.DeserializeObject(content, new { access_token = "", expires_in = "", errcode = "", errmsg = "" }.GetType()); if (resultContent != null && !string.IsNullOrWhiteSpace(resultContent.access_token)) //注意:请求成功时是不会有errcode=0返回,判断access_token是否有值即可 { return resultContent.access_token;//返回请求唯一凭证 } else { //请求失败,返回为空 return ""; } } public static JSTicket getJsApiTicket() { long timeStamp = DateTimeOffset.Now.ToUnixTimeSeconds(); // 相差秒数 if (resultStr == null || timeStamp - timeMark > 7200 - 200) { string accessToken = getAccessToken(); try { //TODO:注意api_ticket 是用于调用微信卡券JS API的临时票据,有效期为7200 秒,通过access_token 来获取。 string url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + accessToken + "&type=jsapi"; string method = "GET"; HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest; CookieContainer cookieContainer = new CookieContainer(); request.CookieContainer = cookieContainer; request.AllowAutoRedirect = true; request.Method = method; request.ContentType = "text/html"; request.Headers.Add("charset", "utf-8"); //发送请求并获取响应数据 HttpWebResponse response = request.GetResponse() as HttpWebResponse; Stream responseStream = response.GetResponseStream(); StreamReader sr = new StreamReader(responseStream, Encoding.UTF8); //获取返回过来的结果 string content = sr.ReadToEnd(); //dynamic resultStr = JsonConvert.DeserializeObject(content, new { errcode = "", errmsg = "", ticket = "", expires_in = "" }.GetType()); resultStr = JsonConvert.DeserializeObject<JSTicket>(content); timeMark = DateTimeOffset.Now.ToUnixTimeSeconds(); // 相差秒数 //请求成功 if (resultStr.errcode == "0" && resultStr.errmsg == "ok") { return resultStr; } else { return resultStr; } } catch (Exception ex) { return new JSTicket { errcode = "-11" }; } } else { return resultStr; } } public static long ConvertDateTimeInt() { DateTime currentDate = DateTime.Now;//当前时间 //转化为时间戳 DateTime localTime = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1)); return long.Parse((currentDate - localTime).TotalSeconds.ToString().Split('.')[0]); } /// <summary> /// 创建随机字符串 /// </summary> /// <returns></returns> public static string createNonceStr() { int length = 16; string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; string str = ""; Random rad = new Random(); for (int i = 0; i < length; i++) { str += chars.Substring(rad.Next(0, chars.Length - 1), 1); } return str; } /// <summary> /// 获取签名 /// </summary> /// <param name="jsapi_ticket">微信公众号调用微信JS临时票据</param> /// <param name="nonceStr">随机串</param> /// <param name="timestamp">时间戳</param> /// <param name="url">当前网页URL</param> /// <returns></returns> public static string GetSignature(string jsapi_ticket, string nonceStr, long timestamp, string url) { var string1Builder = new StringBuilder(); //注意这里参数名必须全部小写,且必须有序 string1Builder.Append("jsapi_ticket=").Append(jsapi_ticket).Append("&") .Append("noncestr=").Append(nonceStr).Append("&") .Append("timestamp=").Append(timestamp).Append("&") .Append("url=").Append(url.IndexOf("#") >= 0 ? url.Substring(0, url.IndexOf("#")) : url); return Sha1(string1Builder.ToString(), Encoding.UTF8); } /// <summary> /// 签名加密,使用SHA加密所得 /// </summary> /// <param name="content">签名加密参数</param> /// <param name="encode">编码UTF-8</param> /// <returns></returns> public static string Sha1(string content, Encoding encode) { try { SHA1 sha1 = new SHA1CryptoServiceProvider(); byte[] bytesIn = encode.GetBytes(content); byte[] bytesOut = sha1.ComputeHash(bytesIn); sha1.Dispose(); string result = BitConverter.ToString(bytesOut); result = result.Replace("-", ""); return result; } catch (Exception ex) { throw new Exception("SHA1加密出错:" + ex.Message); } } } }
API 控制器: MiniProgramController.CS
using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace miniProgram.Controllers { [ApiController] [Route("[controller]")] public class MiniProgramController { [HttpGet] public MiniToken Get() { try { string appid = "wx65b11***********"; JSTicket jsapiTicket = GetAccessTokenUtil.getJsApiTicket(); //string url = HttpContext.Current.Request.Url.ToString(); long timestamp = GetAccessTokenUtil.ConvertDateTimeInt(); string nonceStr = GetAccessTokenUtil.createNonceStr(); // 这里参数的顺序要按照 key 值 ASCII 码升序排序 //string rawstring = "jsapi_ticket=" + jsapiTicket + "&noncestr=" + nonceStr + "×tamp=" + timestamp + "&url=" + url + ""; string signature = GetAccessTokenUtil.GetSignature(jsapiTicket.ticket, nonceStr, timestamp, "http://pt.thingeasy.cn/KEUploads/vr/aolin_webgl_local/index.html"); 拼接json串返回前台 //string rtn = "{\"appid\":\"" + appid + "\",\"jsapi_ticket\":\"" + jsapiTicket + "\",\"noncestr\":\"" + nonceStr + "\",\"timestamp\":\"" + timestamp + "\",\"signature\":\"" + signature + "\"}"; //return rtn; MiniToken token = new MiniToken { appid = appid, jsapi_ticket = jsapiTicket.ticket, noncestr = nonceStr, timestamp = timestamp, signature = signature, }; return token; } catch (Exception e) { return new MiniToken { timestamp = -1, }; } } } }
中间用到的几个类型:
public class MiniToken { public String appid { get; set; } public String jsapi_ticket { get; set; } public String noncestr { get; set; } public long timestamp { get; set; } public String signature { get; set; } } public class JSTicket { public String errcode { get; set; } public String errmsg { get; set; } public String ticket { get; set; } public long expires_in { get; set; } }
API接了
前端 H5 实现:
html 引入
<script src="http://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
步骤:调用 API 拿到需要的凭证,然后设置 分享内容:
核心代码:
<script> let xhr = new XMLHttpRequest() let wxDate = null xhr.open("GET", "http://172.61.10.17/api/MiniProgram", true); xhr.send(null) xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { console.log(xhr.responseText); wxDate = JSON.parse(xhr.responseText) console.log(wxDate); wx.config({ debug: false, appId: wxDate.appid, timestamp: wxDate.timestamp, nonceStr: wxDate.noncestr, signature: wxDate.signature, jsApiList: [ 'checkJsApi', 'updateAppMessageShareData', 'updateTimelineShareData' ] }); wx.ready(function () { //需在用户可能点击分享按钮前就先调用 // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。 // 设置分享朋友内容 wx.updateAppMessageShareData({ title: '奥铃Pro', // 分享标题 desc: '全新一代三冠王轻卡', // 分享描述 link: location.href, imgUrl: 'http://pt.thingeasy.cn/KEUploads/vr/aolin_webgl_local/share_icon.png', // 分享图标 success: function () { // 设置成功 alert('设置成功'); } }) // 设置分享朋友权内容 wx.updateTimelineShareData({ title: '奥铃Pro', // 分享标题 desc: '全新一代三冠王轻卡', // 分享描述 link: location.href, imgUrl: 'http://pt.thingeasy.cn/KEUploads/vr/aolin_webgl_local/share_icon.png', // 分享图标 success: function () { // 设置成功 alert('设置成功'); } }) alert('ready 成功'); }); wx.error(function (res) { alert('ready 失败:', res); // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。 }); } } </script>
完整 index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="http://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script> <title>Document</title> </head> <script> let xhr = new XMLHttpRequest() let wxDate = null xhr.open("GET", "http://172.61.10.17/api/MiniProgram", true); xhr.send(null) xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { console.log(xhr.responseText); wxDate = JSON.parse(xhr.responseText) console.log(wxDate); wx.config({ debug: false, appId: wxDate.appid, timestamp: wxDate.timestamp, nonceStr: wxDate.noncestr, signature: wxDate.signature, jsApiList: [ 'checkJsApi', 'updateAppMessageShareData', 'updateTimelineShareData' ] }); wx.ready(function () { //需在用户可能点击分享按钮前就先调用 // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。 // 设置分享朋友内容 wx.updateAppMessageShareData({ title: '奥铃Pro', // 分享标题 desc: '全新一代三冠王轻卡', // 分享描述 link: location.href, imgUrl: 'http://pt.thingeasy.cn/KEUploads/vr/aolin_webgl_local/share_icon.png', // 分享图标 success: function () { // 设置成功 alert('设置成功'); } }) // 设置分享朋友权内容 wx.updateTimelineShareData({ title: '奥铃Pro', // 分享标题 desc: '全新一代三冠王轻卡', // 分享描述 link: location.href, imgUrl: 'http://pt.thingeasy.cn/KEUploads/vr/aolin_webgl_local/share_icon.png', // 分享图标 success: function () { // 设置成功 alert('设置成功'); } }) alert('ready 成功'); }); wx.error(function (res) { alert('ready 失败:', res); // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。 }); } } </script> <body> </body> </html>
wx.config 里面配置 调试模式 true 的时候:显示下面这个就说明 设置 分享类容 成功了!
注意事项:
updateAppMessageShareData:ok 设置成功之后,你发现 你点击这里分享出去的还是 链接,并不是卡片;你先要收藏一下,然后在分享 或者转发;就能看到卡片了
因为刚开始 服务器是按照 dotnet core 写的,谁知 目标服务器ii7,所有只能用 dotnet framework 重构了一遍!
源码下载地址:https://download.csdn.net/download/nicepainkiller/88093865https://download.csdn.net/download/nicepainkiller/88093865