欢迎各位兄弟 发布技术文章
这里的技术是共享的
紧接上一篇《Senparc.Weixin.MP SDK 微信公众平台开发教程(十一):高级接口说明》,这里专讲OAuth2.0。
首先我们通过一张图片来了解一下OAuth2.0的运作模式:
从上图我们可以看到,整个过程进行了2次“握手”,最终利用授权的AccessToken进行一系列的请求,相关的过程说明如下:
了解了OAuth2.0的基本原理之后,我们来看一下OAuth2.0在微信中是如何运用的。
假设一个场景:用户进入了一个微信公众账号,随后通过消息中的链接,在微信内嵌浏览器中打开了一个游戏网页,这个游戏需要用户登录并且记录用户的游戏得分。
这种情况下我们有两种处理方式:
可以看出,使用OAuth2.0大幅度提高了用户体验,并且可以自动绑定识别用户微信OpenId。
需要注意的是,上面提到的“OAuth2.0授权页面”还有两种形式:
也就是说,snsapi_base的方法可以“神不知鬼不觉”地获取用户OpenId,全自动完成登录注册过程,但是信息量有限;snsapi_userinfo需要用户在指定界面上授权之后,自动完成整个过程,这个授权有一个时间段,超过时间后需要重新询问用户。
源文件文件夹:Senparc.Weixin.MP/AdvancedAPIs/OAuth
相比其他接口OAuth2.0略微复杂,相关内容将在下一篇专门进行介绍:Senparc.Weixin.MP SDK 微信公众平台开发教程(十二):OAuth2.0说明
源代码中相关方法如下:
namespace Senparc.Weixin.MP.AdvancedAPIs { //官方文档:http://mp.weixin.qq.com/wiki/index.php?title=%E7%BD%91%E9%A1%B5%E6%8E%88%E6%9D%83%E8%8E%B7%E5%8F%96%E7%94%A8%E6%88%B7%E5%9F%BA%E6%9C%AC%E4%BF%A1%E6%81%AF#.E7.AC.AC.E4.B8.80.E6.AD.A5.EF.BC.9A.E7.94.A8.E6.88.B7.E5.90.8C.E6.84.8F.E6.8E.88.E6.9D.83.EF.BC.8C.E8.8E.B7.E5.8F.96code /// <summary> /// 应用授权作用域 /// </summary> public enum OAuthScope { /// <summary> /// 不弹出授权页面,直接跳转,只能获取用户openid /// </summary> snsapi_base, /// <summary> /// 弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,即使在未关注的情况下,只要用户授权,也能获取其信息 /// </summary> snsapi_userinfo } public static class OAuth { /// <summary> /// 获取验证地址 /// </summary> /// <param name="appId"></param> /// <param name="redirectUrl"></param> /// <param name="state"></param> /// <param name="scope"></param> /// <param name="responseType"></param> /// <returns></returns> public static string GetAuthorizeUrl(string appId, string redirectUrl, string state, OAuthScope scope, string responseType = "code") { var url = string.Format("https://open.weixin.qq.com/connect/oauth2/authorize?appid={0}&redirect_uri={1}&response_type={2}&scope={3}&state={4}#wechat_redirect", appId, redirectUrl.UrlEncode(), responseType, scope, state); /* 这一步发送之后,客户会得到授权页面,无论同意或拒绝,都会返回redirectUrl页面。 * 如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE。这里的code用于换取access_token(和通用接口的access_token不通用) * 若用户禁止授权,则重定向后不会带上code参数,仅会带上state参数redirect_uri?state=STATE */ return url; } /// <summary> /// 获取AccessToken /// </summary> /// <param name="appId"></param> /// <param name="secret"></param> /// <param name="code">code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。</param> /// <param name="grantType"></param> /// <returns></returns> public static OAuthAccessTokenResult GetAccessToken(string appId, string secret, string code, string grantType = "authorization_code") { var url = string.Format("https://api.weixin.qq.com/sns/oauth2/access_token?appid={0}&secret={1}&code={2}&grant_type={3}", appId, secret, code, grantType); return CommonJsonSend.Send<OAuthAccessTokenResult>(null, url, null, CommonJsonSendType.GET); } /// <summary> /// 刷新access_token(如果需要) /// </summary> /// <param name="appId"></param> /// <param name="refreshToken">填写通过access_token获取到的refresh_token参数</param> /// <param name="grantType"></param> /// <returns></returns> public static OAuthAccessTokenResult RefreshToken(string appId, string refreshToken, string grantType = "refresh_token") { var url = string.Format("https://api.weixin.qq.com/sns/oauth2/refresh_token?appid={0}&grant_type={1}&refresh_token={2}", appId, grantType, refreshToken); return CommonJsonSend.Send<OAuthAccessTokenResult>(null, url, null, CommonJsonSendType.GET); } public static OAuthUserInfo GetUserInfo(string accessToken,string openId) { var url = string.Format("https://api.weixin.qq.com/sns/userinfo?access_token={0}&openid={1}",accessToken,openId); return CommonJsonSend.Send<OAuthUserInfo>(null, url, null, CommonJsonSendType.GET); } } }
具体的示例方法见Senparc.Weixin.MP.Sample/Controllers/OAuth2Controller.cs,以及对应视图的代码。
系列教程索引:http://www.cnblogs.com/szw/archive/2013/05/14/weixin-course-index.html
新浪微博:@苏震巍 QQ:498977166
http://szw.cnblogs.com/
研究、探讨ASP.NET
转载请注明出处和作者,谢谢!
来自 http://www.cnblogs.com/szw/p/3764275.html
为了方便大家开发LBS应用,SDK对常用计算公式,以及百度和谷歌的地图接口做了封装。
常用计算:
用于计算2个坐标点之间的直线距离:Senparc.Weixin.MP.Helpers.Distance(double n1, double e1, double n2, double e2)
根据距离获取维度差:Senparc.Weixin.MP.Helpers.GetLatitudeDifference(double km)
根据距离获取经度差:Senparc.Weixin.MP.Helpers.GetLongitudeDifference(double km)
百度API类:Senparc.Weixin.MP.Helpers.BaiduMapHelper
生成百度静态地图URL:BaiduMapHelper.GetBaiduStaticMap(double lng, double lat, int scale, int zoom, IList<BaiduMarkers> markersList, int width = 400, int height = 300)
最后生成的地址如下:
生成的URL可以直接放到<img>中,或者直接赋值在ResponseMessageNews的Article.PicUrl。
对应的GoogleMap API,SDK中做了一致的操作体验。
GoogleMap API类:Senparc.Weixin.MP.Helpers.GoogleMapHelper
生成百度静态地图URL:GoogleMapHelper.GetGoogleStaticMap(int scale, IList<GoogleMapMarkers> markersList, string size = "640x640")
生成的地址如下:
结合SDk,我们可以在用户发送位置消息过来的时候,使用地图接口做一些功能,例如我们在MessageHandler的OnLocationRequest实践中对消息进行处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | /// <summary> /// 处理位置请求 /// </summary> /// <param name="requestMessage"></param> /// <returns></returns> public override IResponseMessageBase OnLocationRequest(RequestMessageLocation requestMessage) { var responseMessage = ResponseMessageBase.CreateFromRequestMessage<ResponseMessageNews>(requestMessage); var markersList = new List<GoogleMapMarkers>(); markersList.Add( new GoogleMapMarkers() { X = requestMessage.Location_X, Y = requestMessage.Location_Y, Color = "red" , Label = "S" , Size = GoogleMapMarkerSize.Default, }); var mapSize = "480x600" ; var mapUrl = GoogleMapHelper.GetGoogleStaticMap(19 /*requestMessage.Scale*/ /*微信和GoogleMap的Scale不一致,这里建议使用固定值*/ , markersList, mapSize); responseMessage.Articles.Add( new Article() { Description = string .Format( "您刚才发送了地理位置信息。Location_X:{0},Location_Y:{1},Scale:{2},标签:{3}" , requestMessage.Location_X, requestMessage.Location_Y, requestMessage.Scale, requestMessage.Label), PicUrl = mapUrl, Title = "定位地点周边地图" , Url = mapUrl }); responseMessage.Articles.Add( new Article() { Title = "微信公众平台SDK 官网链接" , Description = "Senparc.Weixin.MK SDK地址" , }); |
1 | return responseMessage;<br> } |
实际的开发过程中,除了输出位置的信息,我们还可以根据用户的当前位置,检索就近的点,在Articles中输出,并计算出距离。
系列教程索引:http://www.cnblogs.com/szw/archive/2013/05/14/weixin-course-index.html
新浪微博:@苏震巍 QQ:498977166
http://szw.cnblogs.com/
研究、探讨ASP.NET
转载请注明出处和作者,谢谢!
来自 http://www.cnblogs.com/szw/p/4138442.html
为了确保信息请求消息的到达率,微信服务器在没有及时收到响应消息(ResponseMessage)的情况下,会多次发送同一条请求消息(RequestMessage),包括MsgId等在内的所有文本内容都是一致的。
这种机制确保了在诸如网络状况不佳的情况下消息的回复成功率,但是有时候由于服务器负荷、本身请求过程就需要好几秒才能完成等情况,多次重复的消息反而成了服务器的负担,甚至对业务和数据也可能造成影响。
针对这种情况,SDK增加了去重的设置,只需要在使用MessageHandler的时候加一句话即可:
1 | messageHandler.OmitRepeatedMessage = true ; //启用消息去重功能 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | /// <summary> /// 最简化的处理流程(不加密) /// </summary> [HttpPost] [ActionName( "MiniPost" )] public ActionResult MiniPost( string signature, string timestamp, string nonce, string echostr) { if (!CheckSignature.Check(signature, timestamp, nonce, Token)) { return new WeixinResult( "参数错误!" ); //v0.8+ } var messageHandler = new CustomMessageHandler(Request.InputStream, null , 10); messageHandler.OmitRepeatedMessage = true ; //启用消息去重功能 messageHandler.Execute(); //执行微信处理过程 return new FixWeixinBugWeixinResult(messageHandler); } |
去重的原理是通过当前用户的上下文,判断当前请求消息和上一条请求消息的MsgId是否一致,如果一直的话则终止向下执行。
文件:Senparc.Weixin.MessageHandlers.MessageHandler.cs
1 2 3 4 5 6 7 8 9 10 11 | public virtual void OnExecuting() { if (OmitRepeatedMessage && CurrentMessageContext.RequestMessages.Count > 1) { var lastMessage = CurrentMessageContext.RequestMessages[CurrentMessageContext.RequestMessages.Count - 2]; if (lastMessage.MsgId != 0 && lastMessage.MsgId == RequestMessage.MsgId) { CancelExcute = true ; //重复消息,取消执行 } } } |
去重的效果可以在模拟工具中体验:http://weixin.senparc.com/SimulateTool
系列教程索引:http://www.cnblogs.com/szw/archive/2013/05/14/weixin-course-index.html
新浪微博:@苏震巍 QQ:498977166
http://szw.cnblogs.com/
研究、探讨ASP.NET
转载请注明出处和作者,谢谢!
来自 http://www.cnblogs.com/szw/p/4138516.html
前不久,微信的企业号使用了强制的消息加密方式,随后公众号也加入了可选的消息加密选项。目前企业号和公众号的加密方式是一致的(格式会有少许差别)。
进入公众号后台的“开发者中心”,我们可以看到Url对接的设置:
点击【修改设置】,可以进入到修改页面:
加密的方式一共有3种:
凡是加密的消息,返回的信息也需要经过加密。
Senparc.Weixin.MP已经对三类消息作了自动判断,开发的过程中无需关注任何解密和加密的过程,仍然保持“明文模式”下的开发过程即可。
对应的MessageHandler中,我们可以通过一些参数得知目前的加密状态:
messageHandler.UsingEcryptMessage:是否使用了加密信息(包括兼容模式和安全模式)
messageHandler.UsingCompatibilityModelEcryptMessage:是否使用了兼容模式加密信息
通过上面2个属性的组合,我们便可以知道目前账号使用的是哪种加密模式(当然,大多数情况下开发者已经无需关心)。
为了可以更好地跟踪信息,MessageHandler新加入了FinalResponseDocument这个属性:
messageHandler.ResponseDocument:明文结构的响应数据XML对象
messageHandler.FinalResponseDocument:最终会返回给服务器的XML对象,在不加密的情况下将和ResponseDocument一致,否则会自动进行加密
有关加密的算法(包括几种语言的示例下载)在官方的帮助文档里面可以找到:http://mp.weixin.qq.com/wiki/index.php?title=%E6%8A%80%E6%9C%AF%E6%96%B9%E6%A1%88
这里要说明一下的是EncodingAESKey,官方的解释有点绕。实际上EncodingAESKey是对AESKey的一次Base64编码处理,而AESKey是一个长度为32的随机字符串(从a-z,A-Z,0-9中选取)。由于32个字符的Base64编码长度为固定的44(最后一个字符为=),所以去掉=之后,就生成了最终的43个字符长度的EncodingAESKey。EncodingAESKey在消息加密和解密的过程中都会用到,需要进行严格的保密。
下面是一段C#生成EncodingAESKey的代码:
1 2 3 4 5 6 | protected string CreateEncodingAESKey() { string aesKey = GetRadomStr(32); //获得a-z,A-Z,0-9的随机字符串 var encodingAesKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(aesKey), Base64FormattingOptions.None); return encodingAesKey.Substring(0, encodingAesKey.Length - 1); } |
系列教程索引:http://www.cnblogs.com/szw/archive/2013/05/14/weixin-course-index.html
来自 http://www.cnblogs.com/szw/p/4140069.html
在《Senparc.Weixin.MP SDK 微信公众平台开发教程(八):通用接口说明》中,我介绍了获取AccessToken(通用接口)的方法。
在实际的开发过程中,所有的高级接口都需要提供AccessToken,因此我们每次在调用高级接口之前,都需要执行一次获取AccessToken的方法,例如:
1 | var accessToken = AccessTokenContainer.TryGetAccessToken(appId, appSecret); |
或者当你对appId和appSecret进行过全局注册之后,也可以这样做:
1 | var accessToken = AccessTokenContainer.GetAccessToken(_appId); |
然后使用这个accessToken输入到高级接口的方法中,例如我们可以这样获取菜单:
1 | var result = CommonApi.GetMenu(accessToken); |
通常情况下,这已经是一个很简洁的API调用过程。但是我们不愿意就这样停止,我们准备把几乎所有的API调用都缩短到一行。
这么做的同时,除了让代码更加简便,我们还有两个愿望:
修改之后,我们可以直接这样一行调用API,每次只需要提供一个appId:
1 | var result = CommonApi.GetMenu(appId); |
当前在执行之前,我们需要像以前一样全局注册一下appId和appSecret:
1 | AccessTokenContainer.Register(_appId, _appSecret); //全局只需注册一次,例如可以放在Global的Application_Start()方法中。 |
可以看到,原先的accessToken换成了appId(新版本仍然支持输入accessToken),省去了获取accessToken的过程。具体的过程见下文说明。
之前为了实现自动处理(预料外的)过期的AccessToken,SDK已经提供了Senparc.Weixin.MP/AccessTokenHandlerWapper.Do()方法。这次升级将AccessTokenHandlerWapper.cs重命名为ApiHandlerWapper.cs,废除Do()方法,添加TryCommonApi()方法,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | namespace Senparc.Weixin.MP { /// <summary> /// 针对AccessToken无效或过期的自动处理类 /// </summary> public static class ApiHandlerWapper { /// <summary> /// 使用AccessToken进行操作时,如果遇到AccessToken错误的情况,重新获取AccessToken一次,并重试。 /// 使用此方法之前必须使用AccessTokenContainer.Register(_appId, _appSecret);或JsApiTicketContainer.Register(_appId, _appSecret);方法对账号信息进行过注册,否则会出错。 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="fun"></param> /// <param name="accessTokenOrAppId">AccessToken或AppId。如果为null,则自动取已经注册的第一个appId/appSecret来信息获取AccessToken。</param> /// <param name="retryIfFaild">请保留默认值true,不用输入。</param> /// <returns></returns> public static T TryCommonApi<T>(Func< string , T> fun, string accessTokenOrAppId = null , bool retryIfFaild = true ) where T : WxJsonResult { string appId = null ; string accessToken = null ; if (accessTokenOrAppId == null ) { appId = AccessTokenContainer.GetFirstOrDefaultAppId(); if (appId == null ) { throw new WeixinException( "尚无已经注册的AppId,请先使用AccessTokenContainer.Register完成注册(全局执行一次即可)!" ); } } else if (ApiUtility.IsAppId(accessTokenOrAppId)) { if (!AccessTokenContainer.CheckRegistered(accessTokenOrAppId)) { throw new WeixinException( "此appId尚未注册,请先使用AccessTokenContainer.Register完成注册(全局执行一次即可)!" ); } appId = accessTokenOrAppId; } else { //accessToken accessToken = accessTokenOrAppId; } T result = null ; try { if (accessToken == null ) { var accessTokenResult = AccessTokenContainer.GetAccessTokenResult(appId, false ); accessToken = accessTokenResult.access_token; } result = fun(accessToken); } catch (ErrorJsonResultException ex) { if (!retryIfFaild && appId != null && ex.JsonResult.errcode == ReturnCode.获取access_token时AppSecret错误或者access_token无效) { //尝试重新验证 var accessTokenResult = AccessTokenContainer.GetAccessTokenResult(appId, true ); accessToken = accessTokenResult.access_token; result = TryCommonApi(fun, appId, false ); } } return result; } } } |
对应API的源代码原来是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | /// <summary> /// 获取当前菜单,如果菜单不存在,将返回null /// </summary> /// <param name="accessToken"></param> /// <returns></returns> public static GetMenuResult GetMenu( string accessToken) { var url = string .Format( "https://api.weixin.qq.com/cgi-bin/menu/get?access_token={0}" , accessToken); var jsonString = HttpUtility.RequestUtility.HttpGet(url, Encoding.UTF8); //var finalResult = GetMenuFromJson(jsonString); GetMenuResult finalResult; JavaScriptSerializer js = new JavaScriptSerializer(); try { var jsonResult = js.Deserialize<GetMenuResultFull>(jsonString); if (jsonResult.menu == null || jsonResult.menu.button.Count == 0) { throw new WeixinException(jsonResult.errmsg); } finalResult = GetMenuFromJsonResult(jsonResult); } catch (WeixinException ex) { finalResult = null ; } return finalResult; } |
现在使用TryCommonApi()方法之后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | /// <summary> /// 获取当前菜单,如果菜单不存在,将返回null /// </summary> /// <param name="accessTokenOrAppId">AccessToken或AppId。当为AppId时,如果AccessToken错误将自动获取一次。当为null时,获取当前注册的第一个AppId。</param> /// <returns></returns> public static GetMenuResult GetMenu( string accessTokenOrAppId) { return ApiHandlerWapper.TryCommonApi(accessToken => { var url = string .Format( "https://api.weixin.qq.com/cgi-bin/menu/get?access_token={0}" , accessToken); var jsonString = HttpUtility.RequestUtility.HttpGet(url, Encoding.UTF8); //var finalResult = GetMenuFromJson(jsonString); GetMenuResult finalResult; JavaScriptSerializer js = new JavaScriptSerializer(); try { var jsonResult = js.Deserialize<GetMenuResultFull>(jsonString); if (jsonResult.menu == null || jsonResult.menu.button.Count == 0) { throw new WeixinException(jsonResult.errmsg); } finalResult = GetMenuFromJsonResult(jsonResult); } catch (WeixinException ex) { finalResult = null ; } return finalResult; }, accessTokenOrAppId); } |
我们可以观察到有这样几处变化:
1. 原先的accessToken变量名称改为了accessTokenOrAppId(新版本中所有相关接口都将如此变化)。
修改之后,这个参数可以输入accessToken(向下兼容),也可以输入appId(无需再获取accessToken),SDK会根据字符串长度自动判断属于哪种类型的参数。提供的参数有3种可能:
a) appId。使用appId需要事先对appId和appSecret进行全局注册(上文已说过),当调用API的过程中发现缓存的AccessToken过期时,SDK会自动刷新AccessToken,并重新尝试一次API请求,确保返回正确的结果。如果appId没有被注册过,会抛出异常。
b) accessToken。这种情况下将使用原始的请求方式,如果accessToken无效,将直接抛出异常,不会重试。
c) null。当accessTokenOrAppId参数为null时,SDK会自动获取全局注册的第一个appId。如果某个应用只针对一个确定的微信号开发,可以使用这种方法。当全局没有注册任何appId时,将抛出异常。
2. 原方法内的访问API的代码没有做任何修改,只是被嵌套到了return ApiHandlerWapper.TryCommonApi(accessToken =>{...},accessTokenOrAppId)的方法中,以委托的形式出现,目的是为了在第一次可能的请求失败之后,SDK可以自动执行一次一模一样的代码。
此功能已经在Senparc.Weixin.MP v12.1中发布。
系列教程索引:http://www.cnblogs.com/szw/archive/2013/05/14/weixin-course-index.html
新浪微博:@苏震巍 QQ:498977166
http://szw.cnblogs.com/
研究、探讨ASP.NET
转载请注明出处和作者,谢谢!
来自 http://www.cnblogs.com/szw/p/4624149.html