1.开发前准备
参数获取
corpid
每个企业都拥有唯一的corpid,获取此信息可在管理后台“我的企业”-“企业信息”下查看“企业ID”
secret
secret是企业应用里面用于保障数据安全的“钥匙”,每一个应用都有一个独立的访问密钥,为了保证数据的安全,secret务必不能泄漏。
框架
例子使用yishaadmin开源框架为例
2.企业微信OAuth2接入流程
第一步: 用户点击链接
第二步: Index页取得回调Code
第三步: 根据Code和access_token获取UserID
第四步: 根据UserID到通讯录接口获取其他信息
3.构造网页授权链接
假定当前企业CorpID:wxCorpId
访问链接:http://api.3dept.com/cgi-bin/query?action=get
根据URL规范,将上述参数分别进行UrlEncode,得到拼接的OAuth2链接为:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxCorpId&redirect_uri=http%3a%2f%2fapi.3dept.com%2fcgi-bin%2fquery%3faction%3dget&response_type=code&scope=snsapi_base&state=#wechat_redirect
然后新建应用,将链接放入,配置应用可信域名。
官方文档链接:https://developer.work.weixin.qq.com/document/path/91335
4. 调用代码部分
4.1 appsettings配置
"Wx": {"corpid":"","corpsecret":"","baseurl":"https://qyapi.weixin.qq.com","getUserByCode":"/cgi-bin/user/getuserinfo?access_token={0}&code={1}","getToken":"/cgi-bin/gettoken?corpid={0}&corpsecret={1}","getUserByUserId":"/cgi-bin/user/get?access_token={0}&userid={1}" }
4.2 配置IHttpClientFactory调用微信客户端
publicstatic IHttpClientFactory httpClientFactory {get;set; }
Startup添加以下内容
publicvoid ConfigureServices(IServiceCollection services) { services.AddHttpClient("WxClient", config => { config.BaseAddress=new Uri(Configuration["Wx:baseurl"]); config.DefaultRequestHeaders.Add("Accept","application/json"); }); }
publicvoid Configure(IApplicationBuilder app, IWebHostEnvironment env) { GlobalContext.httpClientFactory= app.ApplicationServices.GetService<IHttpClientFactory>(); }
4.3 类准备
UserCache 类保存用户id,头像,用户名,以及code,按需新增。
using System;using System.Collections.Generic;using System.Text;namespace YiSha.Model.Result {publicclass UserCache {///<summary>/// 用户id///</summary>publicstring UserID {get;set; }///<summary>/// 头像///</summary>publicstring Portrait {get;set; }///<summary>/// 用户名///</summary>publicstring Username {get;set; }///<summary>/// 缓存最近一次Code 用于刷新时code不更新问题///</summary>publicstring Code {get;set; } } }
ApplicationContext用于缓存Token 过期时间以及用户集合避免多次调用微信接口提高响应速度
using System;
using System.Collections.Generic;using System.Linq;using System.Threading.Tasks;using YiSha.Model.Result;namespace YiSha.Admin.Web.App_Code {publicstaticclass ApplicationContext {///<summary>/// 用于多点登录的微信用户///</summary>publicconststring WxUser ="taskUser";///<summary>/// 用于多点登录的微信密码///</summary>publicconststring WxPassWord ="123456";///<summary>/// 过期时间///</summary>publicstatic DateTime TimeOutDate {get;set; }///<summary>/// Token///</summary>publicstaticstring Token {get;set; }///<summary>/// 缓存UserID Name 头像///</summary>publicstatic List<UserCache> UserCache {get;set; } =new List<UserCache>(); } }
获取Token返回实体
using System;using System.Collections.Generic;using System.Text;namespace YiSha.Entity.OAManage
{publicclass GetTokenResult
{///<summary>/// 错误编号///</summary>publicint errcode {get;set; }///<summary>/// 错误信息///</summary>publicstring errmsg {get;set; }///<summary>/// Token///</summary>publicstring access_token {get;set; }///<summary>/// 过期时间///</summary>publicint expires_in {get;set; }
}
}
获取用户id返回实体
using System;using System.Collections.Generic;using System.Text;namespace YiSha.Entity.OAManage
{//获取用户IDpublicclass GetUserInfoResult
{///<summary>/// 错误编号///</summary>publicint errcode {get;set; }///<summary>/// 错误信息///</summary>publicstring errmsg {get;set; }///<summary>/// 用户ID///</summary>publicstring UserID {get;set; }
}
}
获取用户通讯录返回实体
using System;using System.Collections.Generic;using System.Text;namespace YiSha.Entity.OAManage {publicclass GetUserResult {///<summary>/// 错误编号///</summary>publicint errcode {get;set; }///<summary>/// 错误信息///</summary>publicstring errmsg {get;set; }///<summary>/// 名称///</summary>publicstring name {get;set; }///<summary>/// 头像///</summary>publicstring avatar {get;set; } } }
4.4方法准备
获取Token方法,该方法对Token进行了一个缓存,避免重复获取.
注意事项:
开发者需要缓存access_token,用于后续接口的调用(注意:不能频繁调用gettoken接口,否则会受到频率拦截)。当access_token失效或过期时,需要重新获取。
access_token的有效期通过返回的expires_in来传达,正常情况下为7200秒(2小时),有效期内重复获取返回相同结果,过期后获取会返回新的access_token。
由于企业微信每个应用的access_token是彼此独立的,所以进行缓存时需要区分应用来进行存储。
access_token至少保留512字节的存储空间。
企业微信可能会出于运营需要,提前使access_token失效,开发者应实现access_token失效时重新获取的逻辑。
获取Token文档链接https://developer.work.weixin.qq.com/document/path/91039#15074
///<summary>/// 获取Token///</summary>///<returns>Item1 Token;Item2 是否成功</returns>publicstatic Tuple<string,bool> GetToken() {//判断Token是否存在 以及Token是否在有效期内if (string.IsNullOrEmpty(Token) || TimeOutDate > DateTime.Now) {//构造请求链接var requestBuild = GlobalContext.Configuration["Wx:getToken"]; requestBuild=string.Format(requestBuild, GlobalContext.Configuration["Wx:corpid"], GlobalContext.Configuration["Wx:corpsecret"] );using (var wxClient = GlobalContext.httpClientFactory.CreateClient("WxClient")) {var httpResponse = wxClient.GetAsync(requestBuild).Result;vardynamic = JsonConvert.DeserializeObject<GetTokenResult>( httpResponse.Content.ReadAsStringAsync().Result );if (dynamic.errcode ==0) { Token=dynamic.access_token;//过期5分钟前刷新Tokenvar expires_in = Convert.ToDouble(dynamic.expires_in -5 *60); TimeOutDate= DateTime.Now.AddSeconds(expires_in);return Tuple.Create(Token,true); }else {return Tuple.Create($"获取Token失败,错误:{ dynamic.errmsg}",false); } } }else {return Tuple.Create(Token,true); } }
获取用户ID方法,该方法根据获取到的token,以及回调的code进行请求,得到用户id实体
获取访问用户身份文档链接:https://developer.work.weixin.qq.com/document/path/91023
///<summary>/// 获取用户ID///</summary>///<param name="token">企业微信Token</param>///<param name="code">构造请求的回调code</param>///<returns>Item1 UserId;Item2 是否成功</returns>publicstatic Tuple<string,bool> GetUserID(string token,string code) {//构造请求链接var requestBuild = GlobalContext.Configuration["Wx:getUserByCode"]; requestBuild=string.Format(requestBuild, token, code);using (var wxClient = GlobalContext.httpClientFactory.CreateClient("WxClient")) {var httpResponse = wxClient.GetAsync(requestBuild).Result;vardynamic = JsonConvert.DeserializeObject<GetUserInfoResult>( httpResponse.Content.ReadAsStringAsync().Result );if (dynamic.errcode ==0)return Tuple.Create(dynamic.UserID,true);elsereturn Tuple.Create($"获取用户ID失败,请重新打开,原因:{dynamic.errmsg}",false); } }
获取用户通讯录方法,该方法可以通过token和userid进行获取用户头像等信息,按需要调用
读取成员接口文档:https://developer.work.weixin.qq.com/document/path/90196
///<summary>/// 获取用户通讯录///</summary>///<returns>Item1 头像,获取失败时为错误信息;Item2 名称;Item3 是否成功</returns>publicstatic Tuple<string,string,bool> GetUserByID(string token,string userid) {//构造请求链接var requestBuild = GlobalContext.Configuration["Wx:getUserByUserId"]; requestBuild=string.Format(requestBuild, token, userid);//建立HttpClientusing (var wxClient = GlobalContext.httpClientFactory.CreateClient("WxClient")) {var httpResponse = wxClient.GetAsync(requestBuild).Result;vardynamic = JsonConvert.DeserializeObject<GetUserResult>( httpResponse.Content.ReadAsStringAsync().Result );if (dynamic.errcode ==0)return Tuple.Create(dynamic.avatar,dynamic.name,true);elsereturn Tuple.Create($"获取用户通讯录失败,请重新打开,原因:{dynamic.errmsg}","",false); } }
4.5调用
本方法是为了企业微信登录时绕过用户登录直接使用企业微信用户登录,有其他需求根据需要调整。
主页index 中使用code参数获取回调传进来的code,调用GetToken方法获取Token,然后根据Token和Code获取UserID,最后根据UserID和Token获取通讯录的头像和名称。需要注意的是我们要对每个用户最新的code进行缓存,在企业微信内部浏览器时刷新code参数不会变动,但是code只能使用一次会导致接口调用失败。
[HttpGet]publicasync Task<IActionResult> Index(string code) { OperatorInfo operatorInfo=default; TData<List<MenuEntity>> objMenu =await menuBLL.GetList(null); List<MenuEntity> menuList = objMenu.Data; menuList= menuList.Where(p => p.MenuStatus == StatusEnum.Yes.ParseToInt()).ToList();if (code !=null)//企业微信登录 {//获取联系人 从内存中取||从接口取string username, portrait =default;bool issuccess2 =default;//缓存最近的一次code 用于刷新URL时重复code请求失败var codeCache = ApplicationContext.UserCache.FirstOrDefault(o => o.Code == code);if(codeCache ==null) {//获取token Token时间为过期时间减5分钟var (token, issuccess) = GetToken();if (!issuccess)return RedirectToAction("Error",new { message= token });//获取useridvar (userid, issuccess1) = GetUserID(token, code);if (!issuccess1)return RedirectToAction("Error",new { message= userid });var useridCache = ApplicationContext.UserCache.FirstOrDefault(o => o.UserID == userid);if (useridCache ==null)//不存在缓存中 { (portrait, username, issuccess2)= GetUserByID(token, userid);if (!issuccess2)return RedirectToAction("Error",new { message= portrait });//加缓存 ApplicationContext.UserCache.Add(new UserCache() { Code= code, Username= username, Portrait= portrait, UserID= userid });//保存登录日志var log = logLoginBLL.SaveForm(new LogLoginEntity { Remark= username, ExtraRemark= token +":" + userid }); }else//从缓存中获取用户信息 { username= useridCache.Username; portrait= useridCache.Portrait;//更新最新code useridCache.Code = code; } }else { username= codeCache.Username; portrait= codeCache.Portrait; }//模拟登录 TData<UserEntity> userObj =await userBLL.CheckLogin(ApplicationContext.WxUser , ApplicationContext.WxPassWord , (int)PlatformEnum.Web);if (userObj.Tag ==1) {awaitnew UserBLL().UpdateUser(userObj.Data);await Operator.Instance.AddCurrent(userObj.Data.WebToken);var op =await Operator.Instance.Current(); AuthorizeListWhere(op); }//构建前端返回的用户名 以及头像 operatorInfo =new OperatorInfo(); operatorInfo.RealName= username; operatorInfo.UserName= username; operatorInfo.Portrait= portrait; }else//正常网页登录 { operatorInfo=await Operator.Instance.Current();if (operatorInfo ==null)return RedirectToAction("Login");if (operatorInfo.IsSystem !=1) { AuthorizeListWhere(operatorInfo); } }//授权筛选void AuthorizeListWhere(OperatorInfo info) { TData<List<MenuAuthorizeInfo>> objMenuAuthorize = menuAuthorizeBLL.GetAuthorizeList(info).Result; List<long?> authorizeMenuIdList = objMenuAuthorize.Data.Select(p => p.MenuId).ToList(); menuList= menuList.Where(p => authorizeMenuIdList.Contains(p.Id)).ToList(); }new CookieHelper().WriteCookie("UserName", operatorInfo.UserName,false);new CookieHelper().WriteCookie("RealName", operatorInfo.RealName,false); ViewBag.OperatorInfo= operatorInfo; ViewBag.MenuList= menuList;return View(); }
Index.Html调整