在 IIS 環境下使用 C# 操作 LibreOffice 進行檔案轉換
Web 應用要把使用者上傳的 Excel 轉成 ODS、或把報表轉成 PDF,最省事的做法之一是裝一套 LibreOffice,從後端用 soffice.exe --headless --convert-to 去跑。本機開發測試時一切正常,一部署到 IIS 就翻車:程式沒回應、找不到輸出檔、或是直接權限不足。
最容易踩進去的坑不是資料夾權限,而是「身分模擬到底有沒有作用在那支 soffice.exe 上」。網路上很多範例會教你 LogonUser 取得權杖、ImpersonateLoggedOnUser 模擬使用者,然後 Process.Start() 啟動 soffice——這套流程在 ASP.NET 裡其實達不到目的。下面先把為什麼會這樣講清楚,再給出能真正生效的寫法。
環境與會遇到的問題
需要的東西很單純:
- Windows Server 加 IIS
- LibreOffice(本文以預設安裝路徑
C:\Program Files\LibreOffice為例) - .NET Framework 的 Web 專案
部署後常見的症狀:
- 用一般帳號跑轉檔,soffice 起不來或沒有輸出
- 應用程式集區身分權限不足
- 轉檔過程卡住沒回應
- 程式跑完了,預期路徑卻找不到輸出檔
- 設了一堆權限還是失敗
為什麼 Impersonate 之後 Process.Start 沒用
這是整件事的關鍵,先講原理再寫程式。
ASP.NET 的身分模擬是執行緒層級的:ImpersonateLoggedOnUser 把模擬權杖(impersonation token)掛在「目前這條執行緒」上。但 CreateProcess(Process.Start 底層呼叫的就是它)建立子行程時,用的是宿主行程的主要權杖(primary token),而不是當前執行緒的模擬權杖。
結果就是:你在模擬狀態下呼叫 Process.Start() 啟動 soffice.exe,它仍然以 IIS 工作行程(應用程式集區身分)在跑,模擬完全沒傳遞下去。Microsoft 官方文件講得很直接:
in ASP.NET, impersonation is performed at the thread level and not at the process level. Therefore, any process that you spawn from ASP.NET will run under the context of the ASP.NET worker process and not under the impersonated context.
更糟的是,如果你照那種寫法在啟動前 log 一行「切換後身分」,它會印出被模擬的帳號名稱——因為當前執行緒確實在模擬狀態,於是你會誤以為一切正常,實際上 soffice 根本不是用那個身分跑的。這個假象會讓人把問題往資料夾權限那邊一路調到崩潰。
正確做法是用 LogonUser 拿到權杖後,先用 DuplicateTokenEx 把它從模擬權杖轉成主要權杖,再用 CreateProcessAsUser 以這個主要權杖去啟動 soffice.exe。這樣子行程才會真正帶著指定使用者的身分執行。
1. IIS 應用程式集區設定
先確認集區設定,這幾項與後面的方案不衝突:
- 開啟 IIS 管理員
- 找到你的應用程式集區(例如 DefaultAppPool)
- 右鍵 → 進階設定
- 設定:
- 載入使用者設定檔 = True
- 識別 = ApplicationPoolIdentity
這裡要先說清楚一件事,避免混淆。底下要走的是「用 Web.config 設定的具名帳號做身分模擬」這條路線,所以應用程式集區用什麼識別其實不是重點,維持預設的 ApplicationPoolIdentity 即可。真正去跑 soffice 的是你在 LogonUser 指定的那個具名帳號,不是集區身分。
換個角度:要嘛你完全靠應用程式集區身分去跑(那就把集區識別設成有權限的帳號,根本不需要 LogonUser);要嘛你走 CreateProcessAsUser 用具名帳號(那集區身分就不關鍵)。不要兩條路線混著做。本文選後者,因為它讓「轉檔用哪個帳號」這件事獨立於 IIS 集區設定,比較好控管。
2. Windows 使用者權限指派
CreateProcessAsUser 這條路線對「被模擬的那個帳號」有額外的權限要求,這點和單純 impersonation 不同,務必設對。
開啟「本機安全性原則」(Win + R 輸入 secpol.msc),依序展開:安全性設定 → 本機原則 → 使用者權限指派。
針對你要拿來跑轉檔的具名帳號(不是 IIS APPPOOL\DefaultAppPool,而是 Web.config 裡 SystemAccount 指定的那個本機帳號),加入以下權限:
- 取代處理程序層級權杖(Replace a process level token,
SeAssignPrimaryTokenPrivilege) - 調整處理程序的記憶體配額(Increase quotas,
SeIncreaseQuotaPrivilege) - 以批次工作登入(Log on as a batch job)—— 對應
LogonUser用LOGON32_LOGON_BATCH時所需
前兩項是 CreateProcessAsUser 能成功的硬性條件,少了會在 CreateProcessAsUser 失敗。第三項對應下面程式碼用的登入類型。
關於登入類型多講一句:dwLogonType 必須和你實際授予的權限一致。LOGON32_LOGON_BATCH(值 4)要求帳號有「以批次工作登入」權限;LOGON32_LOGON_INTERACTIVE(值 2)則要求「允許本機登入」。網路上不少範例宣稱要加「以批次工作登入」,程式碼卻傳 LOGON32_LOGON_INTERACTIVE,兩邊對不上時 LogonUser 會以錯誤碼 1385(ERROR_LOGON_TYPE_NOT_GRANTED)失敗。本文程式碼統一用 LOGON32_LOGON_BATCH,搭配上面的「以批次工作登入」,服務情境下這組最合適。
3. 資料夾權限設定
為具名帳號設定資料夾權限(讀取、寫入、執行):
- 在資料夾按右鍵 → 內容 → 安全性 → 編輯
- 新增該帳號(例如
.\svc_libreoffice) - 勾選讀取、寫入、修改
需要設定的資料夾:
- LibreOffice 安裝目錄(通常是
C:\Program Files\LibreOffice) - 來源資料夾
- 目標資料夾
- 暫存資料夾(例如
D:\temp) - 給這個帳號一個專屬的 LibreOffice user profile 目錄(下面會解釋為什麼需要)
4. 程式碼實作
下面是修正過的版本:LogonUser 取得權杖 → DuplicateTokenEx 轉成主要權杖 → CreateProcessAsUser 以該身分啟動 soffice.exe,並把輸出與錯誤導向記錄檔。
1 | using System; |
幾個和原始坑直接相關的修正點:
- **改用
CreateProcessAsUser**:這是讓 soffice 真正以具名帳號跑的唯一可靠途徑,Process.Start在模擬情境下做不到。 - 濾鏡與副檔名分開處理:
--convert-to的filter參數可以只給副檔名(ods),也可以帶完整濾鏡名稱(xlsx:Calc MS Excel 2007 XML、csv:Text - txt - csv (StarCalc))。輸出檔的副檔名只由冒號前那段決定,所以推導預期檔名時要先切掉冒號後面,直接把整串接在檔名後一定找不到檔。 - 每次給獨立 user profile:
-env:UserInstallation指向每次唯一的目錄,避免多個轉檔同時跑時共用同一份 profile 互相鎖死(這在 Windows Server 上呼叫多次 soffice 卡住的情形很常見)。 - 加逾時保護:
WaitForSingleObject帶逾時,soffice 萬一卡住不會把整個工作行程拖著一起死。
5. Web.config 設定與密碼加密
帳號設定放在 appSettings:
1 | <configuration> |
這裡有個一定要提醒的安全問題:不要把帳號密碼用明文丟在 Web.config 裡就上線。Web.config 很容易被一起 commit 進版控、被同事或備份帶走,IIS 設定一外洩等於帳密一起外洩。
正式環境請用 ASP.NET 內建的設定保護(protected configuration)把 appSettings 區段加密。在伺服器上用 aspnet_regiis 加密:
1 | aspnet_regiis -pef "appSettings" "C:\inetpub\wwwroot\YourApp" -prov "DataProtectionConfigurationProvider" |
DataProtectionConfigurationProvider走的是 Windows DPAPI,加密金鑰綁機器,適合單機部署。- 若是 Web farm 多台機器共用設定,改用
RsaProtectedConfigurationProvider並把 RSA 金鑰容器匯出到各台機器。
加密後 appSettings 在檔案裡是密文,但程式照 ConfigurationManager.AppSettings["SystemAccount"] 讀取時,ASP.NET 會自動解密,程式碼一行都不用改。
更乾淨的做法是連密碼都不要落地——改用 Windows 群組受管理服務帳戶(gMSA),讓集區直接以該身分跑,連 SystemPassword 都省掉。本文為了示範 CreateProcessAsUser 的完整流程保留具名帳號方案,但實務上 gMSA 是更省心的選擇。
6. 使用方式
1 | // 轉成 ODS |
完整檢查清單
IIS 設定
- 應用程式集區「載入使用者設定檔」= True
- 集區識別維持 ApplicationPoolIdentity(轉檔身分另由具名帳號負責)
具名帳號的使用者權限指派(針對
SystemAccount)- 取代處理程序層級權杖
- 調整處理程序的記憶體配額
- 以批次工作登入(對應程式碼的
LOGON32_LOGON_BATCH)
資料夾權限(讀寫執行,給具名帳號)
- LibreOffice 安裝目錄
- 來源 / 目標資料夾
- 暫存資料夾
- LibreOffice user profile 根目錄
Web.config
- SystemAccount / SystemPassword 設定正確
- 上線前已用 aspnet_regiis 加密 appSettings(不留明文)
常見問題排除
程式跑完卻沒輸出、log 顯示身分已切換
檢查你是不是還在用Process.Start。那條路線在 ASP.NET 下不會把模擬傳給子行程,soffice 仍以集區身分跑。改用DuplicateTokenEx加CreateProcessAsUser。CreateProcessAsUser回傳 false,錯誤碼 1314
1314 是ERROR_PRIVILEGE_NOT_HELD,代表具名帳號缺「取代處理程序層級權杖」或「調整處理程序的記憶體配額」。回到第 2 步補齊。LogonUser失敗,錯誤碼 1385ERROR_LOGON_TYPE_NOT_GRANTED,登入類型和帳號被授予的權限對不上。本文用LOGON32_LOGON_BATCH,就要確認帳號有「以批次工作登入」。多次轉檔時 soffice 卡住沒回應
多半是共用 user profile 造成鎖定。確認每次都用獨立的-env:UserInstallation,並給逾時保護。找不到輸出檔,但檔案其實有產生
檢查預期檔名的推導:濾鏡若帶冒號(如xlsx:...),副檔名只取冒號前那段,別把整串接在檔名後。
結論
這篇要修正的核心觀念只有一個:在 ASP.NET 裡,ImpersonateLoggedOnUser 加 Process.Start 不會讓子行程帶著被模擬的身分跑,因為模擬作用在執行緒、子行程拿的是宿主行程的主要權杖。要讓 soffice.exe 真的以指定帳號執行,必須走 LogonUser → DuplicateTokenEx(轉主要權杖)→ CreateProcessAsUser,並替這個帳號補上「取代處理程序層級權杖」「調整處理程序的記憶體配額」兩項權限。
其餘像濾鏡決定副檔名、每次給獨立 profile、密碼別明文落地,都是部署到真實環境後遲早會撞上的細節。把這些一次處理掉,IIS 上的 LibreOffice 轉檔才會穩定可靠。




