
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));
}
}
}