在 Web 應用程式中透過按鈕觸發伺服器重啟是高風險操作,涉及 P/Invoke 呼叫 Windows API、權限提升、安全性考量。本文完整解析實作細節、必要權限與生產環境替代方案。
透過 P/Invoke 呼叫 advapi32.dll 與 user32.dll 中的 API:
OpenProcessToken:取得進程存取權杖LookupPrivilegeValue:查詢 SeShutdownPrivilege 權限 LUIDAdjustTokenPrivileges:啟用關機權限ExitWindowsEx / InitiateSystemShutdown:執行重啟/關機using System;
using System.Runtime.InteropServices;
using System.Web.UI.WebControls;
public partial class ServerRestart : System.Web.UI.Page
{
// 權限常數
private const int SE_PRIVILEGE_ENABLED = 0x2;
private const int TOKEN_QUERY = 0x8;
private const int TOKEN_ADJUST_PRIVILEGES = 0x20;
private const string SE_SHUTDOWN_NAME = "SeShutdownPrivilege";
private const int EWX_REBOOT = 0x2;
private const int EWX_FORCE = 0x4;
private const int EWX_FORCEIFHUNG = 0x10;
// 結構定義
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct LUID_AND_ATTRIBUTES
{
public long Luid;
public int Attributes;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct TOKEN_PRIVILEGES
{
public int PrivilegeCount;
public LUID_AND_ATTRIBUTES Privileges;
}
// P/Invoke 宣告
[DllImport("kernel32.dll", ExactSpelling = true)]
private static extern IntPtr GetCurrentProcess();
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool OpenProcessToken(IntPtr processHandle, int desiredAccess, out IntPtr tokenHandle);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern bool LookupPrivilegeValue(string systemName, string name, out long luid);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool AdjustTokenPrivileges(IntPtr tokenHandle, bool disableAllPrivileges,
ref TOKEN_PRIVILEGES newState, int bufferLength, IntPtr previousState, IntPtr returnLength);
[DllImport("user32.dll", SetLastError = true, ExactSpelling = true)]
private static extern bool ExitWindowsEx(int flags, int reason);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern bool InitiateSystemShutdown(string machineName, string message,
int timeout, bool forceAppsClosed, bool rebootAfterShutdown);
// 啟用關機權限
private static void EnableShutdownPrivilege()
{
IntPtr processHandle = GetCurrentProcess();
if (!OpenProcessToken(processHandle, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out IntPtr tokenHandle))
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
try
{
LookupPrivilegeValue(null, SE_SHUTDOWN_NAME, out long luid);
var tp = new TOKEN_PRIVILEGES
{
PrivilegeCount = 1,
Privileges = new LUID_AND_ATTRIBUTES { Luid = luid, Attributes = SE_PRIVILEGE_ENABLED }
};
if (!AdjustTokenPrivileges(tokenHandle, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero))
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
}
finally
{
if (tokenHandle != IntPtr.Zero)
CloseHandle(tokenHandle);
}
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
// 強制重啟
public static void ForceReboot()
{
EnableShutdownPrivilege();
if (!ExitWindowsEx(EWX_REBOOT | EWX_FORCE | EWX_FORCEIFHUNG, 0))
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
}
// 遠端/排程關機(可指定訊息、延遲)
public static void ScheduleShutdown(string message, int timeoutSeconds, bool reboot)
{
EnableShutdownPrivilege();
if (!InitiateSystemShutdown(null, message, timeoutSeconds, true, reboot))
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
}
// 網頁按鈕事件
protected void btnReboot_Click(object sender, EventArgs e)
{
try
{
// 記錄審計日誌
AuditLog.Write("Server reboot initiated by web user", User.Identity.Name);
// 立即強制重啟
ForceReboot();
// 或排程重啟(給使用者 60 秒存檔時間)
// ScheduleShutdown("系統維護重啟,請儲存工作", 60, true);
}
catch (Exception ex)
{
lblMessage.Text = $"重啟失敗:{ex.Message}";
AuditLog.Write($"Reboot failed: {ex}", User.Identity.Name);
}
}
}
| 需求 | 說明 |
|---|---|
| IIS 應用程式集區身分識別 | 必須具備 SeShutdownPrivilege,通常需以 Local System 或具備該權限的服務帳號執行 |
| 本機安全性原則 | secpol.msc → 本機原則 → 使用者權利指派 → 「強制從遠端系統關機」/「關閉系統」加入該帳號 |
| UAC 與 Session 0 隔離 | Vista+ 服務在 Session 0 執行,ExitWindowsEx 可能無效,需用 InitiateSystemShutdown |
| 防火牆/群組原則 | 遠端關機需開啟 RPC/WMI 連接埠,或受群組原則限制 |
InitiateSystemShutdown 給使用者緩衝時間(如 60-300 秒)// PowerShell Remoting(推薦用於自動化運維)
Invoke-Command -ComputerName SERVER01 -ScriptBlock { Restart-Computer -Force }
// WMI/CIM
(Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName SERVER01).Win32Shutdown(6)
// 系統管理工具:SCCM、PDQ Deploy、Ansible、SaltStack 等
總結:技術上可行,但生產環境極不建議直接在網頁實作伺服器重啟。應採用專門的運維自動化工具,配合嚴格的權限控制與審計流程。