當你需要一個安全的機制來保護你的資料時,不妨可以使用 Net Core 的資料保護(Data Protection)。
他有以下特點:
1. 容易使用、擴展性高
2. 基於非對稱的加密機制
3. 正常情況下,不需要去設定與管理任何密鑰的儲存位置與生命週期
簡單來說你想加解密資料時,可以直接使用 Data Protection 的 API:
- Protect
- Unprotect
傻瓜式(?)的使用方式,也避免開發者:
1. 搞不清楚「對稱、非對稱加密、Hash、編碼」的差異下,選擇了錯誤的方式來保護敏感資料。
2. 自己造輪子實作加解密...
> 了解更多請參考前一篇 .NET Core 使用 Data Protection API 來保護敏感資料 - 1
by Author
在 Net Framework 時代,透過 MachineKey 來處理 Forms Authentication 的 Cookie 加解密(當然正常情況下也不會去動到 MachineKey,都交由系統處理)。
而 Net Core 雖沒有 Forms Authentication,但卻有差不多的 Cookie-based Authentication,他的 Cookie 加解密則是由上方提到的 Data Protection 機制來處理。
「希望在網站過版時不用停機」
前陣子碰到這個需求,所以同事多建了一個站台,設定好主站台關閉後網址如何對應到備援的站台(同個網址),然後 Cookie 的 Domain 和有效期限也都有設定好,看起來需求就解決了,但實際測試時,卻有以下問題:
1. User 在主站台已經登入的情況下使用系統
2. 把新程式過版到備援站台
3. 關閉主站台,讓後續流量都交由備援站台處理
4. User 繼續使用系統,卻是沒有登入的狀態,被導到登入畫面
當下直覺是備援站台無法讀取主站台的 Cookie,後來確認了一下,一半對一半錯XD
Cookie 是可以讀取的,畢竟是同一個網址,退一步來說就算是相同 Domain 下也會讀取的到,無法讀取的是 Cookie Authentication 的驗證 Cookie。
顯然地,備援站台因為 Key 不對,無法解開主站台留下的 Cookie,所以問題收斂成需要搞懂:
1. Data Protection 的金鑰保存在哪?
2. 金鑰多久會過期?
3. 我要怎麼讓不同站台共用金鑰?
該來的還是得來,啃了官方又香又長的文件。
會依照應用程式的操作環境而有所不同:
1. (這裡不討論)在 Azure 上會存在 %HOME%\ASP.NET\DataProtection-Keys 資料夾中
2. 其餘則是除非有特別設定,否則存放在記憶體
所以有個滿嚴重的問題,如果 Net Core 專案部屬在 IIS 上,應用程式集區預設的:
1. 29 小時定期回收
2. 閒置 20 分自動終止(Terminate)
或是集區手動回收, 都會導致驗證的 Cookie 無法讀取。
除了驗證 Cookie 以外,只要是該機制加密的東西也會無法讀取,像是 CSRF Token。
> 補充一下實驗結果,如果集區回收可以接受金鑰遺失,而閒置不想要因為後續行為遺失的話,動作由 Terminate 改為 Suspend(凍結)、或是閒置時間直接改為 0 分鐘(不終止也不凍結),則不會導致記憶體中的金鑰遺失。
所以如果在 IIS 下想要保存金鑰,有多種設定方式,以下列兩種較方便的設定方式:
1. 建立 Data Protection Registry keys,將金鑰放在 HKLM 機碼中,並限定該集區的帳戶才能存取。
2. (推薦)設定 IIS 應用程式集區載入使用者設定檔,金鑰會存在 %LOCALAPPDATA%\ASP.NET\DataProtection-Keys 資料夾中。
設定為 True 後,會將金鑰保存到上述的資料夾路徑中,也就是圖中的 xml 檔案。
裡面的內容有:
1. 建立、啟用、逾期時間(除非特別設定,否則金鑰預設的存活時間是三個月)
2. 金鑰(在 Windows 平台下會透過 DPAPI 加密)
提醒一下,此金鑰和 MachineKey 一樣重要,千萬不能外洩!
了解 Data Protection 的金鑰管理機制後,回頭看我們的問題,還差了那麼一點,因為上面講的是單個集區保存金鑰,但多個集區(正式、備援)依舊是讀取各自保存的金鑰,只是集區回收後金鑰不會遺失而已。
所以如果要讓多個集區、或是分散式部屬的應用程式能透過 Cookie 達到 SSO 的效果,勢必得把金鑰存到這些應用都能共用的地方。
我們可以指定金鑰存到以下位置:
1. Azure Key Vault
2. File System
3. DB
請依照專案環境選擇合適的方式,像是兩個站台都建在同一台 Server 上,可以考慮存到 File System;反之可以存到 DB 比較方便。
存到系統上非常簡單,如下配置後,就能夠將上述提到的 xml 存到指定路徑。
public void ConfigureServices(IServiceCollection services) { services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(Configuration["YourFilePath"])) .SetApplicationName("YourApplicationName"); // 請把所有想要共用同個金鑰的應用程式都指定相同的 Application Name,不然就算吃到相同的金鑰, // 也會因為 Net Core 應用程式隔離的特性無法成功加解密 }
> 了解更多 SetApplicationName 應用程式隔離
by Author
Key Storage Providers 也有常見的解決方案可以選擇
1. Entity Framework Core
2. Redis
這邊選擇透過 EF Core 存到 DB
寫法如下:
1. NuGet 下載 Microsoft.AspNetCore.DataProtection.EntityFrameworkCore
2. 新增一個 DbContext
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; public class MyKeysContext : DbContext, IDataProtectionKeyContext { // A recommended constructor overload when using EF Core // with dependency injection. public MyKeysContext(DbContextOptions<MyKeysContext> options) : base(options) { } // This maps to the table that stores keys. public DbSet<DataProtectionKey> DataProtectionKeys { get; set; } }
3. 配置連線資訊與指定 PersistKeysToDbContext
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection"))); // Add a DbContext to store your Database Keys services.AddDbContext<MyKeysContext>(options => options.UseSqlServer( Configuration.GetConnectionString("MyKeysConnection"))); // using Microsoft.AspNetCore.DataProtection; services.AddDataProtection() .PersistKeysToDbContext<MyKeysContext>() .SetApplicationName("YourApplicationName"); // 請把所有想要共用同個金鑰的應用程式都指定相同的 Application Name,不然就算吃到相同的金鑰, // 也會因為 Net Core 應用程式隔離的特性無法成功加解密 }
4. migration 然後 update database 產生 DataProtectionKeys 的 Table
dotnet ef migrations add AddDataProtectionKeys --context MyKeysContext dotnet ef database update --context MyKeysContext
存到 DB 其實只是把 xml 內容存進去而已,沒有做特別改動
基本上就是這樣,如果有問題的話大概就是第四步,因為我們專案 EF Core 是採 DB First 形式,還是可以解決,只是會有兩種方法:
1. 可以的話直接 migration 然後 update database,長出 DataProtectionKeys 的 Table 之後,把所有 migration 相關的資料夾、檔案、Table 都刪除。
2. 如果不能用程式動 Table 的話,可以把 DataProtectionKeys 的 Table 寫成 SQL Script 來給相關人員 Create。
這邊附上 SQL Server + SSMS 直接產生的 Script,使用前請先確認你使用的 DB 語法有沒有正確。
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[DataProtectionKeys]') AND type in (N'U')) DROP TABLE [dbo].[DataProtectionKeys] GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[DataProtectionKeys]( [Id] [int] IDENTITY(1,1) NOT NULL, [FriendlyName] [nvarchar](max) NULL, [Xml] [nvarchar](max) NULL, CONSTRAINT [PK_DataProtectionKeys] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO
只要指定金鑰的儲存位置,不管是存到 File System 還是 DB,Net Core 都不會把金鑰加密,因為他不知道 DPAPI 是否為適當的加密機制,所以如果有改變儲存位置,記得選擇適當的加密方式。
1. DPAPI(Windows Only)
2. X.509 憑證
3. 自訂
若以保存到 File System 為例,XML 打開會看到警示: Warning: the key below is in an unencrypted form.。
> 了解更多 NET Core 的待用金鑰加密
by Author
我最後選擇透過 EF Core 將金鑰存到 DB,設定共用的金鑰後,也成功讓正式與備援站台讀取彼此的驗證 Cookie 達到不停機過版的效果,當然未來如果 I/O 遇到效能瓶頸,可能會考慮存到 Redis。
礙於篇幅與實用性,沒有講到全部的細節,故在文中都有穿插連結,不管是有興趣的人還是想要自己處理金鑰的人,都建議閱讀。
文中內容若有錯誤的地方,請不吝告知。
本文同步發表於我的 Blog