net core WebApi——April.Util更新之許可權

来源:https://www.cnblogs.com/AprilBlank/archive/2019/11/10/11832324.html
-Advertisement-
Play Games

[toc] 前言 在之前已經提到過,公用類庫Util已經開源,目的一是為了簡化開發的工作量,畢竟有些常規的功能類庫重覆率還是挺高的,二是為了一起探討學習軟體開發,用的人越多問題也就會越多,解決的問題越多功能也就越完善, 倉庫地址: "April.Util_github" , "April.Util_ ...


目錄

前言

在之前已經提到過,公用類庫Util已經開源,目的一是為了簡化開發的工作量,畢竟有些常規的功能類庫重覆率還是挺高的,二是為了一起探討學習軟體開發,用的人越多問題也就會越多,解決的問題越多功能也就越完善,倉庫地址: April.Util_githubApril.Util_gitee,還沒關註的朋友希望可以先mark,後續會持續維護。

許可權

在之前的net core WebApi——公用庫April.Util公開及發佈中已經介紹了初次發佈的一些功能,其中包括緩存,日誌,加密,統一的配置等等,具體可以再回頭看下這篇介紹,而在其中有個TokenUtil,因為當時發佈的時候這塊兒還沒有更新上,趁著周末來整理下吧。

關於webapi的許可權,可以藉助Identity,Jwt,但是我這裡沒有藉助這些,只是自己做了個token的生成已經存儲用戶主要信息,對於許可權我想大多數人已經有了一套自己的許可權體系,所以這裡我簡單介紹下我的思路。

  1. 首先對於菜單做許可權標示,請求的控制器,請求的事件
  2. 菜單信息維護後,設置角色對應多個菜單
  3. 管理員對應多個角色
  4. 在登錄的時候根據賬號信息獲取對應管理員的角色及最終菜單,控制器,事件
  5. 處理管理員信息後自定義token,可設置token過期時間,token可以反解析(如果到期自動重新授權,我這裡沒有處理)
  6. 每次訪問介面的時候(除公開不需校驗的介面),根據請求的路徑判斷是否有當前控制器許可權(通過中間層),進入介面後判斷是否有對應許可權(通過標簽)

通過上述流程來做許可權的校驗,當然這裡只是針對單應用,如果是多應用的話,這裡還要考慮應用問題(如,一個授權認證工程主做身份校驗,多個應用工程通用一個管理)。

首先,我們需要一個可以存儲管理員的對應屬性集合AdminEntity,主要存儲基本信息,控制器集合,許可權集合,數據集合(也就是企業部門等)。

    /// <summary>
    /// 管理員實體
    /// </summary>
    public class AdminEntity
    {
        private int _ID = -1;
        private string _UserName = string.Empty;
        private string _Avator = string.Empty;
        private List<string> _Controllers = new List<string>();
        private List<string> _Permissions = new List<string>();
        private int _TokenType = 0;
        private bool _IsSuperManager = false;
        private List<int> _Depts = new List<int>();
        private int _CurrentDept = -1;
        private DateTime _ExpireTime = DateTime.Now;

        /// <summary>
        /// 主鍵
        /// </summary>
        public int ID { get => _ID; set => _ID = value; }
        /// <summary>
        /// 用戶名
        /// </summary>
        public string UserName { get => _UserName; set => _UserName = value; }
        /// <summary>
        /// 頭像
        /// </summary>
        public string Avator { get => _Avator; set => _Avator = value; }
        /// <summary>
        /// 控制器集合
        /// </summary>
        public List<string> Controllers { get => _Controllers; set => _Controllers = value; }
        /// <summary>
        /// 許可權集合
        /// </summary>
        public List<string> Permissions { get => _Permissions; set => _Permissions = value; }
        /// <summary>
        /// 訪問方式
        /// </summary>
        public int TokenType { get => _TokenType; set => _TokenType = value; }
        /// <summary>
        /// 是否為超管
        /// </summary>
        public bool IsSuperManager { get => _IsSuperManager; set => _IsSuperManager = value; }
        /// <summary>
        /// 企業集合
        /// </summary>
        public List<int> Depts { get => _Depts; set => _Depts = value; }
        /// <summary>
        /// 當前企業
        /// </summary>
        public int CurrentDept { get => _CurrentDept; set => _CurrentDept = value; }
        /// <summary>
        /// 過期時間
        /// </summary>
        public DateTime ExpireTime { get => _ExpireTime; set => _ExpireTime = value; }
    }

之後我們來完成TokenUtil這塊兒,首先是生成我們的token串,因為考慮到需要反解析,所以這裡採用的是字元串加解密,當然這個加密串具體是什麼可以自定義,目前我這裡設置的是固定需要兩個參數{id},{ts},目的是為了保證加密串的唯一,當然也是為了過期無感知重新授權準備的。

    public class TokenUtil
    {
        /// <summary>
        /// 設置token
        /// </summary>
        /// <returns></returns>
        public static string GetToken(AdminEntity user, out string expiretimstamp)
        {
            string id = user.ID.ToString();
            double exp = 0;
            switch ((AprilEnums.TokenType)user.TokenType)
            {
                case AprilEnums.TokenType.Web:
                    exp = AprilConfig.WebExpire;
                    break;
                case AprilEnums.TokenType.App:
                    exp = AprilConfig.AppExpire;
                    break;
                case AprilEnums.TokenType.MiniProgram:
                    exp = AprilConfig.MiniProgramExpire;
                    break;
                case AprilEnums.TokenType.Other:
                    exp = AprilConfig.OtherExpire;
                    break;
            }
            DateTime date = DateTime.Now.AddHours(exp);
            user.ExpireTime = date;
            double timestamp = DateUtil.ConvertToUnixTimestamp(date);
            expiretimstamp = timestamp.ToString();
            string token = AprilConfig.TokenSecretFormat.Replace("{id}", id).Replace("{ts}", expiretimstamp);
            token = EncryptUtil.EncryptDES(token, EncryptUtil.SecurityKey);
            //LogUtil.Debug($"用戶{id}獲取token:{token}");
            Add(token, user);
            //處理多點登錄
            SetUserToken(token, user.ID);
            return token;
        }

        /// <summary>
        /// 通過token獲取當前人員信息
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        public static AdminEntity GetUserByToken(string token = "")
        {
            if (string.IsNullOrEmpty(token))
            {
                token = GetTokenByContent();
            }
            if (!string.IsNullOrEmpty(token))
            {
                
                AdminEntity admin = Get(token);
                if (admin != null)
                {
                    //校驗時間
                    if (admin.ExpireTime > DateTime.Now)
                    {
                        if (AprilConfig.AllowSliding)
                        {
                            //延長時間
                            admin.ExpireTime = DateTime.Now.AddMinutes(30);
                            //更新
                            Add(token, admin);
                        }
                        return admin;
                    }
                    else
                    {
                        //已經過期的就不再延長了,當然後續根據情況改進吧
                        return null;
                    }
                }
            }
            return null;
        }
        /// <summary>
        /// 通過用戶請求信息獲取Token信息
        /// </summary>
        /// <returns></returns>
        public static string GetTokenByContent()
        {
            string token = "";
            //判斷header
            var headers = AprilConfig.HttpCurrent.Request.Headers;
            if (headers.ContainsKey("token"))
            {
                token = headers["token"].ToString();
            }
            if (string.IsNullOrEmpty(token))
            {
                token = CookieUtil.GetString("token");
            }
            if (string.IsNullOrEmpty(token))
            {
                AprilConfig.HttpCurrent.Request.Query.TryGetValue("token", out StringValues temptoken);
                if (temptoken != StringValues.Empty)
                {
                    token = temptoken.ToString();
                }
            }
            return token;
        }
        /// <summary>
        /// 移除Token
        /// </summary>
        /// <param name="token"></param>
        public static void RemoveToken(string token = "")
        {
            if (string.IsNullOrEmpty(token))
            {
                token = GetTokenByContent();
            }
            if (!string.IsNullOrEmpty(token))
            {
                Remove(token);
            }
        }

        #region 多個登錄
        /// <summary>
        /// 多個登錄設置緩存
        /// </summary>
        /// <param name="token"></param>
        /// <param name="userid"></param>
        public static void SetUserToken(string token, int userid)
        {
            Dictionary<int, List<string>> dicusers = CacheUtil.Get<Dictionary<int, List<string>>>("UserToken");
            if (dicusers == null)
            {
                dicusers = new Dictionary<int, List<string>>();
            }
            List<string> listtokens = new List<string>();
            if (dicusers.ContainsKey(userid))
            {
                listtokens = dicusers[userid];
                if (listtokens.Count <= 0)
                {
                    listtokens.Add(token);
                }
                else
                {
                    if (!AprilConfig.AllowMuiltiLogin)
                    {
                        foreach (var item in listtokens)
                        {
                            RemoveToken(item);
                        }
                        listtokens.Add(token);
                    }
                    else
                    {
                        bool isAdd = true;
                        foreach (var item in listtokens)
                        {
                            if (item == token)
                            {
                                isAdd = false;
                            }
                        }
                        if (isAdd)
                        {
                            listtokens.Add(token);
                        }
                    }
                }
            }
            else
            {

                listtokens.Add(token);
                dicusers.Add(userid, listtokens);
            }
            CacheUtil.Add("UserToken", dicusers, new TimeSpan(6, 0, 0), true);
        }
        /// <summary>
        /// 多個登錄刪除緩存
        /// </summary>
        /// <param name="userid"></param>
        public static void RemoveUserToken(int userid)
        {
            Dictionary<int, List<string>> dicusers = CacheUtil.Get<Dictionary<int, List<string>>>("UserToken");
            if (dicusers != null && dicusers.Count > 0)
            {
                if (dicusers.ContainsKey(userid))
                {
                    //刪除所有token
                    var listtokens = dicusers[userid];
                    foreach (var token in listtokens)
                    {
                        RemoveToken(token);
                    }
                    dicusers.Remove(userid);
                }
            }
        }
        /// <summary>
        /// 多個登錄獲取
        /// </summary>
        /// <param name="userid"></param>
        /// <returns></returns>
        public static List<string> GetUserToken(int userid)
        {
            Dictionary<int, List<string>> dicusers = CacheUtil.Get<Dictionary<int, List<string>>>("UserToken");
            List<string> lists = new List<string>();
            if (dicusers != null && dicusers.Count > 0)
            {
                foreach (var item in dicusers)
                {
                    if (item.Key == userid)
                    {
                        lists = dicusers[userid];
                        break;
                    }
                }
            }
            return lists;
        }
        #endregion

        #region 私有方法(這塊兒還需要改進)

        private static void Add(string token,AdminEntity admin)
        {
            switch (AprilConfig.TokenCacheType)
            {
                //不推薦Cookie
                case AprilEnums.TokenCacheType.Cookie:
                    CookieUtil.Add(token, admin);
                    break;
                case AprilEnums.TokenCacheType.Cache:
                    CacheUtil.Add(token, admin, new TimeSpan(0, 30, 0));
                    break;
                case AprilEnums.TokenCacheType.Session:
                    SessionUtil.Add(token, admin);
                    break;
                case AprilEnums.TokenCacheType.Redis:
                    RedisUtil.Add(token, admin);
                    break;
            }
        }

        private static AdminEntity Get(string token)
        {
            AdminEntity admin = null;
            switch (AprilConfig.TokenCacheType)
            {
                case AprilEnums.TokenCacheType.Cookie:
                    admin = CookieUtil.Get<AdminEntity>(token);
                    break;
                case AprilEnums.TokenCacheType.Cache:
                    admin = CacheUtil.Get<AdminEntity>(token);
                    break;
                case AprilEnums.TokenCacheType.Session:
                    admin = SessionUtil.Get<AdminEntity>(token);
                    break;
                case AprilEnums.TokenCacheType.Redis:
                    admin = RedisUtil.Get<AdminEntity>(token);
                    break;
            }
            return admin;
        }

        private static void Remove(string token)
        {
            switch (AprilConfig.TokenCacheType)
            {
                case AprilEnums.TokenCacheType.Cookie:
                    CookieUtil.Remove(token);
                    break;
                case AprilEnums.TokenCacheType.Cache:
                    CacheUtil.Remove(token);
                    break;
                case AprilEnums.TokenCacheType.Session:
                    SessionUtil.Remove(token);
                    break;
                case AprilEnums.TokenCacheType.Redis:
                    RedisUtil.Remove(token);
                    break;
            }
        }
        #endregion
    }

