有时候,我们的应用程序功能和业务组成比较复杂,需要使用的组件、子程序众多,可能牵涉到桌面客户端程序,bs网页端程序,甚至还有桌面内嵌web的应用程序组成形式。
我们在执行应用程序过程中,登录的Windows操作系统的用户角色也不尽相同,用户可能多数是使用的管理员组的普通用户账户登录,开发人员有时候需要提权,使用的是Administrator账户。
应用程序在执行时如果有内嵌web功能,比如网页版的剪贴板,截图工具等功能,可能启动该应用程序时,可能只需要使用当前用户所属的”普通“权限即可(超管的普通权限就是超管权限;普通用户的普通权限就是普通权限,与此相对的是普通用户的右键”以管理员身份运行“)。
但是到了某些具体的业务环节时,比如操作注册COM组件之类的,可能有时需要使用管理员权限。如果此时是使用的普通账户登录的,这个时候就需要在代码功能实现里动态对此功能执行时进行提权,否则会操作不成功。
一般我们代码操作注册某些组件可以直接使用Process方式。需要提权时在Process的参数类ProcessStartInfo的Verb属性指定为”runas“即可。
当然碰到上面描述的场景,我们第一想法就是指定”runas“,但这样打包出来的程序会带有小盾牌,打开应用程序exe时会弹框要求提权,这样又影响了其他普通权限的功能,可能又得考虑降权,改动量较大。这不是一个好的方式。
有人说需要配置app.manifest里requestedPrivileges 节点的requestedExecutionLevel节 属性level值为requireAdministrator,试验了下,同样会带有小盾牌,执行应用程序exe就让提权,此方案走不通。
后来找到了Windows底层的一个函数,ShellExecuteEx,解决了此问题。
中间还走了一个弯路,不区分当前操作系统登录用户角色身份,都按照ShellExecuteEx方式进行处理。测试人员在使用时,当操作系统登录用户为Administrator时,发现ShellExecuteEx方法执行报错,其实可想而知,超级管理员已经是最高权限了,再提权实际没有意义。
综合Process、ShellExecuteEx,以及Administrator和普通用户的使用场景,找到了一个折中处理方案,可能不是最好的,如有更好思路,欢迎各路大神提供参考意见。现把整体解决思路整理如下。
0.打开应用程序exe时不设置任何提权操作代码功能。动态判断当前Windows用户的角色,如果是Administrator,那么他的权限已经足够使用,在执行COM时直接使用Process方式,它的参数类ProcessStartInfo的Verb默认为空即可。
如果是普通用户(带有管理员组权限),那么他在执行其他业务时,使用普通权限即可(这样不影响一些内嵌网页组件程序工作),当执行COM时调用ShellExecuteEx进行动态提权,以保证COM组件执行成功。如果执行失败,可以参考我之前的帖子来获取具体执行失败的原因,帖子地址:https://www.cnblogs.com/jeff151013/p/16080757.html。
1.辅助方法,获取当前Windows登录用户角色身份
using System.Security.Principal;
public static class UserRoleCheckUtils
{
public static bool? OsCurrentUserIsAdministrator(out Exception ex)
{
bool? isAdministrator = null;
ex = null;
try
{
var windowsIdentity = WindowsIdentity.GetCurrent();
var windowsPrincipal = new WindowsPrincipal(windowsIdentity);
isAdministrator = windowsPrincipal.IsInRole(WindowsBuiltInRole.Administrator);
}
catch (Exception exception)
{
ex = exception;
}
return isAdministrator;
}
}
2.工具类1 ShellUtils,上代码
using System.Runtime.InteropServices;
public class ShellUtils
{
#region ShellExecute
public enum ShowCommands : int
{
SW_HIDE = 0,
SW_SHOWNORMAL = 1,
SW_NORMAL = 1,
SW_SHOWMINIMIZED = 2,
SW_SHOWMAXIMIZED = 3,
SW_MAXIMIZE = 3,
SW_SHOWNOACTIVATE = 4,
SW_SHOW = 5,
SW_MINIMIZE = 6,
SW_SHOWMINNOACTIVE = 7,
SW_SHOWNA = 8,
SW_RESTORE = 9,
SW_SHOWDEFAULT = 10,
SW_FORCEMINIMIZE = 11,
SW_MAX = 11
}
[DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern IntPtr ShellExecute(
IntPtr hwnd,
string lpOperation,
string lpFile,
string lpParameters,
string lpDirectory,
ShowCommands nShowCmd);
#endregion
#region ShellExecuteEx
private const int SwShow = 5;
private const uint SeeMaskInvokeidlist = 12;
/// <summary>
/// 对指定应用程序执行某个操作
/// </summary>
/// <param name="lpExecInfo"></param>
/// <returns></returns>
[DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool ShellExecuteEx(ref Shellexecuteinfo lpExecInfo);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct Shellexecuteinfo
{
/// <summary>
/// 结构大小,以字节为单位
/// </summary>
public int cbSize;
/// <summary>
/// 一个标志数组,用来设置其他成员的有效性。
/// </summary>
public uint fMask;
/// <summary>
/// 可选。执行ShellExecuteEx的窗口句柄,可设为NULL。
/// </summary>
public IntPtr hwnd;
/// <summary>
/// 指定执行的动作,包括:edit ,explore ,find ,open,print, properties
/// </summary>
[MarshalAs(UnmanagedType.LPTStr)]
public string lpVerb;
/// <summary>
/// 以\0 结尾的字符串,指出 lpVerb 的操作对象的路径,被系统支持的操作包括文本的 open 、 print等
/// </summary>
[MarshalAs(UnmanagedType.LPTStr)]
public string lpFile;
/// <summary>
/// 可选。运行/打开程序的参数,如果打开的是一个文档,则该项无效
/// </summary>
[MarshalAs(UnmanagedType.LPTStr)]
public string lpParameters;
/// <summary>
/// 可选。指明工作目录的名字,成员没有说明,则默认为当前目录
/// </summary>
[MarshalAs(UnmanagedType.LPTStr)]
public string lpDirectory;
/// <summary>
/// 必须。指定打开的程序的显示方式,为SW_值中的一个。
/// </summary>
public int nShow;
/// <summary>
/// 【out】如果设置SEE_MASK_NOCLOSEPROCESS S值并且ShellExecuteEx 调用成功,则该项的值大于32,如果调用失败,则将设置为 SE_ERR_XXX 的错误值。
/// </summary>
public IntPtr hInstApp;
/// <summary>
/// 一个ITEMIDLIST结构的地址,用来存储成员的特别标识符,当fMask不包括SEE_MASK_IDLIST或SEE_MASK_INVOKEIDLIST时该项被忽略
/// </summary>
public IntPtr lpIDList;
/// <summary>
/// 用以指明文件类别的名字或GUID,当fMask不包括SEE_MASK_CLASSNAME时该项被忽略
/// </summary>
[MarshalAs(UnmanagedType.LPTStr)]
public string lpClass;
/// <summary>
/// 获得已在系统注册的文件类型的Handle,当fMask不包括SEE_MASK_HOTKEY时该项被忽略
/// </summary>
public IntPtr hkeyClass;
/// <summary>
/// 程序的热键关联,低位存储虚拟关键码(Key Code),高位存储修改标志位(HOTKEYF_),修改标志为(modifier flags)的详细列表请看WM_SETHOTKEY消息的描述,当fmask不包括SEE_MASK_HOTKEY时该项被忽略
/// </summary>
public uint dwHotKey;
/// <summary>
/// 取得对应文件类型的图标的Handle,当fMask不包括SEE_MASK_ICON时该项被忽略
/// </summary>
public IntPtr hIcon;
/// <summary>
/// 指向新启动的程序的句柄。若fMask不设为SEE_MASK_NOCLOSEPROCESS则该项值为NULL。但若程序没有启动,即使fMask设为SEE_MASK_NOCLOSEPROCESS,该值也仍为NULL。
/// </summary>
public IntPtr hProcess;
}
public static bool Execute(string verb, string fileFullPath, string arguments)
{
ShellUtils.Shellexecuteinfo info = new ShellUtils.Shellexecuteinfo();
info.cbSize = Marshal.SizeOf(info);
info.fMask = SeeMaskInvokeidlist;
info.lpVerb = verb;
info.lpFile = fileFullPath;
info.lpParameters = arguments;
info.nShow = SwShow;
return ShellExecuteEx(ref info);
}
#endregion
}
3.工具类2 AddinHelper,主要是提供操作系统登录用户角色判断、Process方法调用和ShellUtils.Execute方法的调用
{
public static bool RegisterAddIn(string dllPath, bool isBit64)
{
string winDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
string regasmPath;
if (isBit64)
{
regasmPath = Path.Combine(winDir, @"Microsoft.NET\Framework64\v4.0.30319\RegAsm.exe");
}
else
{
regasmPath = Path.Combine(winDir, @"Microsoft.NET\Framework\v4.0.30319\RegAsm.exe");
}
//获取当前操作系统登录用户角色是否是超级管理员
var osCurrentUserIsAdministrator = UserRoleCheckUtils.OsCurrentUserIsAdministrator(out Exception ex);
//如果是超管、使用process方式注册;否则使用ShellExecuteEx方式
if (osCurrentUserIsAdministrator.HasValue && osCurrentUserIsAdministrator.Value)
{
return UseProcessOperateAddIn(regasmPath, " /codebase \"" + dllPath + "\"");
}
//如果获取用户身份失败或者是普通用户,将提权注册插件
return ShellUtils.Execute("runas", regasmPath, dllPath);
}
/// <summary>
/// 操作COM插件
/// </summary>
/// <param name="regasmPath"></param>
/// <param name="dllPath">含其他操作命令</param>
/// <returns></returns>
public static bool UseProcessOperateAddIn(string regasmPath, string dllPath)
{
try
{
var startInfo = new ProcessStartInfo();
startInfo.Verb = string.Empty;
startInfo.FileName = regasmPath;
startInfo.Arguments = dllPath;
startInfo.WindowStyle = ProcessWindowStyle.Normal;
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardInput = true;
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true; //让窗体不显示
startInfo.ErrorDialog = false;
Process process = Process.Start(startInfo);
process.EnableRaisingEvents = true;
process.WaitForExit();
StreamReader reader = process.StandardOutput; //截取输出流
string output = process.StandardError.ReadToEnd();
var rtn = process.ExitCode;
if (rtn != 0)
throw new Exception(output);
return true;
}
catch (Exception ex)
{
return false;
}
}
}