.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