中間層

當然這也在之前已經提到過net core Webapi基礎工程搭建(七)——小試AOP及常規測試_Part 1,當時還覺得這個叫做攔截器,too young too simple,至於使用方法這裡就不多說了,可以參考之前2.2版本的東西,也可以看代碼倉庫中的示例工程。

    public class AprilAuthorizationMiddleware
    {
        private readonly RequestDelegate next;

        public AprilAuthorizationMiddleware(RequestDelegate next)
        {
            this.next = next;
        }

        public Task Invoke(HttpContext context)
        {
            if (context.Request.Method != "OPTIONS")
            {
                string path = context.Request.Path.Value;
                if (!AprilConfig.AllowUrl.Contains(path))
                {
                    //獲取管理員信息
                    AdminEntity admin = TokenUtil.GetUserByToken();
                    if (admin == null)
                    {
                        //重新登錄
                        return ResponseUtil.HandleResponse(-2, "未登錄");
                    }
                    if (!admin.IsSuperManager)
                    {
                        //格式統一為/api/Controller/Action,相容多級如/api/Controller1/ConrolerInnerName/xxx/Action
                        string[] strValues = System.Text.RegularExpressions.Regex.Split(path, "/");

                        string controller = "";
                        bool isStartApi = false;
                        if (path.StartsWith("/api"))
                        {
                            isStartApi = true;
                        }
                        for (int i = 0; i < strValues.Length; i++)
                        {
                            //為空,為api,或者最後一個
                            if (string.IsNullOrEmpty(strValues[i]) || i == strValues.Length - 1)
                            {
                                continue;
                            }
                            if (isStartApi && strValues[i] == "api")
                            {
                                continue;
                            }
                            if (!string.IsNullOrEmpty(controller))
                            {
                                controller += "/";
                            }
                            controller += strValues[i];
                        }
                        if (string.IsNullOrEmpty(controller))
                        {
                            controller = strValues[strValues.Length - 1];
                        }
                        if (!admin.Controllers.Contains(controller.ToLower()))
                        {
                            //無權訪問
                            return ResponseUtil.HandleResponse(401, "無權訪問");
                        }

                    }
                }
            }
            return next.Invoke(context);
        }
    }

