ClickOnce安装的时候,如果使用私有CA进行部署,首先需要将CA添加到受信任的根证书发布机构,然后将发布位置添加到【受信任的站点】区域。

手动操作比较繁琐,所以我写了一个工具用来检测、准备环境。当然所有功能用批处理或者PowerShell也一样可以实现。

项目功能不复杂,我就直接新建一个WPF程序,把所有代码都写到MainWindow.cs里了。

首先,由于这两个操作需要管理员权限,所以在程序运行时要向用户请求管理员程序。方法是添加一个应用程序清单文件,修改小节的level属性为requireAdministrator,以下是app.manifest文件的一部分。


  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <!-- UAC 清单选项
             如果想要更改 Windows 用户帐户控制级别,请使用
             以下节点之一替换 requestedExecutionLevel 节点。
        <requestedExecutionLevel  level="asInvoker" uiAccess="false" />
        <requestedExecutionLevel  level="requireAdministrator" uiAccess="false" />
        <requestedExecutionLevel  level="highestAvailable" uiAccess="false" />
            指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
            如果你的应用程序需要此虚拟化来实现向后兼容性,则移除此
            元素。
        -->
        <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>

接下来,向MainWindow主窗口添加功能按钮:环境检测,安装根证书,添加信任站点,安装插件。同时添加两个指示项,告知当前环境的状态。最后是一个保存日志的TextBox。


<Window x:Class="ExcelAddinInstaller.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ExcelAddinInstaller"
        mc:Ignorable="d"
         Title="XXXX Excel增强插件 - 安装工具" Height="500" Width="620" MinHeight="460" MinWidth="560"
         Background="#F3F6FB">
    <Window.Resources>
        <Style x:Key="ActionButtonStyle" TargetType="Button">
            <Setter Property="Height" Value="38" />
            <Setter Property="Margin" Value="0,0,0,10" />
            <Setter Property="FontSize" Value="14" />
            <Setter Property="Foreground" Value="White" />
            <Setter Property="Background" Value="#2F6FEB" />
            <Setter Property="BorderBrush" Value="#2F6FEB" />
            <Setter Property="Cursor" Value="Hand" />
        </Style>
    </Window.Resources>
    <Grid Margin="16">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <UniformGrid Grid.Row="0" Columns="4" Margin="0,0,0,8">
            <Button Style="{StaticResource ActionButtonStyle}"
                    Margin="0,0,10,10"
                    Content="环境检测"
                    Click="CheckOnlyButton_Click" />
            <Button Style="{StaticResource ActionButtonStyle}"
                    Margin="0,0,10,10"
                    Content="安装根证书"
                    Click="InstallCaButton_Click" />
            <Button Style="{StaticResource ActionButtonStyle}"
                    Margin="0,0,10,10"
                    Content="添加信任站点"
                    Click="AddTrustedSiteButton_Click" />
            <Button Style="{StaticResource ActionButtonStyle}"
                    Margin="0,0,0,10"
                    Content="安装插件"
                    Click="InstallButton_Click" />
        </UniformGrid>
        <Border Grid.Row="1" Background="White" BorderBrush="#D0D7E2" BorderThickness="1" CornerRadius="6" Padding="10" Margin="0,0,0,10">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="12" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <Border Grid.Column="0" Background="#F8FAFC" BorderBrush="#E5E7EB" BorderThickness="1" CornerRadius="6" Padding="10">
                    <StackPanel>
                        <TextBlock Text="CA 证书" FontWeight="SemiBold" Foreground="#334155" Margin="0,0,0,6" />
                        <StackPanel Orientation="Horizontal">
                            <TextBlock x:Name="CaStatusIcon" Text="●" FontSize="16" Foreground="#6B7280" Margin="0,0,6,0" />
                            <TextBlock x:Name="CaStatusText" Text="等待检测" Foreground="#6B7280" />
                        </StackPanel>
                    </StackPanel>
                </Border>
                <Border Grid.Column="2" Background="#F8FAFC" BorderBrush="#E5E7EB" BorderThickness="1" CornerRadius="6" Padding="10">
                    <StackPanel>
                        <TextBlock Text="受信任站点" FontWeight="SemiBold" Foreground="#334155" Margin="0,0,0,6" />
                        <StackPanel Orientation="Horizontal">
                            <TextBlock x:Name="SiteStatusIcon" Text="●" FontSize="16" Foreground="#6B7280" Margin="0,0,6,0" />
                            <TextBlock x:Name="SiteStatusText" Text="等待检测" Foreground="#6B7280" />
                        </StackPanel>
                    </StackPanel>
                </Border>
            </Grid>
        </Border>
        <Border Grid.Row="2" Background="White" BorderBrush="#D0D7E2" BorderThickness="1" CornerRadius="6">
            <TextBox x:Name="LogTextBox"
                     Margin="8"
                     IsReadOnly="True"
                     BorderThickness="0"
                     Background="Transparent"
                     FontFamily="Consolas"
                     FontSize="13"
                     TextWrapping="Wrap"
                     VerticalScrollBarVisibility="Auto"
                     HorizontalScrollBarVisibility="Disabled"
                     AcceptsReturn="True" />
        </Border>
    </Grid>
</Window>

最后是MainWindow.cs。我把CA证书直接放到代码里了,也可以放到指定位置用HttpClient下载。


using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Windows;

namespace ExcelAddinInstaller
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        private const int URLZONE_TRUSTED = 2;
        private const string TrustedSiteUrl = "https://192.168.0.101";
        private const string TrustedSiteHost = "192.168.0.101";
        /// <summary>
        /// VSTO 安装程序的下载地址
        /// </summary>
        private const string UrlExe = "https://192.168.0.101/updates/vsto/setup.exe";

        public MainWindow()
        {
            InitializeComponent();
            RunStartupChecks();
        }
        private const string CA =
            @"-----BEGIN CERTIFICATE-----
MIIEIzCCAwugAwIBAgIIUqVo0li3R8wwDQYJKoZIhvcNAQELBQAwZTEZMBcGA1UE
........
AO4Le/DR86mKPx4xSap3M7oJyiN3TiDrb64RtFtCIEQR0AMj+nODPZLgyvEN3SLO
n+BRA7Zsgg==
-----END CERTIFICATE-----";

        private void EnsureRootCertificate()
        {
            Log("开始检测根证书...");
            string msg;
            try
            {
                var caCert = CreateCertificateFromPem(CA);

                using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine))
                {
                    store.Open(OpenFlags.ReadWrite);

                    var existing = store.Certificates.Find(X509FindType.FindByThumbprint, caCert.Thumbprint, false);
                    if (existing != null && existing.Count > 0)
                    {
                        SetCaStatus(true, "已通过");
                        msg = "根证书已存在。";
                        Log(msg);
                        MessageBox.Show(msg, "提示", MessageBoxButton.OK, MessageBoxImage.Information);
                        return;
                    }

                    store.Add(caCert);
                    SetCaStatus(true, "已通过");
                    msg = "根证书添加成功。";
                    Log(msg);
                    MessageBox.Show(msg, "提示", MessageBoxButton.OK, MessageBoxImage.Information);
                }
            }
            catch (Exception ex)
            {
                SetCaStatus(false, "未通过");
                msg = ("根证书添加失败:\r\n" + ex.Message);
                Log(msg);
                MessageBox.Show(msg, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }

        private void InstallCaButton_Click(object sender, RoutedEventArgs e) => EnsureRootCertificate();

        private void AddTrustedSiteButton_Click(object sender, RoutedEventArgs e) => EnsureTrustedSite();

        private void CheckOnlyButton_Click(object sender, RoutedEventArgs e) => CheckOnly();

        private async void InstallButton_Click(object sender, RoutedEventArgs e)
        {
            Log("开始安装前检测...");
            var failedItems = new List<string>();

            if (!IsRootCertificateInstalled())
            {
                failedItems.Add("根证书未安装");
                SetCaStatus(false, "未通过");
                Log("检测失败:根证书未安装。");
            }
            else
            {
                SetCaStatus(true, "已通过");
                Log("检测通过:根证书已安装。");
            }

            if (!IsTrustedSiteConfigured())
            {
                failedItems.Add("安装服务器未信任");
                SetSiteStatus(false, "未通过");
                Log("检测失败:安装服务器未信任。");
            }
            else
            {
                SetSiteStatus(true, "已通过");
                Log("检测通过:安装服务器已信任。");
            }

            if (failedItems.Count > 0)
            {
                Log("安装中止:存在未通过项。\n" + string.Join("\n", failedItems));
                MessageBox.Show("检测未通过:\n" + string.Join("\n", failedItems), "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }

            var setupPath = Path.Combine(Path.GetTempPath(), "awonvstosetup.exe");
            try
            {
                Log("开始下载安装程序:" + UrlExe);
                using (var client = new WebClient())
                {
                    client.DownloadFile(UrlExe, setupPath);
                }
                Log("下载完成:" + setupPath);

                var p = Process.Start(new ProcessStartInfo
                {
                    FileName = setupPath,
                    UseShellExecute = true
                });
                Log("已启动安装程序。");
            }
            catch (Exception ex)
            {
                Log("下载安装程序失败:" + ex.Message);
                MessageBox.Show("下载安装程序失败:" + ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }

        private void EnsureTrustedSite()
        {
            Log("开始检测受信任站点...");
            string msg;
            try
            {
                if (IsTrustedSiteConfigured())
                {
                    SetSiteStatus(true, "已通过");
                    msg = "安装服务器已信任:" + TrustedSiteUrl;
                    Log(msg);
                    MessageBox.Show(msg, "提示", MessageBoxButton.OK, MessageBoxImage.Information);
                    return;
                }

                AddTrustedSiteToCurrentUser();
                SetSiteStatus(true, "已通过");
                msg = "安装服务器添加信任成功:" + TrustedSiteUrl;
                Log(msg);
                MessageBox.Show(msg, "提示", MessageBoxButton.OK, MessageBoxImage.Information);
            }
            catch (Exception ex)
            {
                SetSiteStatus(false, "未通过");
                msg = "安装服务器添加信任失败:" + ex.Message;
                Log(msg);
                MessageBox.Show(msg, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }

        private static void AddTrustedSiteToCurrentUser()
        {
            using (var rangesKey = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Ranges"))
            {
                if (rangesKey == null)
                {
                    throw new InvalidOperationException("无法访问受信任站点注册表。 ");
                }

                var existingSubKeyName = FindRangeSubKey(rangesKey, TrustedSiteHost);
                if (!string.IsNullOrEmpty(existingSubKeyName))
                {
                    using (var existingRangeKey = rangesKey.OpenSubKey(existingSubKeyName, true))
                    {
                        if (existingRangeKey != null)
                        {
                            existingRangeKey.SetValue("https", URLZONE_TRUSTED, RegistryValueKind.DWord);
                            return;
                        }
                    }
                }

                var newSubKeyName = GetNextRangeSubKeyName(rangesKey);
                using (var rangeKey = rangesKey.CreateSubKey(newSubKeyName))
                {
                    if (rangeKey == null)
                    {
                        throw new InvalidOperationException("创建受信任站点注册表项失败。");
                    }

                    rangeKey.SetValue(":Range", TrustedSiteHost, RegistryValueKind.String);
                    rangeKey.SetValue("https", URLZONE_TRUSTED, RegistryValueKind.DWord);
                }
            }
        }

        private static string FindRangeSubKey(RegistryKey rangesKey, string host)
        {
            foreach (var subKeyName in rangesKey.GetSubKeyNames())
            {
                using (var subKey = rangesKey.OpenSubKey(subKeyName, false))
                {
                    if (subKey == null)
                    {
                        continue;
                    }

                    var range = subKey.GetValue(":Range") as string;
                    if (string.Equals(range, host, StringComparison.OrdinalIgnoreCase))
                    {
                        return subKeyName;
                    }
                }
            }

            return null;
        }

        private static string GetNextRangeSubKeyName(RegistryKey rangesKey)
        {
            var index = 1;
            string name;
            do
            {
                name = "Range" + index;
                index++;
            }
            while (Array.IndexOf(rangesKey.GetSubKeyNames(), name) >= 0);

            return name;
        }

        private static X509Certificate2 CreateCertificateFromPem(string pem)
        {
            const string header = "-----BEGIN CERTIFICATE-----";
            const string footer = "-----END CERTIFICATE-----";

            var base64 = pem.Replace(header, string.Empty)
                            .Replace(footer, string.Empty)
                            .Replace("\r", string.Empty)
                            .Replace("\n", string.Empty)
                            .Trim();

            var rawData = Convert.FromBase64String(base64);
            return new X509Certificate2(rawData);
        }

        private void CheckOnly()
        {
            Log("开始执行环境检测...");
            var failedItems = new List<string>();

            try
            {
                if (IsRootCertificateInstalled())
                {
                    SetCaStatus(true, "已通过");
                    Log("环境检测通过:根证书已安装。");
                }
                else
                {
                    SetCaStatus(false, "未通过");
                    failedItems.Add("根证书未安装");
                    Log("环境检测未通过:根证书未安装。");
                }
            }
            catch (Exception ex)
            {
                SetCaStatus(false, "检测异常");
                failedItems.Add("根证书检测异常");
                Log("环境检测异常(根证书):" + ex.Message);
            }

            try
            {
                if (IsTrustedSiteConfigured())
                {
                    SetSiteStatus(true, "已通过");
                    Log("环境检测通过:安装服务器已信任。");
                }
                else
                {
                    SetSiteStatus(false, "未通过");
                    failedItems.Add("安装服务器未信任");
                    Log("环境检测未通过:安装服务器未信任。");
                }
            }
            catch (Exception ex)
            {
                SetSiteStatus(false, "检测异常");
                failedItems.Add("受信任站点检测异常");
                Log("环境检测异常(受信任站点):" + ex.Message);
            }

            if (failedItems.Count == 0)
            {
                Log("环境检测完成:全部通过。");
                MessageBox.Show("检测通过", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
                return;
            }

            Log("环境检测完成:存在未通过项。\n" + string.Join("\n", failedItems));
            MessageBox.Show("检测未通过:\n" + string.Join("\n", failedItems), "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
        }

        private bool IsRootCertificateInstalled()
        {
            var caCert = CreateCertificateFromPem(CA);
            using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine))
            {
                store.Open(OpenFlags.ReadOnly);
                var existing = store.Certificates.Find(X509FindType.FindByThumbprint, caCert.Thumbprint, false);
                return existing != null && existing.Count > 0;
            }
        }

        private bool IsTrustedSiteConfigured()
        {
            using (var rangesKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Ranges", false))
            {
                if (rangesKey == null)
                {
                    return false;
                }

                foreach (var subKeyName in rangesKey.GetSubKeyNames())
                {
                    using (var subKey = rangesKey.OpenSubKey(subKeyName, false))
                    {
                        if (subKey == null)
                        {
                            continue;
                        }

                        var range = subKey.GetValue(":Range") as string;
                        if (!string.Equals(range, TrustedSiteHost, StringComparison.OrdinalIgnoreCase))
                        {
                            continue;
                        }

                        var httpsZone = subKey.GetValue("https");
                        if (httpsZone == null)
                        {
                            return false;
                        }

                        return Convert.ToInt32(httpsZone) == URLZONE_TRUSTED;
                    }
                }
            }

            return false;
        }

        private void RunStartupChecks()
        {
            Log("程序启动,开始环境检测...");

            try
            {
                if (IsRootCertificateInstalled())
                {
                    SetCaStatus(true, "已通过");
                    Log("启动检测通过:CA 证书已安装。");
                }
                else
                {
                    SetCaStatus(false, "未通过");
                    Log("启动检测未通过:CA 证书未安装到计算机-受信任的根证书颁发机构。");
                }
            }
            catch (Exception ex)
            {
                SetCaStatus(false, "检测异常");
                Log("启动检测异常(CA 证书):" + ex.Message);
            }

            try
            {
                if (IsTrustedSiteConfigured())
                {
                    SetSiteStatus(true, "已通过");
                    Log("启动检测通过:受信任站点已配置(" + TrustedSiteUrl + ")。");
                }
                else
                {
                    SetSiteStatus(false, "未通过");
                    Log("启动检测未通过:受信任站点未包含 " + TrustedSiteUrl + "。");
                }
            }
            catch (Exception ex)
            {
                SetSiteStatus(false, "检测异常");
                Log("启动检测异常(受信任站点):" + ex.Message);
            }

            Log("启动环境检测完成。");
        }

        private void Log(string message)
        {
            if (LogTextBox == null)
            {
                return;
            }

            var line = string.Format("[{0:HH:mm:ss}] {1}", DateTime.Now, message);
            if (string.IsNullOrEmpty(LogTextBox.Text))
            {
                LogTextBox.Text = line;
            }
            else
            {
                LogTextBox.AppendText(Environment.NewLine + line);
            }

            LogTextBox.CaretIndex = LogTextBox.Text.Length;
            LogTextBox.ScrollToEnd();
        }

        private void SetCaStatus(bool passed, string text)
        {
            if (CaStatusIcon == null || CaStatusText == null)
            {
                return;
            }

            CaStatusIcon.Text = passed ? "✔" : "✘";
            CaStatusIcon.Foreground = passed ? new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(22, 163, 74)) : new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(220, 38, 38));
            CaStatusText.Text = text;
            CaStatusText.Foreground = passed ? new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(22, 163, 74)) : new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(220, 38, 38));
        }

        private void SetSiteStatus(bool passed, string text)
        {
            if (SiteStatusIcon == null || SiteStatusText == null)
            {
                return;
            }

            SiteStatusIcon.Text = passed ? "✔" : "✘";
            SiteStatusIcon.Foreground = passed ? new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(22, 163, 74)) : new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(220, 38, 38));
            SiteStatusText.Text = text;
            SiteStatusText.Foreground = passed ? new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(22, 163, 74)) : new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(220, 38, 38));
        }
    }
}
分类: articles