.NET Core 企业微信网页授权登录

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调用微信客户端

public static IHttpClientFactory  httpClientFactory { get; set; }

 

 

Startup添加以下内容

 

 public void ConfigureServices(IServiceCollection services)
 {
       services.AddHttpClient("WxClient", config => 
            {
                config.BaseAddress = new Uri(Configuration["Wx:baseurl"]);
                config.DefaultRequestHeaders.Add("Accept", "application/json");
            });
}

public void 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
{
    public class UserCache
    {
        /// <summary>
        ///  用户id
        /// </summary>
        public string UserID { get; set; }

        /// <summary>
        ///  头像
        /// </summary>
        public string Portrait { get; set; }

        /// <summary>
        ///  用户名
        /// </summary>
        public string Username { get; set; }

        /// <summary>
        ///  缓存最近一次Code 用于刷新时code不更新问题
        /// </summary>
        public string 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 { public static class ApplicationContext { /// <summary> /// 用于多点登录的微信用户 /// </summary> public const string WxUser = "taskUser"; /// <summary> /// 用于多点登录的微信密码 /// </summary> public const string WxPassWord = "123456"; /// <summary> /// 过期时间 /// </summary> public static DateTime TimeOutDate { get; set; } /// <summary> /// Token /// </summary> public static string Token { get; set; } /// <summary> /// 缓存UserID Name 头像 /// </summary> public static List<UserCache> UserCache { get; set; } = new List<UserCache>(); } }

获取Token返回实体

using System; using System.Collections.Generic; using System.Text; namespace YiSha.Entity.OAManage { public class GetTokenResult { /// <summary>
        /// 错误编号 /// </summary>
        public int errcode { get; set; } /// <summary>
        /// 错误信息 /// </summary>
        public string errmsg { get; set; } /// <summary>
        /// Token /// </summary>
        public string access_token { get; set; } /// <summary>
        /// 过期时间 /// </summary>
        public int expires_in { get; set; } } }

获取用户id返回实体

using System; using System.Collections.Generic; using System.Text; namespace YiSha.Entity.OAManage { //获取用户ID
    public class GetUserInfoResult { /// <summary>
        /// 错误编号 /// </summary>
        public int errcode { get; set; } /// <summary>
        /// 错误信息 /// </summary>
        public string errmsg { get; set; } /// <summary>
        /// 用户ID /// </summary>
        public string UserID { get; set; } } }

获取用户通讯录返回实体

using System;
using System.Collections.Generic;
using System.Text;

namespace YiSha.Entity.OAManage
{
    public class GetUserResult
    {
        /// <summary>
        /// 错误编号
        /// </summary>
        public int errcode { get; set; }

        /// <summary>
        /// 错误信息
        /// </summary>
        public string errmsg { get; set; }

        /// <summary>
        /// 名称
        /// </summary>
        public string name { get; set; }

        /// <summary>
        /// 头像
        /// </summary>
        public string 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>
        public static 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;
                    var dynamic = JsonConvert.DeserializeObject<GetTokenResult>(
                                          httpResponse.Content.ReadAsStringAsync().Result
                                          );

                    if (dynamic.errcode == 0)
                    {
                        Token = dynamic.access_token;
                        //过期5分钟前刷新Token
                        var 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>
        public static 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;

                var dynamic = JsonConvert.DeserializeObject<GetUserInfoResult>(
                                      httpResponse.Content.ReadAsStringAsync().Result
                                      );
                if (dynamic.errcode == 0)
                    return Tuple.Create(dynamic.UserID, true);
                else
                    return Tuple.Create($"获取用户ID失败,请重新打开,原因:{dynamic.errmsg}", false);
            }

        }

 

获取用户通讯录方法,该方法可以通过token和userid进行获取用户头像等信息,按需要调用

读取成员接口文档:https://developer.work.weixin.qq.com/document/path/90196

 /// <summary>
        /// 获取用户通讯录
        /// </summary>
        /// <returns>Item1 头像,获取失败时为错误信息;Item2  名称;Item3 是否成功</returns>
        public static Tuple<string, string, bool> GetUserByID(string token, string userid)
        {
            //构造请求链接
            var requestBuild = GlobalContext.Configuration["Wx:getUserByUserId"];
            requestBuild = string.Format(requestBuild, token, userid);
            //建立HttpClient
            using (var wxClient = GlobalContext.httpClientFactory.CreateClient("WxClient"))
            {
                var httpResponse = wxClient.GetAsync(requestBuild).Result;
                var dynamic = JsonConvert.DeserializeObject<GetUserResult>(
                                     httpResponse.Content.ReadAsStringAsync().Result
                                     );
                if (dynamic.errcode == 0)
                    return Tuple.Create(dynamic.avatar, dynamic.name, true);
                else
                    return Tuple.Create($"获取用户通讯录失败,请重新打开,原因:{dynamic.errmsg}", "", false);
            }
        }

 

4.5调用

  本方法是为了企业微信登录时绕过用户登录直接使用企业微信用户登录,有其他需求根据需要调整。

  主页index 中使用code参数获取回调传进来的code,调用GetToken方法获取Token,然后根据Token和Code获取UserID,最后根据UserID和Token获取通讯录的头像和名称。需要注意的是我们要对每个用户最新的code进行缓存,在企业微信内部浏览器时刷新code参数不会变动,但是code只能使用一次会导致接口调用失败。

 [HttpGet]
        public async 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 });
                    //获取userid
                    var (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)
                {
                    await new 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调整

 

 5.截图

 

页面下部广告