Ok,我們先來看下Login中的操作以及實現效果吧。

        [HttpPost]
        public async Task<ResponseDataEntity> Login(LoginFormEntity formEntity)
        {
            if (string.IsNullOrEmpty(formEntity.LoginName) || string.IsNullOrEmpty(formEntity.Password))
            {
                return ResponseUtil.Fail("請輸入賬號密碼");
            }
            if (formEntity.LoginName == "admin")
            {
                //這裡實際應該通過db獲取管理員
                string password = EncryptUtil.MD5Encrypt(formEntity.Password, AprilConfig.SecurityKey);
                if (password == "B092956160CB0018")
                {
                    //獲取管理員相關許可權,同樣是db獲取,這裡只做展示
                    AdminEntity admin = new AdminEntity
                    {
                        UserName = "超級管理員",
                        Avator = "",
                        IsSuperManager = true,
                        TokenType = (int)AprilEnums.TokenType.Web
                    };
                    string token = TokenUtil.GetToken(admin, out string expiretimestamp);
                    int expiretime = 0;
                    int.TryParse(expiretimestamp, out expiretime);
                    //可以考慮記錄登錄日誌等其他信息
                    return ResponseUtil.Success("", new { username = admin.UserName, avator = admin.Avator, token = token, expire = expiretime });
                }
            }
            else if (formEntity.LoginName == "test")
            {
                //這裡做許可權演示
                AdminEntity admin = new AdminEntity
                {
                    UserName = "測試",
                    Avator = "",
                    TokenType = (int)AprilEnums.TokenType.Web
                };
                admin.Controllers.Add("weatherforecast");
                admin.Permissions.Add("weatherforecast_log");//控制器_事件(Add,Update...)
                string token = TokenUtil.GetToken(admin, out string expiretimestamp);
                int expiretime = 0;
                int.TryParse(expiretimestamp, out expiretime);
                //可以考慮記錄登錄日誌等其他信息
                return ResponseUtil.Success("", new { username = admin.UserName, avator = admin.Avator, token = token, expire = expiretime });
            }
            //這裡其實已經可以考慮驗證碼相關了,但是這是示例工程,後續可持續關註我,會有基礎工程(帶許可權)的實例公開
            return ResponseUtil.Fail("賬號密碼錯誤");
        }

可能乍一看會先吐槽下,明明是非同步介面還用同步的方法,沒有非同步的實現空浪費記憶體xxx,因為db考慮是要搞非同步,所以這裡示例就這樣先寫了,主要是領會精神,咳咳。

來試下效果吧,首先我們隨便訪問個白名單外的介面。

測試

然後我們通過賬號登陸Login介面(直接寫死了,admin,123456),獲取到token。
登錄

然後我們來訪問介面。
測試

是不是還是未登錄,沒錯,因為沒有token的傳值,當然我這裡是通過query傳值,支持header,token,query。

測試

