.NET Core企業微信網頁授權登入

微風吹過~發表於2022-04-21

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 Tuple<string,bool> GetToken()
        {
            //判斷Token是否存在 以及Token是否在有效期內
            if(string.IsNullOrEmpty(ApplicationContext.Token) || ApplicationContext.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;
                    if(httpResponse.StatusCode ==  System.Net.HttpStatusCode.OK)
                    {
                        var  dynamic= JsonConvert.DeserializeObject<GetTokenResult>(
                                          httpResponse.Content.ReadAsStringAsync().Result
                                          );

                        ApplicationContext.Token = dynamic.access_token;
                        //過期5分鐘前重新整理Token
                        var expires_in = Convert.ToDouble(dynamic.expires_in - 5 * 60);
                        ApplicationContext.TimeOutDate = DateTime.Now.AddSeconds(expires_in);
                        return Tuple.Create(ApplicationContext.Token,true);
                    }
                    else
                    {
                        return Tuple.Create("獲取企業微信Token失敗,請稍後重試!", false);
                    }
                }
            }
            else
            {
                return Tuple.Create(ApplicationContext.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 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;
                    if (httpResponse.StatusCode == System.Net.HttpStatusCode.OK)
                    {
                        var dynamic = JsonConvert.DeserializeObject<GetUserInfoResult>(
                                          httpResponse.Content.ReadAsStringAsync().Result
                                          );

                        return Tuple.Create(dynamic.UserID, true);
                    }
                    else
                    {
                        return Tuple.Create("獲取使用者ID失敗,請稍後重試!", false);
                    }
             }
            
        }

獲取使用者通訊錄方法,該方法可以通過token和userid進行獲取使用者頭像等資訊,按需要呼叫

讀取成員介面文件:https://developer.work.weixin.qq.com/document/path/90196

   /// <summary>
        /// 獲取使用者通訊錄
        /// </summary>
        /// <returns>Item1 頭像,獲取失敗時為錯誤資訊;Item2  名稱;Item3 是否成功</returns>
        public 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;
                if (httpResponse.StatusCode == System.Net.HttpStatusCode.OK)
                {
                    var dynamic = JsonConvert.DeserializeObject<GetUserResult>(
                                      httpResponse.Content.ReadAsStringAsync().Result
                                      );
                    return Tuple.Create(dynamic.avatar, dynamic.name, true);
                }
                else
                {
                    return Tuple.Create("獲取使用者ID失敗,請稍後重試!","", 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("error1", new { errormessage = token });
                    //獲取userid
                    var (userid, issuccess1) = GetUserID(token, code);
                    if (!issuccess1) return RedirectToAction("error1", new { errormessage = userid });

                    var useridCache = ApplicationContext.UserCache.FirstOrDefault(o => o.UserID == userid);
                    if (useridCache == null)//不存在快取中
                    {
                        (portrait, username, issuccess2) = GetUserByID(token, userid);
                        if (!issuccess2) return RedirectToAction("error1", new { errormessage = 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.截圖

 

相關文章