ASP.NET沒有魔法——ASP.NET Identity的加密與解密

来源:http://www.cnblogs.com/selimsong/archive/2017/11/03/7771875.html
-Advertisement-
Play Games

前面文章介紹瞭如何使用Identity在ASP.NET MVC中實現用戶的註冊、登錄以及身份驗證。這些功能都是與用戶信息安全相關的功能,數據安全的重要性永遠放在第一位。那麼對於註冊和登錄功能來說要把密碼及用戶其它信息通過表單的形式安全的提交到伺服器上,那麼最適合的方法就是使用HTTPS(如果有條件或 ...


   前面文章介紹瞭如何使用Identity在ASP.NET MVC中實現用戶的註冊、登錄以及身份驗證。這些功能都是與用戶信息安全相關的功能,數據安全的重要性永遠放在第一位。那麼對於註冊和登錄功能來說要把密碼及用戶其它信息通過表單的形式安全的提交到伺服器上,那麼最適合的方法就是使用HTTPS(如果有條件或者有安全需求,應該所有請求都基於HTTPS,本章不涉及HTTPS的介紹),而在註冊時用戶的密碼應該加密後保存在資料庫中,包括登錄時對用戶名的驗證也是對密碼明文加密後再進行匹配,對於身份驗證來說,伺服器生成的用戶信息字元串是必須進行加密的,其目的是保護用戶信息並且能夠讓當前的伺服器(或集群)能夠識別。

    本章將從以下幾點對Identity中涉及到的加解密進行介紹:
  ● 常用的加密方法
  ● .Net對常用加密方法的實現
  ● Identity用戶密碼的加解密
  ● Identity用戶身份信息的處理過程
  ● MachineKey的加解密
  ● 自定義Identity身份信息的驗證(基於MachineKey)

常用的加密方法

  軟體中常用的加密方法分為兩類,一類是密文可解密回明文的,而另一類是密文不可解密的。
  對於可解密的這一類主要是通過對稱加密演算法非對稱加密演算法,如DES、AES、RSA等,它們最主要的特點是需要“密鑰”來進行加解密工作,如果密鑰泄露了,那麼就會造成安全問題。
  而不可解密的這一類主要是通過MD5、SHA1這些單向Hash演算法來提取“信息指紋”,已達到“加密”的效果,但這種方法也存在缺陷就是只要演算法相同,那麼對同一個字元串加密後的結果就是相同的,當黑客拿到了用戶資料庫,雖然用戶密碼是被加密存儲,但是黑客可以通過建立“彩虹表”的方式破解密碼。所以又出現了一種通過加“鹽”的演算法,通過加入特殊的“鹽”來保證相同HASH演算法相同字元串的加密不一致性,但如果“鹽”泄露了黑客仍然能夠破解,所以又有了“隨機鹽”。
  參考:http://mp.weixin.qq.com/s?__biz=MzIwMzg1ODcwMw==&mid=2247486407&idx=1&sn=51dfbce7d04ab6faeb0f5a27a5bdcbf8&source=41#wechat_redirect

.Net對常用加密方法的實現

  .Net的System.Security.Cryptography命名空間下包含了用於加解密的類型,這些類型有些是基於托管代碼的,有些是基於Windows API的。
  .Net的加解密類型位於system.dll程式集中(註:非windows平臺下可以通過nuget安裝System.Security.Cryptography.Primitives.dll)的System.Security.Cryptography命名空間下。並且將加密演算法分為了三類:
  ● 對稱加密演算法:AES、DES等
  ● 非對稱加密演算法:RSA、ECDH 等
  ● HASH演算法:SHA1、MD5等
  另外要註意的是.Net中使用面向數據流的方式實現了對稱加密和HASH演算法。這樣的設計可以通過串聯的方式將多個加密演算法合併在一起對“數據流”進行操作(這個東西有點類似於owin中間件的方式,可以根據需求動態的對數據加密進行處理)。
  微軟官方文檔對加密演算法的使用推薦:
  ● 數據保護:Aes
  ● 數據完整性:HMACSHA256、HMACSHA512
  ● 數字簽名:ECDsa、 RSA
  ● 密鑰交換:ECDiffieHellman、 RSA
  ● 隨機數生成:RNGCryptoServiceProvider
  ● 通過密碼生成Key(使用隨機鹽的Hash演算法):Rfc2898DeriveBytes
  更多信息參考文檔:https://docs.microsoft.com/en-us/dotnet/standard/security/cryptography-model

Identity用戶密碼的加解密

  用戶的密碼一般來說是一個長度較短的包含各種字元的字元串,而對用戶密碼加密的目的是避免用戶密碼在資料庫中明文存儲,明文存儲密碼會導致系統開發或運營人員對用戶信息安全的威脅以及黑客攻擊數據泄露導致的用戶信息安全。所以一般來說加密密碼使用無法解密的Hash演算法“加密”
  根據前面文章的分析得知,用戶的創建和密碼的匹配都是通過Identity中的UserManager類型完成的:
  1. 註冊調用的代碼:

  

  2. 登錄調用的代碼(註:SignInManager位於Microsoft.AspNet.Identity.Owin程式集中):

  

  但通過查看源碼可知,SignInManager實際上也是通過UserManager來匹配密碼的:

  

  3. 所以根據上面的分析,用戶密碼的加密是在UserManager中完成的,而UserManager定義中有一個IPasswordHasher的介面,該介面定義了密碼的Hash加密以及Hash後的密碼校驗

  

  IPasswordHasher的預設實現是PasswordHasher類型:

  

  從代碼中可以看到PassswordHasher又是通過一個名稱為Crypto類型的靜態方法完成加密和驗證的:

  Hash計算:

  

  Hash驗證: 

   

  從Crypto的代碼中可以得出以下幾點結論:
  1. Identity中預設的密碼加密基於Rfc2898演算法(通過隨機鹽以及設置迭代次數來計算hash值的演算法)。
  2. 演算法中的“鹽”長度為16位,迭代計算次數為1000次(註:每次實例化Rfc2898DeriveBytes類型時會根據鹽的長度,創建一個隨機的數組。Rfc2898DeriveBytes的GetBytes的演算法不在此詳解,有興趣可參考文檔和源碼)。
  3. 加密時Identity將鹽和加密後的結果進行了拼接,前16位數據為鹽後面的是密碼加密結果。
  4. 密碼的“解密”實際上是通過已經加密的結果先獲取其前16位數據拿到鹽,然後再對傳入的密碼和這個鹽進行一次Hash,然後比較兩次的Hash結果是否相同(註:Hash演算法無法解密)。
  如果要對Identity中的用戶密碼加密演算法進行變更或者擴展,僅需實現新的IPasswordHasher,然後在創建UserManager實例時將其替換即可。
  註:事實上如果黑客拿到了上面數據理論上仍舊是可以破解密碼的,但由於鹽是隨機的,所以導致大批量破解會更加麻煩,這樣哪怕數據泄露了也有時間進行一些補救,所以Rfc2898是一種常用的密碼加密方式。

 Identity用戶身份信息的處理過程

  Identity的用戶身份信息相對於密碼來說要複雜很多,因為密碼僅僅是一個字元串,對一個字元串的加解密很容易,但是Identity的用戶身份信息實際上是一個AuthenticationTicket實例:

  

  那麼Identity是如何對這個用戶身份信息實例進行處理的呢?

  1. 首先我們知道的是Identity通過app.UseCookieAuthentication方法在管道中添加了一個類型為CookieAuthenticationMiddleware的中間件,而通過對源碼分析可以看到,該中間件中實際上是通過創建一個名為CookieAuthenticationHandler的內部類型,通過這個類型完成了請求時Cookie的獲取、驗證,驗證失敗的跳轉以及響應時Cookie的寫入等功能。

  其中Cookie的加解密代碼如下:

  解密:先獲取Cookie值,然後通過TicketDataFormat的Unprotect方法返回一個AuthenticationTicket實例:

  

  加密:將AuthenticationTicket實例通過TicketDataFormat的Protect方法轉換為一個加密後的字元串。

  

  2. Identity對用戶身份信息的處理主要是通過TicketDataFormat完成,從上面代碼中可以看到TicketDataFormat是來來自Options。這裡的Options實際上就是app.UseCookieAuthentication方法中的參數CookieAuthenticationOptions:

  

  TicketDataFormat預設值是在構造方法中創建的,它需要一個protector(註:Protector實際上就是加解密的組件,本章後面詳解)  

  

  3. TicketDataFormat的職責:

  由於TicketDataFormat是繼承於SecureDataFormat類型,並且僅僅是在構造方法中硬編碼了傳入基類的參數,所以其功能實際上是基類實現的:

  

  職責一:數據“保護”,先通過序列化器將泛型類型TData進行序列化(這裡的TData實際上是AuthenticationTicket類型),然後通過加密組件對序列化後的二進位進行加密,最後通過編碼器將二進位數據轉換為Base64Url字元串,代碼如下圖:

  

  這裡要註意以下兩點:

    1). 序列化器是由TicketDataFormat構造方法中硬編碼的,其真實類型為TicketSerializer(對於序列化這個概念,實際上就是將一個程式中的記憶體實例,用二進位數據或者XML、Json等方式保存下來,然後需要使用的時候在通過這些數據把它反序列化為之前的記憶體實例,這裡的TicketSerializer是一個二進位序列化器):

    

    2). 編碼器的名稱為Base64Url與Base64編碼器的區別是,由於Base64字元串中可能會存在斜杠(/)等特殊符號,但是這些符號在url中是無法被正確識別的,所以Base64Url對這些字元進行了特殊處理:

    

  職責二:數據的“解保護”實際上就是保護功能反過來:先將Base64Url字元串解碼為二進位數據,然後對二進位數據解密,最後對解密後的數據進行反序列化:

  

  而本章的重點實際上是在數據的加解密上,所以protector才是關註重點,這裡的protector從上面的代碼中可以看到是通過IAppBulider創建的:

  

  前面的文章分析過,Owin的核心實際上是一個字典,所以通過Owin來獲取的東西應該是保存在字典中的:

  

  AppBuilder的初始化代碼:

  

  根據上面的分析得出,在沒有指定特殊的數據保護器情況下,Identity使用MachineKeyDataProtector作為預設的數據保護器。

  補充說明:

  Identity中的身份驗證的原理,實際上是獲取到Cookie成功解密並反序列化為AuthenticationTicket實例後,將通過身份驗證的Identity(該Identity中的IsAuthenticated屬性為true)信息添加到HTTP請求的上下文中的。MVC中需要通過身份驗證的訪問控制就是通過請求上下文中Identity的IsAuthenticated屬性完成判斷的。

  

