首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# ASP.NET 網頁強制重啟伺服器實作與權限解析

     編輯:關於C#

在 Web 應用程式中透過按鈕觸發伺服器重啟是高風險操作,涉及 P/Invoke 呼叫 Windows API、權限提升、安全性考量。本文完整解析實作細節、必要權限與生產環境替代方案。

核心實作原理

透過 P/Invoke 呼叫 advapi32.dll 與 user32.dll 中的 API:

  1. OpenProcessToken:取得進程存取權杖
  2. LookupPrivilegeValue:查詢 SeShutdownPrivilege 權限 LUID
  3. AdjustTokenPrivileges:啟用關機權限
  4. 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 連接埠,或受群組原則限制

🛡️ 生產環境安全建議

  • 多重確認機制:需輸入驗證碼、雙人授權、IP 白名單
  • 審計日誌:記錄發起者、時間、原因、結果
  • 延遲執行:使用 InitiateSystemShutdown 給使用者緩衝時間(如 60-300 秒)
  • 角色限制:僅限特定管理員角色存取該頁面
  • 替代方案:考慮使用 Windows 排程工作、SCCM、Ansible、PowerShell Remoting 等運維工具,而非網頁直接觸發

現代替代方案

// 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 等

總結:技術上可行,但生產環境極不建議直接在網頁實作伺服器重啟。應採用專門的運維自動化工具,配合嚴格的權限控制與審計流程。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved