.NET Core修改傳入的參數


在分布式系統的交互中,一般有兩種方式

一、服務頒發給消費方Token令牌,消費方在請求的時候攜帶上令牌

二、加密參數處理,服務方與消費方協調加密解密處理方式

二者各有各的用途和場景,這里不必多言。今天要說的是對第二種交互方式的一點小擴展。

❓問題

一般我們是雙方協定好加密解密方式,在消費方加密參數模型,發起請求,服務方在接收到后,進行解密操作,拿到真實的參數模型后,再進行邏輯處理。

嗯,流程是這樣的,沒啥問題。但是解密參數這一過程卻有點小麻煩,為何?因為需要解密,我們定義的接收參數模型已經變更了,就像這樣。

加密參數模型:

其中的Parameter經過解密后,它也許是一個Order模型、又或者是一個Product模型,不管怎么說,解密后的參數模型才是我們所真正需要的。也是因為如此,我們也無法使用框架為我們帶來的模型驗證(盡管可以通過其它方式來實現),並且我們需要在每個Action內部重復編寫幾乎相同的解密邏輯代碼。

真實參數模型:

更糟糕的是,它使我們的API代碼變得晦澀難懂,因為它們幾乎都是一樣的

怎么辦?有沒有一種方法既可以達到解密的目的,又可以讓我們的API還原本來的樣貌呢?答案是有的。

❗解決

目的:1,自動完成解密。2,使用模型驗證。3,解耦API,做它本身自己的事。

那么說到這兒就已經比較明顯了,首先,需要剝離解密邏輯,解耦API方法,還原它本來的樣子,其次需要繼續使用框架的模型驗證,那么ActionFilter無法使用,因為此時框架已經完成了對模型的校驗。因此我們可以初步得到這樣一條結論,我們需要在API收到請求后,還未進入API的ActionFilter之前,完成解密的過程,並修改請求傳入的參數。

方案一,自定義中間件

中間件是可行的,它在請求中執行的地方符合我們的要求。

第一步,自定義Attribute,標識解密方法

可以加一些自己的內容,比如圖中所示的簽名加密方式、參數解密方式什么的,用來給API方法自定義配置解密方式。最主要的還是起到一個標識的作用,在需要執行解密過程的方法上打上此Attribute即可。

此時,我們的API已經和原本的樣子並無差異,如果此方法不需要解密,去掉AutoDecryptModel標識即可。

第二步,自定義中間件,完成解密過程。

public async Task Invoke(HttpContext context)
        {
            HttpRequest request = context.Request;
            if (request.Method.Equals("POST"))
            {
                //從應用緩存獲取需要自動解密的請求路徑
                var autoDecryptCacheConfigs =
                    _cache.Get<List<AutoDecryptCacheConfig>>(MemoryCacheConstants.AutoDecryptMethodTemplates);
                if (request.Path.HasValue)
                {
                    var path = request.Path.Value ?? "";
                    var decryptConfig = autoDecryptCacheConfigs.FirstOrDefault(o => path.EndsWith(o.RouteTemplate));
                    //如果當前請求匹配需要自動解密
                    if (decryptConfig is { Mandatory: true })
                    {
                        //讀取body
                        request.Body.Position = 0;
                        using StreamReader sr = new StreamReader(request.Body);
                        string requestBody = await sr.ReadToEndAsync();
                        request.Body.Position = 0;

                        _logger.LogInformation(
                            $"當前路徑:{path},請求參數:{requestBody},解密配置:{{ {GetDecryptConfigDescription(decryptConfig)} }}");
                        //解析參數格式是否符合自動解密 不符合則直接傳遞
                        var encryptParameter = (JsonConvert.DeserializeObject<EncryptParameterRequest>(requestBody)) ??
                                               new EncryptParameterRequest();

                        if (!string.IsNullOrWhiteSpace(encryptParameter.Sign) &&
                            !string.IsNullOrWhiteSpace(encryptParameter.Parameter))
                        {
                            //驗證簽名
                            if (VerifySign(encryptParameter, decryptConfig.SecuritySignCryptType, decryptConfig.Key))
                            {
                                //簽名對比通過 開始解密
                                var decryptParameter = DecryptParameter(encryptParameter.Parameter,
                                    decryptConfig.SecurityParameterDecryptType, decryptConfig.Key);
                                if (!string.IsNullOrWhiteSpace(decryptParameter))
                                {
                                    _logger.LogInformation($"當前路徑:{path},請求參數:{requestBody},解密結果:{decryptParameter}");
                                    //寫入解密參數
                                    MemoryStream stream = new MemoryStream();
                                    StreamWriter writer = new StreamWriter(stream);
                                    await writer.WriteAsync(decryptParameter);
                                    await writer.FlushAsync();
                                    //寫入
                                    request.Body = stream;
                                    //重置指針
                                    request.Body.Position = 0;
                                }
                            }
                            else
                            {
                                throw new Exception("422-簽名對比失敗,請求不通過");
                            }
                        }
                        else
                        {
                            throw new Exception("422-簽名或加密參數不能為空");
                        }
                    }
                }
            }
            //拋給下一個中間件
            await _next.Invoke(context);
        }

只貼出了最主要的Invoke方法,可以看出,做的事情就是,匹配當前請求的路徑是不是需要進行解密操作,需要解密的話,進行解密操作后再將真實的參數模型重新寫入請求中。

這里有兩點需要注意:

  1. 中間件是拿不到Action的上下文的一些信息的。所以我們在這里其實也不知道它請求的是哪個控制器的哪個方法,因此也無法確定該方法是不是有我們自定義的AutoDecryptModel標識,也就是說我們不知道這個進來的請求到底需不需要進行解密操作,由此引出接下來的第三步,來確定需要進行解密的請求。

  2. 讀取與修改請求的Body。這里不進行過多的闡述,總之我們需要在讀取請求的Body之前,先執行一個這樣的方法:

      public async Task Invoke(HttpContext context)
            {
                //允許讀取請求的Body
                context.Request.EnableBuffering();
                //拋給下一個中間件
                await _next.Invoke(context);
            }
    

    這個中間件需要放在解密中間件之前,詳細原因請查看此篇文章:解決Asp.Net Core 3.1 中無法讀取HttpContext.Request.Body的問題

第三步,確定需要進行解密的請求。

通過第二步我們可以得到,我們需要讓解密中間件知道哪些請求是需要進行解密操作的。這里采用的方法是在程序啟動時候,通過反射拿到所有具有AutoDecryptModel特性的Action,將其請求路徑記錄下來並保存到內存緩存中。此方式在程序的整個生命周期中,只會執行一次。

修改Host代碼:

                Log.Information("Starting ship api web host");
                var webHost = CreateHostBuilder(args).Build();
                //Build后  Run之前
                //此時DI構建完成  但是中間件管道還未完成構建
                //Run之后中間件管道才開始構建  IStartupFilter才會被執行

                //獲取需要自動解密參數的方法
                var autoDecryptCacheConfigs = GetAssemblyAutoDecryptModelMethods();
                //加入緩存服務
                var cache = webHost.Services.GetRequiredService<IMemoryCache>();
                cache.Set(MemoryCacheConstants.AutoDecryptMethodTemplates, autoDecryptCacheConfigs);

                //啟動Host
                webHost.Run();

獲取方法:

        /// <summary>
        /// 獲取程序集中需要自動解密的方法
        /// </summary>
        /// <returns></returns>
        private static List<AutoDecryptCacheConfig> GetAssemblyAutoDecryptModelMethods()
        {
            List<AutoDecryptCacheConfig> configs = new();
            var assembly = typeof(Startup).Assembly.GetTypes().AsEnumerable()
                .Where(type => typeof(BaseController).IsAssignableFrom(type) && type != typeof(BaseController)).ToList();
            assembly.ForEach(r =>
            {
                var methods = r.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
                foreach (var methodInfo in methods)
                {
                    if (methodInfo.IsDefined(typeof(AutoDecryptModelAttribute), true))
                    {
                        if (methodInfo.IsDefined(typeof(RouteAttribute), true))
                        {
                            //獲取路由模板
                            RouteAttribute route = methodInfo.GetCustomAttribute<RouteAttribute>(true);
                            //在特性中添加自定義信息 寫入緩存  可以實現方法指定加密解密方式、指定密匙以及是否可以兼容不處理
                            AutoDecryptModelAttribute autoDecryptAttribute = methodInfo.GetCustomAttribute<AutoDecryptModelAttribute>(true);
                            if (route != null && autoDecryptAttribute != null)
                            {
                                var template = route.Template;
                                configs.Add(new AutoDecryptCacheConfig()
                                {
                                    RouteTemplate = template,
                                    SecuritySignCryptType = autoDecryptAttribute.SecuritySignCryptType,
                                    SecurityParameterDecryptType = autoDecryptAttribute.SecurityParameterDecryptType,
                                    Key = autoDecryptAttribute.Key,
                                    //Mandatory = autoDecryptAttribute.Mandatory
                                });

                                Console.WriteLine($"method:{r.Namespace}-{r.Name}-{methodInfo.Name};route:{template}");
                            }
                        }
                        else
                        {
                            var err =
                                @$"方法使用特性[{nameof(AutoDecryptModelAttribute)}]時,必須與特性[{nameof(RouteAttribute)}]一起使用,
                                                        method:{r.Namespace}-{r.Name}-{methodInfo.Name}";
                            Log.Error(err);
                        }
                    }
                }
            });
            return configs;
        }