這裡因為是超管,所以許可權隨意搞,無所謂,接下來展示下普通用戶的許可權標示。

目前可以通過標簽AprilPermission,把當前的控制器與對應事件的許可權作為參數傳遞,之後根據當前管理員信息做校驗。

    public class AprilPermissionAttribute : Attribute, IActionFilter
    {

        public string Permission;
        public string Controller;
        /// <summary>
        /// 構造函數
        /// </summary>
        /// <param name="_controller">控制器</param>
        /// <param name="_permission">介面事件</param>
        public AprilPermissionAttribute(string _controller, string _permission)
        {
            Permission = _permission;
            Controller = _controller;
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            LogUtil.Debug("AprilPermission OnActionExecuted");
        }
        public void OnActionExecuting(ActionExecutingContext context)
        {
            AdminEntity admin = TokenUtil.GetUserByToken();
            if (admin == null || admin.ExpireTime <= DateTime.Now)
            {
                context.Result = new ObjectResult(new { msg = "未登錄", code = -2 });
            }
            if (!admin.IsSuperManager)
            {
                string controller_permission = $"{Controller}_{Permission}";
                if (!admin.Controllers.Contains(Controller) || !admin.Permissions.Contains(controller_permission))
                {
                    context.Result = new ObjectResult(new { msg = "無權訪問", code = 401 });
                }
            }
        }
    }

針對幾個介面做了調整,附上標簽後判斷許可權,我們來測試下登錄test,密碼隨意。

測試
測試
測試
測試

至此許可權相關的功能也統一起來,當然如果有個性化的還是需要調整的,後續也是會不斷的更新改動。

小結

許可權還是稍微麻煩點兒啊,通過中間層,標簽以及TokenUtil來完成登錄授權這塊兒,至於數據的劃分,畢竟這個東西不是通用的,所以只是點出來而沒有去整合,如果有好的建議或者自己整合的通用類庫也可以跟我交流。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Eclipse部署多模塊項目到tomcat,啟動時找不到jar的解決方法。 ...
  • 在使用redis時,一般會設置一個過期時間,當然也有不設置過期時間的,也就是永久不過期。當設置了過期時間,redis是如何判斷是否過期,以及根據什麼策略來進行刪除的。 設置過期時間 expire key time(以秒為單位) 這是最常用的方式setex(String key, int second ...
  • scp是secure copy的簡寫,用於在Linux下進行遠程拷貝文件的命令,和它類似的命令有cp,不過cp只是在本機進行拷貝不能跨伺服器,而且scp傳輸是加密的。可能會稍微影響一下速度。當你伺服器硬碟變為只讀 read only system時,用scp可以幫你把文件移出來。另外,scp還非常不 ...
  • 你可能想創建一個在應用的任何地方都可以訪問的函數,這個教程將幫你實現 👏 很多教程都會說,你在 composer.json 這個文件中通過添加一個自動載入的文件,就可以實現這個需求。但我認為這不是一個好的方式,當你在 helpers.php 文件中添加了更多的函數時,可讀性將變得很差。 下麵我將介 ...
  • 在說正題之前先解釋一下交換機模式是個籠統的稱呼,它不是一個單獨的模式(包括了訂閱模式,路由模式和主題模式),交換機模式是一個比較常用的模式,主要是為了實現數據的同步。 首先,說一下訂閱模式,就和字面上的意思差不多主要就是一個生產者,多個消費者,同一個消息被多個消費者獲取,先看一下官網的圖示 整體執行 ...
  • 當創建隊列jobs、監聽器或訂閱伺服器以推送到隊列中時,您可能會開始認為,一旦分派,隊列工作器決定如何處理您的邏輯就完全由您自己決定了。 嗯……並不是說你不能從作業內部與隊列工作器交互,但是通常情況下,哪怕你做了,也是沒必要的。 這個神奇的騷操作的出現是因為“InteractsWithQueue”這 ...
  • 環境:MacOS 10.13 MAMAP Prophp 7.0.33 + xdebugVisual Studio Code前言我所理解的 POP Chain:利用魔術方法並巧妙構造特殊屬性調用一系列函數或類方法以執行某種敏感操作的調用堆棧反序列化常用魔法函數前言我所理解的 POP Chain:利用魔 ...
  • 【ASP.NET Core學習】在ASP.NET Core 種使用Entity Framework Core介紹,包括如何添加Entity Framwork Core,創建模型和遷移到資料庫,查詢數據,保存數據,使用事務,處理併發衝突 ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...