微信公众号开发之access_token的全局共用
最近做微信公众号开发,涉及到access_token的缓存问题(避免各自的应用都去取access_token,同时解决微信 appid和appsecret的安全问题),在通用权限管理系统底层增加了实现方法:
(access_token默认2小时过期,每取一次,上一次的就自动失效,每天取的次数有限制)
//----------------------------------------------------------------- // All Rights Reserved , Copyright (C) 2016 , Hairihan TECH, Ltd. //----------------------------------------------------------------- using System; using System.Net; using System.Text; using System.Web.Script.Serialization; namespace DotNet.Business.HttpUtilities { using DotNet.Utilities; /// <summary> /// WeChatUtilities /// 微信公共服务,远程微信调用接口 /// /// 修改记录 /// /// 2016.11.16 版本:1.0 SongBiao 远程调用服务。 /// /// <author> /// <name>SongBiao</name> /// <date>2016.11.16</date> /// </author> /// </summary> public class WeChatUtilities { /// <summary> /// 获取微信AccessToken /// Redis全局缓存,过期自动获取 /// 对应于公众号是全局唯一的票据,重复获取将导致上次获取的access_token失效 /// </summary> /// <returns></returns> public static string GetAccessToken() { string key = "WXAccessToken"; string accessToken = string.Empty; DateTime expiresAt = DateTime.Now; using (var redisClient = PooledRedisHelper.GetTokenClient()) { AccessTokenResult tokenResult = redisClient.Get<AccessTokenResult>(key); // 不存在或者已过期 if (tokenResult == null || (tokenResult != null && DateTime.Now > tokenResult.expiresAt)) { JavaScriptSerializer js = new JavaScriptSerializer(); string appId = BaseSystemInfo.WeiXinAppId; string appSecret = BaseSystemInfo.WeiXinAppSecret; var url = string.Format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}", appId, appSecret); using (WebClient wc = new WebClient()) { wc.Proxy = null; wc.Encoding = Encoding.UTF8; string returnText = wc.DownloadString(url); if (returnText.Contains("errcode")) { //可能发生错误 //可能发生错误 WxJsonResult errorResult = js.Deserialize<WxJsonResult>(returnText); if (errorResult.errcode != 0) { //发生错误 throw new Exception(string.Format("微信请求发生错误!错误代码:{0},说明:{1}", (int)errorResult.errcode, errorResult.errmsg)); } } tokenResult = js.Deserialize<AccessTokenResult>(returnText); // 添加到缓存中 减少10秒 避免一些问题 expiresAt = DateTime.Now.AddSeconds(tokenResult.expires_in); tokenResult.expiresAt = expiresAt; redisClient.Set(key, tokenResult, expiresAt); NLogHelper.Trace(DateTime.Now + ",微信accessToken过期,重新获取,下次过期时间:" + expiresAt); } } accessToken = tokenResult.access_token; } return accessToken; } #region 微信公用 /// <summary> /// 微信接口 /// </summary> interface IJsonResult { string errmsg { get; set; } object P2PData { get; set; } } /// <summary> /// 公众号返回码(JSON) /// 应该更名为ReturnCode_MP,但为减少项目中的修改,此处依旧用ReturnCode命名 /// </summary> enum ReturnCode { 系统繁忙此时请开发者稍候再试 = -1, 请求成功 = 0, 获取access_token时AppSecret错误或者access_token无效 = 40001, 不合法的凭证类型 = 40002, 不合法的OpenID = 40003, 不合法的媒体文件类型 = 40004, 不合法的文件类型 = 40005, 不合法的文件大小 = 40006, 不合法的媒体文件id = 40007, 不合法的消息类型 = 40008, 不合法的图片文件大小 = 40009, 不合法的语音文件大小 = 40010, 不合法的视频文件大小 = 40011, 不合法的缩略图文件大小 = 40012, 不合法的APPID = 40013, 不合法的access_token = 40014, 不合法的菜单类型 = 40015, 不合法的按钮个数1 = 40016, 不合法的按钮个数2 = 40017, 不合法的按钮名字长度 = 40018, 不合法的按钮KEY长度 = 40019, 不合法的按钮URL长度 = 40020, 不合法的菜单版本号 = 40021, 不合法的子菜单级数 = 40022, 不合法的子菜单按钮个数 = 40023, 不合法的子菜单按钮类型 = 40024, 不合法的子菜单按钮名字长度 = 40025, 不合法的子菜单按钮KEY长度 = 40026, 不合法的子菜单按钮URL长度 = 40027, 不合法的自定义菜单使用用户 = 40028, 不合法的oauth_code = 40029, 不合法的refresh_token = 40030, 不合法的openid列表 = 40031, 不合法的openid列表长度 = 40032, 不合法的请求字符不能包含uxxxx格式的字符 = 40033, 不合法的参数 = 40035, 不合法的请求格式 = 40038, 不合法的URL长度 = 40039, 不合法的分组id = 40050, 分组名字不合法 = 40051, 缺少access_token参数 = 41001, 缺少appid参数 = 41002, 缺少refresh_token参数 = 41003, 缺少secret参数 = 41004, 缺少多媒体文件数据 = 41005, 缺少media_id参数 = 41006, 缺少子菜单数据 = 41007, 缺少oauth_code = 41008, 缺少openid = 41009, access_token超时 = 42001, refresh_token超时 = 42002, oauth_code超时 = 42003, 需要GET请求 = 43001, 需要POST请求 = 43002, 需要HTTPS请求 = 43003, 需要接收者关注 = 43004, 需要好友关系 = 43005, 多媒体文件为空 = 44001, POST的数据包为空 = 44002, 图文消息内容为空 = 44003, 文本消息内容为空 = 44004, 多媒体文件大小超过限制 = 45001, 消息内容超过限制 = 45002, 标题字段超过限制 = 45003, 描述字段超过限制 = 45004, 链接字段超过限制 = 45005, 图片链接字段超过限制 = 45006, 语音播放时间超过限制 = 45007, 图文消息超过限制 = 45008, 接口调用超过限制 = 45009, 创建菜单个数超过限制 = 45010, 回复时间超过限制 = 45015, 系统分组不允许修改 = 45016, 分组名字过长 = 45017, 分组数量超过上限 = 45018, 不存在媒体数据 = 46001, 不存在的菜单版本 = 46002, 不存在的菜单数据 = 46003, 解析JSON_XML内容错误 = 47001, api功能未授权 = 48001, 用户未授权该api = 50001, 参数错误invalid_parameter = 61451, 无效客服账号invalid_kf_account = 61452, 客服帐号已存在kf_account_exsited = 61453, /// <summary> /// 客服帐号名长度超过限制(仅允许10个英文字符,不包括@及@后的公众号的微信号)(invalid kf_acount length) /// </summary> 客服帐号名长度超过限制 = 61454, /// <summary> /// 客服帐号名包含非法字符(仅允许英文+数字)(illegal character in kf_account) /// </summary> 客服帐号名包含非法字符 = 61455, /// <summary> /// 客服帐号个数超过限制(10个客服账号)(kf_account count exceeded) /// </summary> 客服帐号个数超过限制 = 61456, 无效头像文件类型invalid_file_type = 61457, 系统错误system_error = 61450, 日期格式错误 = 61500, 日期范围错误 = 61501, //新加入的一些类型,以下文字根据P2P项目格式组织,非官方文字 发送消息失败_48小时内用户未互动 = 10706, 发送消息失败_该用户已被加入黑名单_无法向此发送消息 = 62751, 发送消息失败_对方关闭了接收消息 = 10703, 对方不是粉丝 = 10700 } /// <summary> /// 返回接口 /// </summary> interface IWxJsonResult : IJsonResult { ReturnCode errcode { get; set; } } /// <summary> /// 公众号JSON返回结果(用于菜单接口等) /// </summary> [Serializable] class WxJsonResult : IWxJsonResult { public ReturnCode errcode { get; set; } public string errmsg { get; set; } /// <summary> /// 为P2P返回结果做准备 /// </summary> public virtual object P2PData { get; set; } } #endregion } /// <summary> /// access_token请求后的JSON返回格式 /// </summary> [Serializable] public class AccessTokenResult { /// <summary> /// 获取到的凭证 /// </summary> public string access_token { get; set; } /// <summary> /// 凭证有效时间,单位:秒 /// </summary> public int expires_in { get; set; } /// <summary> /// 凭证过期有效时间 /// </summary> public DateTime expiresAt { get; set; } } }
通过缓存access_token方式实现以后,同步微信后台用户数据正常了。
==============
上面第一个方法可以作为C#开发的同学的获取AccessToken的公共方法,因为还有其他语言开发的同学,所以在这里又增加了一个获取AccessToken的对外接口
//----------------------------------------------------------------------- // <copyright file="WeChatService.ashx" company="Hairihan"> // Copyright (C) 2016 , All rights reserved. // </copyright> //----------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace DotNet.UserCenter { using DotNet.Business.HttpUtilities; using DotNet.Utilities; /// <summary> /// WeChatService /// /// 修改记录 /// /// /// 2016-11-17 版本:1.0 SongBiao 创建 /// /// <author> /// <name>SongBiao</name> /// <date>2016-11-17</date> /// </author> /// </summary> public class WeChatService : IHttpHandler { /// <summary> /// 获取服务器时间 /// </summary> /// <param name="context"></param> private void GetServerDateTime(HttpContext context) { JsonResult<string> jsonResult = new JsonResult<string>() { Status = true, StatusMessage = "成功获取服务器时间", Data = DateTime.Now.ToString(BaseSystemInfo.DateTimeFormat) }; context.Response.Write(jsonResult.ToJson()); } /// <summary> /// 获取用户中心库时间 /// </summary> /// <param name="context"></param> private void GetDbDateTime(HttpContext context) { JsonResult<string> jsonResult = new JsonResult<string>(); try { using (IDbHelper dbHelper = DbHelperFactory.GetHelper(BaseSystemInfo.UserCenterDbType, BaseSystemInfo.UserCenterDbConnection)) { string result = DateTime.Parse(dbHelper.GetDbDateTime()).ToString(BaseSystemInfo.DateTimeFormat); jsonResult.Status = true; jsonResult.StatusMessage = "成功获取用户中心库时间"; jsonResult.Data = result; } } catch (Exception ex) { jsonResult.Status = true; jsonResult.StatusMessage = "获取用户中心库时间异常:" + ex.Message; NLogHelper.Trace(ex, "UserService GetDbDateTime 异常"); } context.Response.Write(jsonResult.ToJson()); } /// <summary> /// 获取AccessToken /// </summary> /// <param name="context"></param> private void GetAccessToken(HttpContext context) { BaseResult baseResult = new BaseResult(); try { string accessToken = WeChatUtilities.GetAccessToken(); if (!string.IsNullOrWhiteSpace(accessToken)) { baseResult.Status = true; baseResult.ResultValue = accessToken; baseResult.StatusMessage = Status.OK.GetDescription(); } else { baseResult.Status = false; baseResult.StatusMessage = "AccessToken获取失败。"; } } catch (Exception ex) { baseResult.Status = false; baseResult.StatusMessage = "AccessToken获取异常:"+ex.Message; } context.Response.Write(baseResult.ToJson()); } public void ProcessRequest(HttpContext context) { context.Response.ContentType = "text/plain"; if (context.Request["function"] == null) { this.GetServerDateTime(context); } else { string function = context.Request["function"]; if (function.Equals("GetServerDateTime", StringComparison.OrdinalIgnoreCase)) { this.GetServerDateTime(context); } else if (function.Equals("GetAccessToken", StringComparison.OrdinalIgnoreCase)) { this.GetAccessToken(context); } else { context.Response.Write(BaseResult.Error("function对应方法不存在。").ToJson()); } context.Response.End(); } } public bool IsReusable { get { return false; } } } }