MachineKey的加解密

  .Net中有一個名為MachineKey的組件,它用於Forms驗證用戶信息、asp.net 的View State以及跨進程的會話狀態數據的加密和驗證,MachineKey可以通過在web.config文件中加入以下的配置文件來對MachineKey的加解密、驗證演算法及其密鑰進行配置,詳情可參考文檔:https://msdn.microsoft.com/en-us/library/w8h3skw9(v=vs.100).aspx

  

  而上面分析知道Identity使用MachineKeyDataProtector作為數據保護器,而MachineKeyDataProtector實際上使用的就是MachineKey:

  

  註:由於MachineKey相關代碼比較複雜,本文中僅對其主要的一些對象以及加解密過程進行介紹:

  MachineKey的主要相關對象: 

  ● AspNetCryptoServiceProvider(內部類型):ASP.NET用其獲取適合的加密組件。
  ● MachineKeySection:用於表示MachineKey的配置信息。
  ● MachineKeyCryptoAlgorithmFactory(內部類型):MachineKey的加密演算法工廠,依賴MachineKeySection,可以從配置文件中獲取加密演算法類型。
  ● MachineKeyMasterKeyProvider(內部類型):密鑰提供器,依賴MachineKeySection,可以從配置文件中獲取密鑰信息。
  ● MachineKeyDataProtectorFactory (內部類型):數據保護器工廠,用於創建自定義加解密類型(配置文件中可以通過alg:algorithm_name方法使用自定義的加密演算法)。
  ● Purpose(內部類型):用於根據加密目的來生成真正用於加密和校驗的密鑰,Identity使用的目的為User_MacineKey_Protect,User_MacineKey_Protect的主目的為User.MachineKey.Protect,特殊目的為"Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware", "ApplicationCookie","v1"(數據來自源碼分析)。換句話說如果密鑰相同,但是加密目的不一樣,那麼真實用於加解密的密鑰也是不同的。

  

  上圖為Purpose的定義,從定義中也可以看出針對功能的不同如Forms驗證的角色信息的以及WebForm中一系列組件的目的均不相同。

    ● NetFXCryptoService(內部類型):MachineKey在.Net平臺下使用的加解密服務組件。也是Identity中使用的身份信息加解密組件。

   以下代碼為NetFXCryptoService加解密的演算法,其演算法包括了數據加解密以及數據完整性校驗兩個部分:

  加密:

 1 public byte[] Protect(byte[] clearData) //claerData為需要加密的二進位數據
 2     {
 3         byte[] buffer4;
 4         using (SymmetricAlgorithm algorithm = this._cryptoAlgorithmFactory.GetEncryptionAlgorithm()) //通過工廠獲取加密演算法,實際上就是使用預設的或配置文件指定的如AES等
 5         {
 6             algorithm.Key = this._encryptionKey.GetKeyMaterial();//Purpose通過配置文件獲取加密密鑰並根據實際目的派生出來的真實密鑰
 7             if (this._predictableIV)
 8             {
 9                 algorithm.IV = CryptoUtil.CreatePredictableIV(clearData, algorithm.BlockSize);
10             }
11             else
12             {
13                 algorithm.GenerateIV();
14             }
15             byte[] iV = algorithm.IV;
16             using (MemoryStream stream = new MemoryStream())
17             {
18                 stream.Write(iV, 0, iV.Length);
19                 using (ICryptoTransform transform = algorithm.CreateEncryptor())
20                 {
21                     using (CryptoStream stream2 = new CryptoStream(stream, transform, CryptoStreamMode.Write))
22                     {
23                         stream2.Write(clearData, 0, clearData.Length);
24                         stream2.FlushFinalBlock();
25                         using (KeyedHashAlgorithm algorithm2 = this._cryptoAlgorithmFactory.GetValidationAlgorithm())//通過工廠獲取數據校驗的演算法,該演算法在配置文件中配置,如SHA1等
26                         {
27                             algorithm2.Key = this._validationKey.GetKeyMaterial();//Purpose通過配置文件獲取的數據校驗密鑰並根據實際目的派生出來的真實密鑰
28                             byte[] buffer = algorithm2.ComputeHash(stream.GetBuffer(), 0, (int) stream.Length);
29                             stream.Write(buffer, 0, buffer.Length);
30                             buffer4 = stream.ToArray();
31                         }
32                     }
33                 }
34             }
35         }
36         return buffer4;
37     }
View Code

  解密(加密的反過程):

 1 public byte[] Unprotect(byte[] protectedData)
 2     {
 3         byte[] buffer3;
 4         using (SymmetricAlgorithm algorithm = this._cryptoAlgorithmFactory.GetEncryptionAlgorithm())
 5         {
 6             algorithm.Key = this._encryptionKey.GetKeyMaterial();
 7             using (KeyedHashAlgorithm algorithm2 = this._cryptoAlgorithmFactory.GetValidationAlgorithm())
 8             {
 9                 algorithm2.Key = this._validationKey.GetKeyMaterial();
10                 int offset = algorithm.BlockSize / 8;
11                 int num2 = algorithm2.HashSize / 8;
12                 int count = (protectedData.Length - offset) - num2;
13                 if (count <= 0)
14                 {
15                     return null;
16                 }
17                 byte[] buffer = algorithm2.ComputeHash(protectedData, 0, offset + count);
18                 if (!CryptoUtil.BuffersAreEqual(protectedData, offset + count, num2, buffer, 0, buffer.Length))
19                 {
20                     buffer3 = null;
21                 }
22                 else
23                 {
24                     byte[] dst = new byte[offset];
25                     Buffer.BlockCopy(protectedData, 0, dst, 0, dst.Length);
26                     algorithm.IV = dst;
27                     using (MemoryStream stream = new MemoryStream())
28                     {
29                         using (ICryptoTransform transform = algorithm.CreateDecryptor())
30                         {
31                             using (CryptoStream stream2 = new CryptoStream(stream, transform, CryptoStreamMode.Write))
32                             {
33                                 stream2.Write(protectedData, offset, count);
34                                 stream2.FlushFinalBlock();
35                                 buffer3 = stream.ToArray();
36                             }
37                         }
38                     }
39                 }
40             }
41         }
42         return buffer3;
43     }
View Code

