Files
SRCC/docs/admin-command-execution.md
2026-06-22 11:33:18 +08:00

9.1 KiB

管理员权限执行指令

适用于 Windows 平台,检查是否以管理员身份运行及提权执行命令


1. 检测当前是否以管理员运行

package main

import (
    "fmt"
    "os/user"
    "runtime"
    "syscall"
    "unsafe"
)

var (
    shell32         = syscall.NewLazyDLL("shell32.dll")
    isUserAnAdmin   = shell32.NewProc("IsUserAnAdmin")
)

func isAdmin() bool {
    if runtime.GOOS != "windows" {
        // Linux/macOS: 检测 UID 是否为 0
        u, err := user.Current()
        if err != nil {
            return false
        }
        return u.Uid == "0"
    }

    ret, _, _ := isUserAnAdmin.Call()
    return ret != 0
}

func main() {
    if isAdmin() {
        fmt.Println("当前以管理员身份运行")
    } else {
        fmt.Println("当前以普通用户身份运行")
    }
}

2. 以管理员身份重新启动自身

func runAsAdmin() error {
    if runtime.GOOS != "windows" {
        return fmt.Errorf("此功能仅支持 Windows")
    }

    exe, _ := os.Executable()
    verb := "runas"
    cwd, _ := os.Getwd()

    verbPtr, _ := syscall.UTF16PtrFromString(verb)
    exePtr, _ := syscall.UTF16PtrFromString(exe)
    cwdPtr, _ := syscall.UTF16PtrFromString(cwd)

    var showCmd int32 = 1 // SW_NORMAL

    return syscall.ShellExecute(0, verbPtr, exePtr, nil, cwdPtr, showCmd)
}

// 使用方式:检测到非管理员时提权重启
func ensureAdmin() {
    if !isAdmin() {
        fmt.Println("需要管理员权限,正在提权...")
        runAsAdmin()
        os.Exit(0)
    }
}

注意: runAsAdmin 会弹出 UAC 弹窗,用户需点击"是"确认。


3. 以管理员身份执行任意程序

func runAsAdminExe(targetExe string, args string) error {
    verbPtr, _ := syscall.UTF16PtrFromString("runas")
    exePtr, _ := syscall.UTF16PtrFromString(targetExe)
    // args 传入时是 lpParameters,填充启动参数
    paramsPtr, _ := syscall.UTF16PtrFromString(args)
    dirPtr, _ := syscall.UTF16PtrFromString("")

    var showCmd int32 = 1
    return syscall.ShellExecute(0, verbPtr, exePtr, paramsPtr, dirPtr, showCmd)
}

4. 使用 PowerShell 管理员执行命令

func runAsAdminPSCommand(command string) *exec.Cmd {
    // 组装 PowerShell 命令,-Verb RunAs 触发 UAC 提权
    psCmd := fmt.Sprintf(
        `Start-Process PowerShell -Verb RunAs -ArgumentList '-NoProfile -Command "%s"'`,
        strings.ReplaceAll(command, `"`, `\"`),
    )
    return exec.Command("powershell", "-Command", psCmd)
}

缺点: Start-Process 会开启新窗口,无法直接获取输出。如需获取输出,见下方方式。


5. 管理员执行命令 + 获取输出

方式 A:不走 PowerShell,纯 ShellExecuteEx + cmd.exe

func runAsAdminCmd(command string) (string, error) {
    // 输出重定向到临时文件
    tmpFile := filepath.Join(os.TempDir(),
        fmt.Sprintf("cmdout_%d.txt", os.Getpid()))

    // 拼命令: cmd /c "your_command > tmpFile 2>&1"
    fullCmd := fmt.Sprintf(`%s > "%s" 2>&1`, command, tmpFile)
    cmdLine, _ := syscall.UTF16PtrFromString(`/c ` + fullCmd)

    var op uint32
    shell32 := syscall.NewLazyDLL("shell32.dll")
    procShellExecuteEx := shell32.NewProc("ShellExecuteExW")

    // SEE_MASK_NOCLOSEPROCESS = 0x40 — 获取进程句柄以便等待
    type SHELLEXECUTEINFO struct {
        cbSize       uint32
        fMask        uint32
        hwnd         syscall.Handle
        lpVerb       *uint16
        lpFile       *uint16
        lpParameters *uint16
        lpDirectory  *uint16
        nShow        int32
        hInstApp     uintptr
        lpIDList     uintptr
        lpClass      *uint16
        hkeyClass    syscall.Handle
        dwHotKey     uint32
        hIcon        syscall.Handle
        hProcess     syscall.Handle
    }

    verb, _ := syscall.UTF16PtrFromString("runas")
    cmdexe, _ := syscall.UTF16PtrFromString("cmd.exe")

    info := SHELLEXECUTEINFO{
        cbSize:       uint32(unsafe.Sizeof(SHELLEXECUTEINFO{})),
        fMask:        0x40, // SEE_MASK_NOCLOSEPROCESS
        lpVerb:       verb,
        lpFile:       cmdexe,
        lpParameters: cmdLine,
        nShow:        0, // SW_HIDE 隐藏窗口
    }

    ret, _, _ := procShellExecuteEx.Call(uintptr(unsafe.Pointer(&info)))
    if ret == 0 {
        return "", fmt.Errorf("ShellExecuteEx 失败")
    }
    defer syscall.CloseHandle(info.hProcess)

    // 等待进程结束
    syscall.WaitForSingleObject(info.hProcess, syscall.INFINITE)

    // 读取输出文件
    data, err := os.ReadFile(tmpFile)
    os.Remove(tmpFile)
    if err != nil {
        return "", err
    }
    return string(data), nil
}

关键点:

  • SEE_MASK_NOCLOSEPROCESS (0x40) — 获取进程句柄
  • WaitForSingleObject — 等待提权后的 cmd.exe 执行完毕
  • SW_HIDE (0) — 隐藏 cmd 黑窗口
  • > tmpFile 2>&1 — stdout 和 stderr 都重定向到临时文件
  • 无需 PowerShell,纯 Win32 API

方式 B:PowerShell 方式(兼容性好,但依赖 PS)

func runAsAdminWithPS(command string) (string, error) {
    tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("cmdout_%d.txt", os.Getpid()))

    psCmd := fmt.Sprintf(
        `Start-Process cmd -Verb RunAs -Wait -WindowStyle Hidden -RedirectStandardOutput "%s" -ArgumentList '/c %s'`,
        tmpFile, command,
    )

    exec.Command("powershell", "-Command", psCmd).Run()

    data, err := os.ReadFile(tmpFile)
    os.Remove(tmpFile)
    return string(data), err
}

6. 完整示例:Fyne GUI 管理员指令执行器

package main

import (
    "fmt"
    "os"
    "path/filepath"
    "runtime"
    "sync"
    "syscall"
    "unsafe"

    "fyne.io/fyne/v2"
    "fyne.io/fyne/v2/app"
    "fyne.io/fyne/v2/container"
    "fyne.io/fyne/v2/widget"
)

var isAdmin bool

func init() {
    if runtime.GOOS == "windows" {
        shell32 := syscall.NewLazyDLL("shell32.dll")
        proc := shell32.NewProc("IsUserAnAdmin")
        ret, _, _ := proc.Call()
        isAdmin = ret != 0
    }
}

func runAdminCmd(command string) (string, error) {
    tmpFile := filepath.Join(os.TempDir(),
        fmt.Sprintf("srm_admin_out_%d.txt", os.Getpid()))

    fullCmd := fmt.Sprintf(`%s > "%s" 2>&1`, command, tmpFile)
    cmdLine, _ := syscall.UTF16PtrFromString(`/c ` + fullCmd)

    type SHELLEXECUTEINFO struct {
        cbSize, fMask      uint32
        hwnd               syscall.Handle
        lpVerb, lpFile     *uint16
        lpParameters       *uint16
        lpDirectory, _     *uint16
        nShow              int32
        _, _, _, _         uintptr
        hProcess           syscall.Handle
    }

    verb, _ := syscall.UTF16PtrFromString("runas")
    cmdexe, _ := syscall.UTF16PtrFromString("cmd.exe")

    info := SHELLEXECUTEINFO{
        cbSize:       uint32(unsafe.Sizeof(SHELLEXECUTEINFO{})),
        fMask:        0x40, // SEE_MASK_NOCLOSEPROCESS
        lpVerb:       verb,
        lpFile:       cmdexe,
        lpParameters: cmdLine,
        nShow:        0, // SW_HIDE
    }

    proc := syscall.NewLazyDLL("shell32.dll").NewProc("ShellExecuteExW")
    ret, _, _ := proc.Call(uintptr(unsafe.Pointer(&info)))
    if ret == 0 {
        return "", fmt.Errorf("ShellExecuteEx 失败")
    }
    defer syscall.CloseHandle(info.hProcess)

    syscall.WaitForSingleObject(info.hProcess, syscall.INFINITE)

    data, err := os.ReadFile(tmpFile)
    os.Remove(tmpFile)
    return string(data), err
}

func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("管理员指令执行器")
    myWindow.Resize(fyne.NewSize(600, 400))

    var mu sync.Mutex

    cmdEntry := widget.NewEntry()
    cmdEntry.SetPlaceHolder("输入命令,如: ipconfig /flushdns")

    statusLabel := widget.NewLabel("当前状态: 用户")
    if isAdmin {
        statusLabel.SetText("当前状态: 管理员 ✓")
    }

    output := widget.NewMultiLineEntry()
    output.Disable()

    progress := widget.NewProgressBarInfinite()
    progress.Hide()

    runBtn := widget.NewButton("以管理员身份执行", func() {
        command := cmdEntry.Text
        if command == "" {
            return
        }
        go func() {
            mu.Lock()
            progress.Show()
            runBtn.Disable()
            mu.Unlock()

            out, err := runAdminCmd(command)

            mu.Lock()
            progress.Hide()
            runBtn.Enable()
            mu.Unlock()

            if err != nil {
                output.SetText("错误: " + err.Error())
            } else {
                output.SetText(out)
            }
        }()
    })

    content := container.NewVBox(
        statusLabel,
        widget.NewSeparator(),
        cmdEntry,
        runBtn,
        progress,
        widget.NewLabel("输出:"),
        output,
    )

    myWindow.SetContent(content)
    myWindow.ShowAndRun()
}

7. 方法对比

方式 优点 缺点
ShellExecute("runas") 原生 API,稳定 只能启动 exe,无法获取输出
PowerShell Start-Process -Verb RunAs -RedirectStandardOutput 可获取输出 需临时文件,略慢
直接加 go.mod 嵌入 Manifest 程序启动时即提权 始终以管理员运行,不够灵活