1. 程式人生 > >.NET Core中的資料保護元件

.NET Core中的資料保護元件

背景介紹

在 OWASP(開放式 Web 應用程式安全專案) 2013 年釋出的報告中,將不安全的直接物件引用(Insecure Direct Object Reference)標記為 十大 Web 應用程式風險之一, 其表現形式是物件的引用(例如資料庫主鍵)被各種惡意攻擊利用, 所以對於Api返回的各種主鍵外來鍵ID, 我們需要進行加密。

.NET Core 的資料保護元件

.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.csConfigureService方法中配置使用預設的資料保護元件。

    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介面, 並實現了OnActionExecutingOnActionExecuted方法
  • 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特性。 執行程式碼之後,程式碼和之前的效果一樣。