背景介绍
在 OWASP(开放式 web 应用程序安全项目) 2013 年发布的报告中, 将不安全的直接对象引用 (Insecure Direct Object Reference) 标记为 十大 Web 应用程序风险之一, 其表现形式是对象的引用 (例如数据库主键) 被各种恶意攻击利用, 所以对于 API 返回的各种主键外键 ID, 我们需要进行加密.
.NET Core 中内置了一个 IDataProtectionProvider 接口和一个 IDataProtector 接口. 其中 IDataProtectionProvider 是创建保护组件的接口, IDataProtector 是数据保护的接口. 开发人员可以实现这 2 个接口, 创建数据保护组件.
内置的数据保护组件
.NET Core 中默认提供了一个数据保护组件, 下面我们来尝试使用这个默认组件来保护我们的数据.
例: 当前我们有一个 Movie 类, 代码如下, 我们期望当获取 Movie 对象的时候, Id 字段是加密的.
- public class Movie
- {
- public Movie(int id, string title)
- {
- Id = id;
- Title = title;
- }
- public int Id { get; set; }
- public string Title { get; set; }
- }
首先我们需要在 Startup.cs 中 ConfigureService 方法中配置使用默认的数据保护组件.
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddMvc();
- services.AddDataProtection();
- }
这段代码会启用. NET Core 默认的数据保护器.
然后我们创建一个 MoviesController, 并在构造函数中注入 IDataProtectionProvider 对象, 然后使用这个 Provider 对象创建一个实现 IDataProtector 接口的数据保护器对象
- [Route("movies")]
- public class MoviesController : Controller
- {
- private readonly IDataProtector protector;
- public MoviesController(IDataProtectionProvider provider)
- {
- this.protector = provider.CreateProtector("protect_my_query_string");
- }
- }
TIPS: 使用 Provider 创建 Protector 的时候, 我们传入了一个参数 "protect_my_query_string", 这个参数标明了这个保护器的用途, 你也可以把它就当成这个保护器的名字.
注意: 不同用途的保护器不能解密对方的加密字符串., 如果使用了保护器 A 去解密保护器 B 生成的字符串, 会产生以下异常 CryptographicException: The payload was invalid.
然后我们在 MovieController 中添加 2 个 API, 一个是获取所有 Movies 对象的, 一个是获取指定 Movie 对象的
- [HttpGet]
- public IActionResult Get()
- {
- var model = GetMovies();
- var outputModel = model.Select(item => new
- {
- Id = this.protector.Protect(item.Id.ToString()),
- item.Title,
- item.ReleaseYear,
- item.Summary
- });
- return Ok(outputModel);
- }
- [HttpGet("{id}")]
- public IActionResult Get(string id)
- {
- var orignalId = int.Parse(this.protector.Unprotect(id));
- var model = GetMovies();
- var outputModel = model.Where(item => item.Id == orignalId);
- return Ok(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":"钢铁侠"}]
带过期时间的数据保护器(Limited Lifetime)
.NET Core 默认还提供了一种带过期时间的数据保护器, 这种数据保护器许多使用场景, 最常用的场景就是当为一个重置密码操作的 Token 设置失效时间, 这样一旦超时的, Token 就不能解密成功, 从而我们就可以认定重置密码操作超时了.
.NET Core 中, 我们可以使用 IDataProtector 接口的 ToTimeLimitedDataProtector 方法创建一个带过期时间的数据保护器.
这里我们还是使用默认还是继续以上面的例子为例, 代码修改如下
- private readonly ITimeLimitedDataProtector protector;
- public MoviesController(IDataProtectionProvider provider)
- {
- this.protector = provider.CreateProtector("protect_my_query_string")
- .ToTimeLimitedDataProtector();
- }
- [HttpGet]
- public IActionResult Get()
- {
- var model = GetMovies(); // simulate call to repository
- var outputModel = model.Select(item => new
- {
- Id = this.protector.Protect(item.Id.ToString(),
- TimeSpan.FromSeconds(10)),
- item.Title,
- item.ReleaseYear,
- item.Summary
- });
- return Ok(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: The payload expired at 9/29/2018 11:25:05 AM +00:00. 这说明当前加密的有效期已过, 不能正确解密了.
Tips: 使用 Action Filter 解密参数
在之前的代码中, 我们在获取单个 Movie 的方法中, 我们手动调用了 Unprotected 方法来解密 id 属性.
- [HttpGet("{id}")]
- public IActionResult Get(string id)
- {
- var orignalId = int.Parse(this.protector.Unprotect(id));
- var model = GetMovies(); // simulate call to repository
- var outputModel = model.Where(item => item.Id == orignalId);
- return Ok(outputModel);
- }
下面我们改用 Action Filter 来改进这部分代码.
首先我们创建一个 DecryptReferenceFilter, 代码如下:
- public class DecryptReferenceFilter : IActionFilter
- {
- private readonly IDataProtector protector;
- public DecryptReferenceFilter(IDataProtectionProvider provider)
- {
- this.protector = provider.CreateProtector("protect_my_query_string");
- }
- public void OnActionExecuting(ActionExecutingContext context)
- {
- object param = context.RouteData.Values["id"].ToString();
- var id = int.Parse(this.protector.Unprotect(param.ToString()));
- context.ActionArguments["id"] = id;
- }
- public void OnActionExecuted(ActionExecutedContext context)
- {
- }
- }
- public class DecryptReferenceAttribute : TypeFilterAttribute
- {
- public DecryptReferenceAttribute() :
- base(typeof(DecryptReferenceFilter))
- { }
- }
代码解释
这里
DecryptReferenceFilter
实现了 IActionFilter 接口, 并实现了 OnActionExecuting 和 OnActionExecuted 方法
在
DecryptReferenceFilter
类中, 我们注入了默认的数据保护器提供器, 并在构造函数中初始化了一个数据保护器
在 OnActionExecuting 中我们从 RouteData 中获取到未解密的 id 字段, 然后将其解密之后, 替换了之前未解密的 id 字段, 这样 ModelBinder 就会使用解密后的字符串来绑定模型.
最终修改
最后我们修改一下获取单个 Movie 的 API, 代码如下:
- [HttpGet("{id}")]
- [DecryptReference]
- public IActionResult Get(int id)
- {
- var model = GetMovies();
- var outputModel = model.Where(item => item.Id == id);
- return Ok(outputModel);
- }
我们在获取单个 Movie 的方法上添加了 DecryptReference 特性.
运行代码之后, 代码和之前的效果一样.
来源: https://www.cnblogs.com/lwqlun/p/9726191.html