運行起來看看:

可以看出在標記有我們AutoDecryptModel特性的Action被正確識別打印了出來,接下來只要配合我們的中間件,從內存緩存項中匹配當前請求是否需要進行解密操作,即可實現我們最初的需求。

本來到這里應該結束了,一想到常用的ActionFilter不能滿足這次的要求,便又去看了看.Net Core的幾個Filter,結果這一看。

猛的注意到了以前忽略的ModelBinding,且平時常用其他四個Filter,經常忽略ResourceFilter,似乎意識到了什么,趕緊去查了查這個Filter的定義,通常是需要對Model加工處理才用。瞬間汗顏......馬上去添加了新的解決方案。

方案二,自定義ResourceFilter

實現IAsyncResourceFilter

將我們的AutoDecryptModel特性實現IAsyncResourceFilter接口,如果當前請求的Action上存在AutoDecryptModel特性,那么框架會自動幫你調用解密方法,只需要一個類即可完成整個流程。

全部代碼如下:

    /// <summary>
    /// 自動解密請求參數
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class AutoDecryptModelAttribute : Attribute, IAsyncResourceFilter
    {
        /// <summary>
        /// 默認密匙
        /// </summary>
        private readonly string _defaultKey =
            AppConfigurationUtil.Configuration["Application:Security:DefaultSecurityKey"];

        /// <summary>
        /// 簽名加密方式
        /// </summary>
        public SecuritySignCryptTypeEnum SecuritySignCryptType { get; }

        /// <summary>
        /// 參數解密方式
        /// </summary>
        public SecurityParameterDecryptTypeEnum SecurityParameterDecryptType { get; }

        /// <summary>
        /// 密匙
        /// </summary>
        public string Key { get; }

        /// <summary>
        /// 自動解密 默認MD5加密簽名 DES加密參數
        /// </summary>
        public AutoDecryptModelAttribute() :
            this(SecuritySignCryptTypeEnum.Md5Encrypt, SecurityParameterDecryptTypeEnum.DesDecrypt)
        {
            Key = _defaultKey;
        }

        /// <summary>
        /// 自動解密
        /// </summary>
        /// <param name="securitySignCryptType">簽名加密方式</param>
        public AutoDecryptModelAttribute(SecuritySignCryptTypeEnum securitySignCryptType) : this(securitySignCryptType,
            SecurityParameterDecryptTypeEnum.DesDecrypt)
        {
            Key = _defaultKey;
        }

        /// <summary>
        /// 自動解密
        /// </summary>
        /// <param name="securityParameterDecryptType">參數解密方式</param>
        public AutoDecryptModelAttribute(SecurityParameterDecryptTypeEnum securityParameterDecryptType) : this(
            SecuritySignCryptTypeEnum.Md5Encrypt, securityParameterDecryptType)
        {
            Key = _defaultKey;
        }

        /// <summary>
        /// 自動解密
        /// </summary>
        /// <param name="key">密匙</param>
        /// <param name="securityCryptType">簽名加密方式</param>
        /// <param name="securityParameterDecryptType">參數解密方式</param>
        public AutoDecryptModelAttribute(string key,
            SecuritySignCryptTypeEnum securityCryptType = SecuritySignCryptTypeEnum.Md5Encrypt,
            SecurityParameterDecryptTypeEnum securityParameterDecryptType =
                SecurityParameterDecryptTypeEnum.DesDecrypt) : this(securityCryptType, securityParameterDecryptType,
            key)
        {
        }

        /// <summary>
        /// 自動解密
        /// </summary>
        /// <param name="securityCryptType">簽名加密方式</param>
        /// <param name="securityParameterDecryptType">參數解密方式</param>
        /// <param name="key">密匙</param>
        public AutoDecryptModelAttribute(SecuritySignCryptTypeEnum securityCryptType,
            SecurityParameterDecryptTypeEnum securityParameterDecryptType,
            string key = null)
        {
            Key = key;
            SecuritySignCryptType = securityCryptType;
            SecurityParameterDecryptType = securityParameterDecryptType;
        }

        public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
        {
            HttpRequest request = context.HttpContext.Request;
            string path = request.Path.Value ?? "";

            //讀取body
            request.Body.Position = 0;
            using StreamReader sr = new StreamReader(request.Body);
            string requestBody = await sr.ReadToEndAsync();
            request.Body.Position = 0;

            Console.WriteLine(
                $"當前路徑:{path},請求參數:{requestBody},解密配置:{{ {GetDecryptConfigDescription()} }}");
            //解析參數格式是否符合自動解密 不符合則直接傳遞
            var encryptParameter = (JsonConvert.DeserializeObject<EncryptParameterRequest>(requestBody)) ??
                                   new EncryptParameterRequest();

            if (!string.IsNullOrWhiteSpace(encryptParameter.Sign) &&
                !string.IsNullOrWhiteSpace(encryptParameter.Parameter))
            {
                //驗證簽名
                if (VerifySign(encryptParameter))
                {
                    //簽名對比通過 開始解密
                    var decryptParameter = DecryptParameter(encryptParameter.Parameter);
                    if (!string.IsNullOrWhiteSpace(decryptParameter))
                    {
                        //_logger.LogInformation
                        Console.WriteLine($"當前路徑:{path},請求參數:{requestBody},解密結果:{decryptParameter}");
                        //寫入解密參數
                        MemoryStream stream = new MemoryStream();
                        StreamWriter writer = new StreamWriter(stream);
                        await writer.WriteAsync(decryptParameter);
                        await writer.FlushAsync();
                        //寫入
                        request.Body = stream;
                        //重置指針
                        request.Body.Position = 0;
                    }
                }
                else
                {
                    throw new Exception("422-簽名對比失敗,請求不通過");
                }
            }
            else
            {
                throw new Exception("422-簽名或加密參數不能為空");
            }

            await next();
        }

        /// <summary>
        /// 驗證簽名
        /// </summary>
        /// <param name="parameter">請求參數</param>
        private bool VerifySign(EncryptParameterRequest parameter)
        {
            //如果有加密方式不需要Key,則需要修改case
            RequiredKey(Key);
            string toVerifyStr = parameter.Parameter + Key;
            switch (SecuritySignCryptType)
            {
                case SecuritySignCryptTypeEnum.Md5Encrypt:
                    return (SecurityMd5Crypt.Md5Encrypt(toVerifyStr)).Equals(parameter.Sign);
                case SecuritySignCryptTypeEnum.Md5Encrypt16:
                    return (SecurityMd5Crypt.Md5Encrypt16(toVerifyStr)).Equals(parameter.Sign);
                case SecuritySignCryptTypeEnum.Md5Encrypt32:
                    return (SecurityMd5Crypt.Md5Encrypt32(toVerifyStr)).Equals(parameter.Sign);
                case SecuritySignCryptTypeEnum.Sha1Encrypt:
                    return (SecurityShaCrypt.Sha1Encrypt(toVerifyStr)).Equals(parameter.Sign);
                case SecuritySignCryptTypeEnum.Sha256Encrypt:
                    return (SecurityShaCrypt.Sha256Encrypt(toVerifyStr)).Equals(parameter.Sign);
            }
            return false;
        }

        /// <summary>
        /// 解密參數
        /// </summary>
        /// <param name="encryptParameterStr">加密參數參數</param>
        /// <returns></returns>
        private string DecryptParameter(string encryptParameterStr)
        {
            if (!string.IsNullOrWhiteSpace(encryptParameterStr))
            {
                switch (SecurityParameterDecryptType)
                {
                    case SecurityParameterDecryptTypeEnum.DesDecrypt:
                        RequiredKey(Key);
                        return SecurityDesCrypt.DesDecrypt(encryptParameterStr, Key);
                    case SecurityParameterDecryptTypeEnum.Base64Decrypt:
                        return SecurityBase64Crypt.Base64Decrypt(encryptParameterStr);
                }
            }
            return null;
        }

        private static void RequiredKey(string key)
        {
            if (string.IsNullOrWhiteSpace(key))
            {
                throw new Exception("422-密匙不能為空");
            }
        }

        /// <summary>
        /// 獲取解密配置相關描述信息
        /// </summary>
        /// <returns></returns>
        private string GetDecryptConfigDescription()
        {
            return
                $"簽名加密方式:{SecuritySignCryptType.ToDescriptionString()},參數解密方式:{SecurityParameterDecryptType.ToDescriptionString()}";
        }
    }

使用方式和之前一樣,在需要進行自動解密的Action上打上AutoDecryptModel特性標識即可,解密方式也可以靈活配置,由於此時ModelBinding還未執行,解密后的參數模型也可以應用框架的模型驗證,完美達到了我們的目的。相比方案一精簡干練了很多,省去了中間件,省去了反射配置等等。運行起來驗證測試,一切如預期一樣進行~,至此完結,撒花。

總結

多多了解框架的設計,思考與實踐。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM