公司项目,要集成微信公众号实现登录和消息推送,但没有独立服务器和域名,且由于安全考虑需部署在内网,因此建立以下模型实现
背景:CRM集成微信用户登录,实现微信关注公众号,从菜单访问CRM系统,获得授权后绑定账号,无账号新建后自动绑定,此后每次微信授权即可直接登录。
难点:CRM系统部署在内网环境,且端口号不为80,因此与微信服务器无法直接通讯(主要是指获得微信回调)。
解决过程:
1、明确网络架构
“应用服务器”向微信发起申请code和access_token等请求是不受限制的,但由于端口和域名限制而无法接收其回调,所以使用有绑定域名的“公网服务器”用于接收微信回调请求,并在接收请求后立即原样将ResponseContent传递给“应用服务器”,从而模拟“应用服务器”直接接收回调的效果。
2、开发过程
2.1、通过Nuget安装Senparc.Weixin组件:
"Senparc.Weixin": "4.19.0", //基础库
"Senparc.Weixin.MP": "14.4.4" //公众号相关功能
2.2、应用服务器发起微信登录请求
请求过程基本包括:
1、用户是否已登录
2、获取OAuth验证地址(借助appId和srcret)
3、弹出用户授权确认,回调获取code
4、使用code换取access_token和用户信息(包含openid)
5、根据openid数据库查找用户是否存在
6、存在,直接登录;不存在,向微信服务器申请获取用户信息,回调并跳转到绑定/新建账号界面
7、登录
在首页新增“微信登录”按钮,点击后跳转至OAuth验证地址:
(图片略)
生成OAuth验证地址的代码实现:
引用:using Senparc.Weixin.MP.AdvancedAPIs;
Controller:
[AllowAnonymous] [HttpGet("login")] public IActionResult Login(string err="") { string SessionID = Guid.NewGuid().ToString(); // 用state作为参数,存入缓存中,并在回调中加以判断,避免在多次握手过程中泄露redirect_url及参数导致的安全问题,以增强系统安全性 HttpContext.Session.SetString("SessionID", SessionID); _memoryCache.Set<string>(SessionID, "", TimeSpan.FromMinutes(15));//15分钟过期 ViewBag.UrlUserInfo = OAuthApi.GetAuthorizeUrl(appId, wx_proxy, SessionID, Senparc.Weixin.MP.OAuthScope.snsapi_userinfo); ViewBag.error = err; return View("Login"); }
View: <a class="layadmin-user-jump-change gray" href="@ViewBag.UrlUserInfo" id="weixin" >微信登录</a>
2.3、应用服务器接收回调方法编写
新建WebAPI,实现接口:
/// <summary> /// 微信用户授权回调,获取用户信息 /// </summary> /// <param name="code"></param> /// <param name="state">传递给微信服务器的参数</param> /// <param name="proxy_state">从回调代理服务器传来的参数</param> /// <returns></returns> [HttpGet("userinfocallback")] public ActionResult UserInfoCallback(string code, string state, string proxy_state) { if (string.IsNullOrEmpty(code)) { return Ok(new { success = false, msg = "您拒绝了授权" }); } if (_memoryCache.Get(state) == null) { return Ok(new { success = false, msg = "验证失败!请从正规途径访问" }); } OAuthAccessTokenResult result = null; try { result = OAuthApi.GetAccessToken(appId, secret, code); } catch (Exception ex) { return Ok(new { success = false, msg = "出错:" + ex.Message }); } if (result.errcode != ReturnCode.请求成功) { return Ok(new { success = false, msg = "请求错误:" + result.errmsg }); } try { OAuthUserInfo userInfo = OAuthApi.GetUserInfo(result.access_token, result.openid); bool hasReg = _context.WEIXIN_USERs.Where(w => w.IS_DELETE == false && w.OPENID == userInfo.openid).Count() > 0 ? true : false; if (hasReg) { } else { //创建微信用户信息 WEIXIN_USER wx_user = new WEIXIN_USER(); wx_user.GID = Guid.NewGuid().ToString(); wx_user.UNIONID = string.IsNullOrEmpty(userInfo.unionid) ? string.Empty : userInfo.unionid; wx_user.SEX = userInfo.sex == 1 ? "男" : (userInfo.sex == 2 ? "女" : "未知"); wx_user.PROVINCE = string.IsNullOrEmpty(userInfo.province) ? string.Empty : userInfo.province; wx_user.OPENID = string.IsNullOrEmpty(userInfo.openid) ? string.Empty : userInfo.openid; wx_user.NICKNAME = string.IsNullOrEmpty(userInfo.nickname) ? string.Empty : userInfo.nickname; wx_user.MODIFY_DATE = DateTime.Now; wx_user.CREATE_DATE = DateTime.Now; wx_user.IS_DELETE = false; wx_user.HEADIMGURL = string.IsNullOrEmpty(userInfo.headimgurl) ? string.Empty : userInfo.headimgurl; wx_user.COUNTRY = string.IsNullOrEmpty(userInfo.country) ? string.Empty : userInfo.country; wx_user.CITY = string.IsNullOrEmpty(userInfo.city) ? string.Empty : userInfo.city; wx_user.BZ1 = appId; wx_user.BZ2 = string.Empty; _context.WEIXIN_USERs.Add(wx_user); _context.SaveChanges(); } //将用户openid写入缓存 _memoryCache.Set(state, userInfo.openid); return Ok(new { success = true, openid = userInfo.openid }); } catch (Exception ex) { return Ok(new { success = false, msg = "获取用户信息错误:" + ex.Message }); } }
2.4、公网服务器(中间服务器)功能编写
新建ashx一般处理程序,在其中实现接口:
try { //获得URL参数 string URLParam = HttpContext.Current.Request.Url.Query; string URL = System.Configuration.ConfigurationManager.AppSettings["wx_host"].ToString()+"/api/weixin/userinfocallback"; string NewURL = String.Format("{0}{1}", URL, URLParam);//得到新的带参数的URL地址 string result; if (context.Request.HttpMethod == "GET")//如果是Get请求,直接以Get方式调用自己的接口,一般用在微信接入验证 { result = GetWebData(NewURL); } else//Post请求,一般用在发送消息 { context.Request.InputStream.Position = 0; StreamReader reader = new StreamReader(context.Request.InputStream); string sReqData = reader.ReadToEnd();//加密XML信息 result = PostWithParm(NewURL, sReqData); } JsonData r= JsonMapper.ToObject(result); if (r["success"].ToString().ToLower()=="true") { string openid = r["openid"].ToString().ToLower(); HttpContext.Current.Response.Redirect(System.Configuration.ConfigurationManager.AppSettings["wx_host"].ToString() + "/wxlogin/" + openid, true); } else { string error=HttpContext.Current.Server.UrlEncode(r["msg"].ToString().Trim()); HttpContext.Current.Response.Redirect(System.Configuration.ConfigurationManager.AppSettings["wx_host"].ToString() + "/login?err="+error, true); } } catch(Exception ex) { HttpContext.Current.Response.Write(ex.ToString()); }
将其接收微信服务器回调请求后发送的地址设置为应用服务器的回调方法。
2.5、创建账号、登录方法省略