在分布式系统的交互中,一般有两种方式
一、服务颁发给消费方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方法,可以看出,做的事情就是,匹配当前请求的路径是不是需要进行解密操作,需要解密的话,进行解密操作后再将真实的参数模型重新写入请求中。
这里有两点需要注意:
-
中间件是拿不到Action的上下文的一些信息的。所以我们在这里其实也不知道它请求的是哪个控制器的哪个方法,因此也无法确定该方法是不是有我们自定义的
AutoDecryptModel
标识,也就是说我们不知道这个进来的请求到底需不需要进行解密操作,由此引出接下来的第三步,来确定需要进行解密的请求。 -
读取与修改请求的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还未执行,解密后的参数模型也可以应用框架的模型验证,完美达到了我们的目的。相比方案一精简干练了很多,省去了中间件,省去了反射配置等等。运行起来验证测试,一切如预期一样进行~,至此完结,撒花。
总结
多多了解框架的设计,思考与实践。