详解.NET Core中的数据保护组件
背景介绍
在OWASP(开放式Web应用程序安全项目)2013年发布的报告中,将不安全的直接对象引用(InsecureDirectObjectReference)标记为十大Web应用程序风险之一,其表现形式是对象的引用(例如数据库主键)被各种恶意攻击利用,所以对于Api返回的各种主键外键ID,我们需要进行加密。
.NETCore的数据保护组件
.NETCore中内置了一个IDataProtectionProvider接口和一个IDataProtector接口。其中IDataProtectionProvider是创建保护组件的接口,IDataProtector是数据保护的接口。开发人员可以实现这2个接口,创建数据保护组件。
内置的数据保护组件
.NETCore中默认提供了一个数据保护组件,下面我们来尝试使用这个默认组件来保护我们的数据。
例:当前我们有一个Movie类,代码如下,我们期望当获取Movie对象的时候,Id字段是加密的。
publicclassMovie
{
publicMovie(intid,stringtitle)
{
Id=id;
Title=title;
}
publicintId{get;set;}
publicstringTitle{get;set;}
}
首先我们需要在Startup.cs中ConfigureService方法中配置使用默认的数据保护组件。
publicvoidConfigureServices(IServiceCollectionservices)
{
services.AddMvc();
services.AddDataProtection();
}
这段代码会启用.NETCore默认的数据保护器。
然后我们创建一个MoviesController,并在构造函数中注入IDataProtectionProvider对象,然后使用这个Provider对象创建一个实现IDataProtector接口的数据保护器对象
[Route("movies")]
publicclassMoviesController:Controller
{
privatereadonlyIDataProtectorprotector;
publicMoviesController(IDataProtectionProviderprovider)
{
this.protector=provider.CreateProtector("protect_my_query_string");
}
}
TIPS:使用Provider创建Protector的时候,我们传入了一个参数"protect_my_query_string",这个参数标明了这个保护器的用途,你也可以把它就当成这个保护器的名字。
注意:不同用途的保护器不能解密对方的加密字符串。,如果使用了保护器A去解密保护器B生成的字符串,会产生以下异常CryptographicException:Thepayloadwasinvalid.
然后我们在MovieController中添加2个Api,一个是获取所有Movies对象的,一个是获取指定Movie对象的
[HttpGet]
publicIActionResultGet()
{
varmodel=GetMovies();
varoutputModel=model.Select(item=>new
{
Id=this.protector.Protect(item.Id.ToString()),
item.Title,
item.ReleaseYear,
item.Summary
});
returnOk(outputModel);
}
[HttpGet("{id}")]
publicIActionResultGet(stringid)
{
varorignalId=int.Parse(this.protector.Unprotect(id));
varmodel=GetMovies();
varoutputModel=model.Where(item=>item.Id==orignalId);
returnOk(outputModel);
}
代码解释
- 在获取Movie列表的api中,我们使用了IDataProtector接口的Protect方法对Id字段进行了加密
- 相应的在获取单个Movie对象的api中,我们需要使用IDataProtector接口的Unprotect方法对Id字段进行解密。
最终效果
首先我们调用/api/movies,返回结果如下,id字段已经被正确加密了
[{
"id":"CfDJ8D9KlbQBeipPoQwll5uLR6ygyO6avkgI2teCQGZQShNwsxC9ApDdsnyYd1K5IyNHjhZcRoGd6W31se3W6TWM8H9UdLEPn4fJpS5uKkqUa0PMV6a0ZZHBQSnlGoisSnj29g",
"title":"泰坦尼克号"
},{
"id":"CfDJ8D9KlbQBeipPoQwll5uLR6wkMUYyzflIzy3CwoMhcaO-np2WOy4czIL3WZd2FWi7Tsy119tDeFq7yAeye4o2W-KmbffpGXnTDZzNv2QbCrAm7-AyEN35g3pkfAYHa3X7aQ",
"title":"我是谁"
},{
"id":"CfDJ8D9KlbQBeipPoQwll5uLR6x2AXM6ulCwts2-uQSfzIU8UquTz-OAZIl-49D5-CYYl5H4mfZH8VihhCBJ60MMrZOlZla9qvb8EIP6GYRkEap4nhktbzGxW0Qu5r3edm6_Kg",
"title":"蜘蛛侠"
},{
"id":"CfDJ8D9KlbQBeipPoQwll5uLR6zDZeLtPIVlkRLCd_V6Mr2kTzWsCkfYgmS0-cqhFAOu4dUWGtx6d402_eKnObAOFUClEDdF4mrUeDQawE71DDa805umhbAvX2712i7UgYO5MA",
"title":"钢铁侠"
}]
然后我们继续调用api,查询钢铁侠的电影信息
/api/movies/CfDJ8D9KlbQBeipPoQwll5uLR6zDZeLtPIVlkRLCd_V6Mr2kTzWsCkfYgmS0-cqhFAOu4dUWGtx6d402_eKnObAOFUClEDdF4mrUeDQawE71DDa805umhbAvX2712i7UgYO5MA
结果也正确的返回了。
[{"id":4,"title":"钢铁侠"}]
带过期时间的数据保护器(LimitedLifetime)
.NETCore默认还提供了一种带过期时间的数据保护器,这种数据保护器许多使用场景,最常用的场景就是当为一个重置密码操作的Token设置失效时间,这样一旦超时的,Token就不能解密成功,从而我们就可以认定重置密码操作超时了。
.NETCore中,我们可以使用IDataProtector接口的ToTimeLimitedDataProtector方法创建一个带过期时间的数据保护器。
这里我们还是使用默认还是继续以上面的例子为例,代码修改如下
privatereadonlyITimeLimitedDataProtectorprotector;
publicMoviesController(IDataProtectionProviderprovider)
{
this.protector=provider.CreateProtector("protect_my_query_string")
.ToTimeLimitedDataProtector();
}
[HttpGet]
publicIActionResultGet()
{
varmodel=GetMovies();//simulatecalltorepository
varoutputModel=model.Select(item=>new
{
Id=this.protector.Protect(item.Id.ToString(),
TimeSpan.FromSeconds(10)),
item.Title,
item.ReleaseYear,
item.Summary
});
returnOk(outputModel);
}
代码解释
- 这里我们定义了一个ITimeLimitedDataProtector接口对象protector,并在构造函数中使用ToTimeLimitedDataProtector方法,将一个普通的数据保护器转换成了一个带过期时间的数据保护器
- 在获取Movie列表的api中,我们依然使用Protect方法来加密Id字段,与之前不同的是,这里我们加入了第二个TimeSpan参数,这个参数表示了当前加密的有效时间只有10秒。
最终效果
现在我们重新运行项目,还是和之前一样先调用/api/movies方法来获取Movies列表,结果如下
[{
"id":"CfDJ8D9KlbQBeipPoQwll5uLR6yzbDbZ931toH32VC6Jqg8DWsrmiLrOxOFFViH4QWZne43jwSVzBjzJIfctYKZniZKNVbr50RRIZpW2fe9UtPajEzBhI-H32Effm-F0ColUaA",
"title":"泰坦尼克号"
},{
"id":"CfDJ8D9KlbQBeipPoQwll5uLR6zDDVymvftZK9lKBIjEyuoNTzOEu0SC2-qfTy6quXir2S8f3A1r44f9Yz3Sd_cyLZUp-_4gfJAasMfE8_ngYLrJmdsjN9LZ0g4vox0WJLjiGA",
"title":"我是谁"
},{
"id":"CfDJ8D9KlbQBeipPoQwll5uLR6zL-M2jzv2HCeTiHjevkXvI2216NERplp43TOjCXtj4S52ll68sLyQNtG2FhhWlsOmFGvYY5G4gm5SKfASMMgE1jBr20xc2b_djWdLhWLIxnA",
"title":"蜘蛛侠"
},{
"id":"CfDJ8D9KlbQBeipPoQwll5uLR6wAoZKCHTG0lvgYS3If_0_eAD30a2YV8RjNagwLXUdCSKsO3kyS58hqDqAPHw_KHwNpd-hjDFl3hFPa8LOWHyk901oc6ZuSxwzxFlljaVreFA",
"title":"钢铁侠"
}]
等待10秒钟后,我们继续调用api,查询钢铁侠的电影信息
/api/movies/CfDJ8D9KlbQBeipPoQwll5uLR6wAoZKCHTG0lvgYS3If_0_eAD30a2YV8RjNagwLXUdCSKsO3kyS58hqDqAPHw_KHwNpd-hjDFl3hFPa8LOWHyk901oc6ZuSxwzxFlljaVreFA
返回了错误信息CryptographicException:Thepayloadexpiredat9/29/201811:25:05AM+00:00.这说明当前加密的有效期已过,不能正确解密了。
Tips:使用ActionFilter解密参数
在之前的代码中,我们在获取单个Movie的方法中,我们手动调用了Unprotected方法来解密id属性
[HttpGet("{id}")]
publicIActionResultGet(stringid)
{
varorignalId=int.Parse(this.protector.Unprotect(id));
varmodel=GetMovies();//simulatecalltorepository
varoutputModel=model.Where(item=>item.Id==orignalId);
returnOk(outputModel);
}
下面我们改用ActionFilter来改进这部分代码。
首先我们创建一个DecryptReferenceFilter,代码如下:
publicclassDecryptReferenceFilter:IActionFilter
{
privatereadonlyIDataProtectorprotector;
publicDecryptReferenceFilter(IDataProtectionProviderprovider)
{
this.protector=provider.CreateProtector("protect_my_query_string");
}
publicvoidOnActionExecuting(ActionExecutingContextcontext)
{
objectparam=context.RouteData.Values["id"].ToString();
varid=int.Parse(this.protector.Unprotect(param.ToString()));
context.ActionArguments["id"]=id;
}
publicvoidOnActionExecuted(ActionExecutedContextcontext)
{
}
}
publicclassDecryptReferenceAttribute:TypeFilterAttribute
{
publicDecryptReferenceAttribute():
base(typeof(DecryptReferenceFilter))
{}
}
代码解释
- 这里DecryptReferenceFilter实现了IActionFilter接口,并实现了OnActionExecuting和OnActionExecuted方法
- 在DecryptReferenceFilter类中,我们注入了默认的数据保护器提供器,并在构造函数中初始化了一个数据保护器
- 在OnActionExecuting中我们从RouteData中获取到未解密的id字段,然后将其解密之后,替换了之前未解密的id字段,这样ModelBinder就会使用解密后的字符串来绑定模型。
最终修改
最后我们修改一下获取单个Movie的api,代码如下:
[HttpGet("{id}")]
[DecryptReference]
publicIActionResultGet(intid)
{
varmodel=GetMovies();
varoutputModel=model.Where(item=>item.Id==id);
returnOk(outputModel);
}
我们在获取单个Movie的方法上添加了DecryptReference特性。
运行代码之后,代码和之前的效果一样。
源码地址:http://xiazai.jb51.net/201809/yuanma/id_protector_jb51.rar