自定義Identity身份信息的驗證(基於MachineKey)

  本例將在Controller的Action方法中獲取登錄生成的Cookie值,並將其解密後反序列化成AuthenticactionTicket實例:

  代碼:

 1  public ActionResult Index()
 2         {
 3             //1.從Cookie中獲取加密後的用戶信息字元串
 4             var cookieStr = this.HttpContext.Request.Cookies[".AspNet.ApplicationCookie"].Value.ToString();
 5             //2.將用戶信息字元串以Base64Url的方式轉換為二進位數據
 6             var cookieBytes = TextEncodings.Base64Url.Decode(cookieStr);
 7             //3.轉換後的二進位數據通過MachineKey進行解密(註:MachinKey預設使用User_MacineKey_Protect為主目的,
 8             //特殊目的由Owin Cookie驗證中間件提供)
 9             var result = MachineKey.Unprotect(cookieBytes,
10                 new string[] { "Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware",
11                 "ApplicationCookie",
12                 "v1"});
13             TicketSerializer ticketSerializer = new TicketSerializer();
14             //4.將解密後的二進位數據反序列化為AuthenticationTicket實例
15             var ticket = ticketSerializer.Deserialize(result);
16             
17             return View();
18         }
View Code

  登錄後的運行結果:

  

  註:MachineKey可以通過配置文件來改變加解密以及數據驗證的演算法及密鑰,該配置文件可以通過IIS的“電腦密鑰”功能來實現:

  

  

小結

  本章在軟體開發中常用的加密演算法及其在.Net中的應用介紹的基礎上,引出了Identity中用戶密碼以及用戶信息的加解密的過程與方法,其中用戶密碼的加解密較為簡單,而用戶信息作為一個複雜的對象實例,在加解密之前還需要進行序列化與反序列的流程,另外也得知了對於用戶信息的保護不僅僅是加密而且還附帶了數據完整性驗證功能。數據安全是一個非常重要的話題,而Identity的身份驗證是預設ASP.NET MVC帶有獨立身份驗證模板提供的功能,一個一分鐘就能創建的應用程式模板就提供瞭如此複雜的用戶數據安全保護功能,由此可見.Net的強大之處。

  另外本章除了介紹Identity,實際上也是介紹了一種數據保護以及身份驗證的方式,在沒有使用Identity的情況下,仍舊可以使用其理念來打造一個符合自身需求的數據保護方案。

參考:

  https://docs.microsoft.com/en-us/dotnet/standard/security/cryptography-model
  https://msdn.microsoft.com/en-us/library/ff648652.aspx
  https://www.rfc-editor.org/rfc/rfc2898.txt
  https://www.codeproject.com/articles/16645/asp-net-machinekey-generator
  http://www.cnblogs.com/happyhippy/archive/2006/12/23/601353.html
  http://mp.weixin.qq.com/s?__biz=MzIwMzg1ODcwMw==&mid=2247486407&amp;idx=1&amp;sn=51dfbce7d04ab6faeb0f5a27a5bdcbf8&source=41#wechat_redirect

本文鏈接:http://www.cnblogs.com/selimsong/p/7771875.html 

 ASP.NET沒有魔法——目錄


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、幾種枚舉類代碼示例 1、最簡單枚舉類 2、一般枚舉類用法 二、枚舉類方法 1、values()方法,返回一個enum實例的數組,數組中元素嚴格保持其在enum中聲明時的順序 2、ordinal(),返回該enum實例在enum中聲明的次序(從0開始) 3、compareTo()方法,比較enum ...
  • 什麼是框架 ...
  • 1、整數轉二進位 2、搖骰子游戲 3、猜密碼 4、查詢天氣 首先創建city.py ...
  • 人生得意須盡歡,莫使金樽空對月。 先天下之憂而憂,後天下之樂而樂。 大東北的天氣已經漸入佳境了,在夜深人靜的時候,隨著滑鼠的移動,鍵盤清脆的聲音,開啟了今天的睡前代碼工程!今天聊聊JDBC版本的分頁,分頁功能在很多的web項目中都是必須的功能,然而分頁有真假分頁,假分頁,例如某某某網站,你懂得。什麼 ...
  • Spring簡介 網站: 複雜的Java EE項目用Spring才會得到優化,如果太簡單的項目用框架反而會變的麻煩。 ...
  • 背景 最近興趣使然寫了幾個Python庫,也發佈到了Pypi上,雖然沒什麼人下載,但自己在其他機器上用著也會很方便。這裡我向大家介紹一下如何在Pypi上發表自己的Python庫。 準備 註冊賬號 很顯然地要在Pypi上註冊一個賬號。 安裝必要的庫 setuptools 原則上安裝了pip的環境都有s ...
  • 初學者,寫的不好請指出。 #第一步以insertTime為條件查詢時間段內的數據 #第二部步可以選擇是否再以通話Id為條件篩選第一步所查詢出來的數據 #因為使用的是配置文件,所以首先在代碼當前目錄下創建一個配置文件,db.conf 代碼: ...
  • 面向對象編程的基本理念與核心設計思想 解釋下多態性(polymorphism),封裝性(encapsulation),內聚(cohesion)以及耦合(coupling)。 繼承(Inheritance)與聚合(Aggregation)的區別在哪裡。 你是如何理解乾凈的代碼(Clean Code)與 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:這個WPF項目通過XAML繪製汽車動態速度表盤,實現了0-300的速度刻度,包括數字、指針,並通過定時器模擬速度變化,展示了動態效果。詳細實現包括界面設計、刻度繪製、指針角度計算等,通過C#代碼與XAML文件結合完成。 新建 WPF 項目: 在 Visual Studio 中創建一個新的 WP ...
  • 概述:在WPF中使用`WpfAnimatedGif`庫展示GIF動畫,首先確保全裝了該庫。通過XAML設置Image控制項,指定GIF路徑,然後在代碼中使用庫提供的方法實現動畫控制。這簡化了在WPF應用中處理GIF圖的過程,提供了方便的介面來管理動畫播放和暫停。 當使用 WpfAnimatedGif  ...
  • 您是否曾經訪問過一個網站,它需要很長時間載入,最終你敲擊 F5 重新載入頁面。 即使用戶刷新了瀏覽器取消了原始請求,而對於伺服器來說,API也不會知道它正在計算的值將在結束時被丟棄,刷新五次,伺服器將觸發 5 個請求。 為瞭解決這個問題,ASP.NET Core 為 Web 伺服器提供了一種機制,就 ...
  • 本章將和大家分享如何通過 Elasticsearch 實現自動補全查詢功能。 一、自動補全-安裝拼音分詞器 1、自動補全需求說明 當用戶在搜索框輸入字元時,我們應該提示出與該字元有關的搜索項,如圖: 2、使用拼音分詞 要實現根據字母做補全,就必須對文檔按照拼音分詞。在 GitHub 上恰好有 Ela ...
  • using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Windows.Forms; namespace OOP { pub ...
  • 概述:以上內容詳細介紹了在C#中如何從另一個線程更新GUI,包括基礎功能和高級功能。對於WinForms,使用`Control.Invoke`;對於WPF,使用`Dispatcher.Invoke`。高級功能使用`SynchronizationContext`實現線程間通信,確保清晰、可讀性高的代碼 ...
  • Nuget包 Microsoft.Extensions.Telemetry.Abstractions 包含的新的日誌記錄source generator,它支持使用[LogProperties]將整個對象作為State與日誌一起記錄。 我將展示一種方法來控制如何使用[LogProperties]對象 ...
  • 支持.Net/.Net Core/.Net Framework,可以部署在Docker, Windows, Linux, Mac。 常見的ORM技術(比如:Entity Framework,Dapper,SqlSugar,NHibernate,等…),它們不是在做Sql語句的程式化變種,就是在做Sq ...
  • 一、引言 在現代應用程式開發中,尤其是在涉及I/O操作(如網路請求、文件讀寫等)時,非同步編程成為了提高性能和用戶體驗的關鍵技術。C#作為.NET框架下的主流開發語言,提供了強大的非同步編程支持,通過async/await關鍵字,可以讓開發者以同步的方式編寫非同步代碼,極大地簡化了非同步編程的複雜性。本文將 ...
  • 一、引言 在.NET開發中,操作Office文檔(特別是Excel和Word)是一項常見的需求。然而,在伺服器端或無Microsoft Office環境的場景下,直接使用Office Interop可能會面臨挑戰。為瞭解決這個問題,開源庫NPOI應運而生,它提供了無需安裝Office即可創建、讀取和 ...