2025年4月2日 星期三

[研究]SQL Server 2019 「維護計畫精靈」的「副檔名」該輸入 bak 還是 .bak

[研究]SQL Server 2019 「維護計畫精靈」的「副檔名」該輸入 bak 還是 .bak

2025-04-02



SQL Server 2019 「維護計畫精靈」的「定期維護清除工作」畫面的「副檔名」該輸入 "bak" 還是 ".bak"

ChatGPT 說:

在 SQL Server 2019 維護計畫精靈(Maintenance Plan Wizard)中的 定期維護清除工作(Maintenance Cleanup Task)設定 副檔名(File Extension)時,應該輸入 bak(不含 .),而不是 .bak。

原因:

SQL Server 只會比對副檔名的字串,不需要加 .,否則可能會導致匹配錯誤。

官方文件與社群經驗 建議填入副檔名時,不包含 .,例如:

備份檔案:bak

壓縮備份檔案:trn

日誌備份檔案:log

如果輸入 .bak,SQL Server 可能無法正確刪除 .bak 檔案,因為它只會比對 檔案名稱的最後幾個字元是否等於 bak,而不是 .bak。

因此,請輸入 bak ✅ 而不是 .bak ❌。

(完)

相關

2025年4月1日 星期二

[研究]收到的信件主旨前面被加上 [SPF檢查失敗],這是甚麼 ?

[研究]收到的信件主旨前面被加上 [SPF檢查失敗],這是甚麼 ?

2025-04-01

收到的信件主旨前面被加上 [SPF檢查失敗],這是甚麼 ?

ChatGPT 說:

信件主旨前面被加上 [SPF檢查失敗],表示該郵件的寄件伺服器在 SPF (Sender Policy Framework) 檢查時未通過,可能是垃圾郵件或詐騙郵件的跡象。

SPF 檢查機制

SPF 是一種 電子郵件驗證機制,用來防止發送者偽造郵件地址。當你的郵件伺服器收到一封信時,它會:

  • 查詢寄件者的 SPF 紀錄(寄件網域的 DNS 設定)。
  • 檢查寄信的 IP 地址是否在該 SPF 紀錄允許的範圍內。
  • 如果 IP 地址 不在 SPF 紀錄內,就會視為 SPF 失敗,可能標示為可疑郵件。


SPF 檢查失敗的可能原因

  • 寄件伺服器未設定 SPF 紀錄:如果寄件方的網域沒有設定 SPF,收件伺服器無法驗證,可能會標示為 SPF 失敗。
  • 寄件者使用了未授權的伺服器發送郵件:例如:公司郵件應該由企業郵件伺服器發送,但寄件者使用 Gmail、Yahoo 或其他第三方伺服器代發,導致 SPF 失敗。
  • 郵件經過轉寄(Forwarding):如果郵件從 A 伺服器寄給 B,然後 B 轉寄給 C,C 可能會發現 B 不是原寄件網域的授權伺服器,因此 SPF 會失敗。
  • 郵件被偽造(釣魚或垃圾郵件):如果攻擊者偽造寄件者的地址,但實際發信的伺服器 IP 不在 SPF 記錄內,會導致 SPF 失敗。


應對方式

  • 如果是可信來源的郵件:你可以聯絡寄件者,請他們檢查自己的 SPF 設定,確保他們的寄件伺服器 IP 在 SPF 紀錄內。
  • 如果是垃圾郵件或釣魚郵件:不要點擊郵件內的連結或開啟附件,直接刪除或標示為垃圾郵件。

如果這種情況頻繁發生,建議你的 IT 管理員或郵件管理員檢查郵件過濾與 SPF 設定,確保未誤判正常郵件。

(完)

[研究]DropDownList從資料庫抓出的值,不在下拉選項中的處理

[研究]DropDownList從資料庫抓出的值,不在下拉選項中的處理

2025-04-01

環境:Visual Studio 2022 + ASP.NET + WebForm + Web Application + C# + SQL Server 2019 + SQL Server Management Studio (SSMS) 19


DropDownListClass.cs

using System;
using System.Web.UI.WebControls;

namespace WebApplicationProject1.Controls
{
    public class DropDownListClass : DropDownList
    {
        protected override void PerformDataBinding(System.Collections.IEnumerable dataSource)
        {
            // 過濾掉 DropDownList 在 DataBinding 時可能會發生的 Exception 問題!
            // 如果 Exception 發生,預設會選取第一個下拉選項!
            try
            {
                base.PerformDataBinding(dataSource);
            }
            catch (ArgumentOutOfRangeException ex)
            {
                ArgumentOutOfRangeException o = ex;
            }
        }
    }
}

(完)

相關

[研究]建立連接至 SQL Server 時,發生網路相關或執行個體特定的錯誤

[研究]建立連接至 SQL Server 時,發生網路相關或執行個體特定的錯誤

2025-04-01

環境:Visual Studio 2022 + ASP.NET + WebForm + Web Application + C# + SQL Server 2019 + SQL Server Management Studio (SSMS) 19

********************************************************************************



********************************************************************************

'/' 應用程式中發生伺服器錯誤。

找不到網路路徑。

描述: 在執行目前 Web 要求的過程中發生未處理的例外狀況。請檢閱堆疊追蹤以取得錯誤的詳細資訊,以及在程式碼中產生的位置。

例外狀況詳細資訊: System.ComponentModel.Win32Exception: 找不到網路路徑。

原始程式錯誤:

行 55:             {
行 56:                 SqlCommand command = new SqlCommand(queryString, connection);
行 57:                 connection.Open();
行 58:                 try
行 59:                 {




堆疊追蹤:

[Win32Exception (0x80004005): 找不到網路路徑。]

[SqlException (0x80131904): 建立連接至 SQL Server 時,發生網路相關或執行個體特定的錯誤。找不到或無法存取伺服器。確認執行個名稱是否正確,以及 SQL Server 是否設定為允許遠端連線。 (provider: Named Pipes Provider, error: 40 - 無法開啟至 SQL Server 的連接)]
   System.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, UInt32 waitForMultipleObjectsTimeout, Boolean allowCreate, Boolean onlyOneCheckConnection, DbConnectionOptions userOptions, DbConnectionInternal& connection) +1341
   System.Data.ProviderBase.DbConnectionPool.TryGetConnection(DbConnection owningObject, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal& connection) +159
   System.Data.ProviderBase.DbConnectionFactory.TryGetConnection(DbConnection owningConnection, TaskCompletionSource`1 retry, DbConnectionOptions userOptions, DbConnectionInternal oldConnection, DbConnectionInternal& connection) +382
   System.Data.ProviderBase.DbConnectionInternal.TryOpenConnectionInternal(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource`1 retry, DbConnectionOptions userOptions) +307
   System.Data.SqlClient.SqlConnection.TryOpenInner(TaskCompletionSource`1 retry) +198
   System.Data.SqlClient.SqlConnection.TryOpen(TaskCompletionSource`1 retry) +422
   System.Data.SqlClient.SqlConnection.Open() +199   System.Web.UI.Control.OnLoad(EventArgs e) +99
   System.Web.UI.<LoadRecursiveAsync>d__246.MoveNext() +284
   System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() +32
   System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) +62
   System.Web.Util.WithinCancellableCallbackTaskAwaiter.GetResult() +32
   System.Web.UI.<ProcessRequestMainAsync>d__523.MoveNext() +6925



版本資訊: Microsoft .NET Framework 版本:4.0.30319; ASP.NET 版本:4.8.4770.0

********************************************************************************

後來檢查 Deploy 的目的目錄中的 Web.Config 檔案內容,發現選錯 Deploy 發布設定檔,改選正確的重新 Deploy 後正常。

(完)

相關

2025年3月31日 星期一

[研究]SQL Server 2019如何禁止sa或sysadmin群組中帳號遠端存取資料庫

[研究]SQL Server 2019如何禁止sa或sysadmin群組中帳號遠端存取資料庫

2025-03-31

限制 sa 帳戶的存取權限

連線到 SQL Server Management Studio (SSMS)

執行下列 SQL 語句來停用 sa 帳戶:

ALTER LOGIN sa DISABLE;   

若仍需使用 sa,但只限本機存取,可以變更 sa 密碼並強制使用 Windows 身分驗證。

**********

建立登入觸發器 (Logon Trigger) 限制遠端 sysadmin 登入

建立觸發器來限制 sa 帳號若不是從某電腦登入,拒絕:

CREATE TRIGGER BlockRemoteSysadminLogin
ON ALL SERVER
FOR LOGON
AS
BEGIN
    IF ORIGINAL_LOGIN() IN ('sa') AND HOST_NAME() NOT IN ('本機電腦名稱')
    BEGIN
        ROLLBACK;
    END
END;


註:主機名稱請在 SSMS 用 SELECT HOST_NAME() 取得,而非用「命令提示字元」(cmd.exe) 用 hostname 指令取得,有可能不同。

建立觸發器來限制 sysadmin 群組若不是從某電腦登入,拒絕:

CREATE TRIGGER BlockRemoteSysadminLogin
ON ALL SERVER
FOR LOGON
AS
BEGIN
    IF IS_SRVROLEMEMBER('sysadmin', ORIGINAL_LOGIN()) = 1 
       AND HOST_NAME() NOT IN ('本機電腦名稱')
    BEGIN
        ROLLBACK;
    END
END;








注意,本機電腦名稱要用 hostname 的結果,不可用句點 (.) 代替,否則之後會完全不能進去



設定完成後,sysadmin 只能從本機登入。

********************************************************************************

【若不小心 "本機電腦名稱" 設定為 "." 】

T-SQL 的 "本機電腦名稱" 若設定為 "." 後,會被立刻強制登出 SSMS,無法再下任何 T-SQL 指令,如何解決。

使用 SQL Server 單人模式 (Single-User Mode)

C:\>net stop MSSQLSERVER
SQL Server (MSSQLSERVER) 服務正在停止.
SQL Server (MSSQLSERVER) 服務已經成功停止。
C:\>net start MSSQLSERVER /m
SQL Server (MSSQLSERVER) 服務正在啟動 .
SQL Server (MSSQLSERVER) 服務已經啟動成功。

C:\>sqlcmd -S localhost -E
1> DROP TRIGGER BlockRemoteSysadminLogin ON ALL SERVER;
2> GO
1> exit

C:\>net stop MSSQLSERVER
SQL Server (MSSQLSERVER) 服務正在停止.
SQL Server (MSSQLSERVER) 服務已經成功停止。

C:\>net start MSSQLSERVER
SQL Server (MSSQLSERVER) 服務正在啟動 .
SQL Server (MSSQLSERVER) 服務已經啟動成功。

C:\>

上面敝人實際測試過可行。

(完)


[研究]哪個元件 OpenXML, ClosedXML, Aspose Cells, ExcelDataReader, NPOI 匯出成 .xlsx 會較省記憶體、較快?

[研究]哪個元件 OpenXML, ClosedXML, Aspose Cells, ExcelDataReader, NPOI 匯出成 .xlsx 會較省記憶體、較快?

2025-03-31

 ASP.NET, WebForm, 從 SQL Server 讀取20萬筆資料,用哪個元件 OpenXML, ClosedXML, Aspose Cells, ExcelDataReader, NPOI 匯出成 .xlsx 會較省記憶體、較快

ChatGPT 說:

在處理大量資料(如 20 萬筆資料)並將其匯出為 .xlsx 格式時,選擇合適的元件對於效能與記憶體使用至關重要。以下是你列出來的幾個元件的比較,根據記憶體消耗與效能來評估:

1. OpenXML SDK

  • 記憶體消耗:OpenXML 是處理 .xlsx 格式的底層 API,直接與 Excel 檔案結構互動,因此它的記憶體消耗相對較低。它不會加載整個工作簿進記憶體,而是逐步寫入檔案,因此適合處理大量資料。
  • 效能:效能相對較快,特別是在處理簡單的資料寫入時。它的低層級操作使其可以有效處理大量資料,而不會因為大量的記憶體消耗而產生效能瓶頸。
  • 缺點:操作上相對較為繁瑣,需手動處理各種 Excel 的細節(如格式化、資料填充等)。
  • 結論:如果記憶體消耗是主要考量,OpenXML SDK 是處理大量資料時效能最好且最省記憶體的選擇。

2. ClosedXML

  • 記憶體消耗:ClosedXML 會加載整個工作簿進記憶體,這意味著當處理大量資料時,它的記憶體消耗會比較高。對於 20 萬筆資料,記憶體消耗會比 OpenXML 高。
  • 效能:ClosedXML 提供高層次的 API,使得開發人員能夠快速上手,但它的效能在處理大型資料集時會受到記憶體限制。
  • 優點:API 使用簡單,開發過程中可以很容易地進行格式化和其他 Excel 特性設定。
  • 缺點:處理大型資料集時,記憶體消耗和效能可能會成為瓶頸。
  • 結論:如果開發速度和易用性比效能更重要,ClosedXML 是不錯的選擇,但在處理大量資料時記憶體消耗會較高。

3. Aspose.Cells

  • 記憶體消耗:Aspose.Cells 是一個商業元件,對於大型資料集的處理有很好的效能,記憶體消耗比 OpenXML 稍高,但仍然可以處理較大資料集。對於 20 萬筆資料,它的效能表現會較為穩定。
  • 效能:Aspose.Cells 提供非常豐富的功能和強大的性能,特別是在處理大型文件和複雜格式化時非常優秀。然而,它是商業授權的元件,會有額外成本。
  • 優點:功能強大,能夠處理複雜的格式、公式和圖表,並且效能較為穩定。
  • 缺點:商業授權,需支付費用。
  • 結論:如果需要高度功能性和穩定性,並且能夠接受商業授權的費用,Aspose.Cells 是一個很好的選擇,但對於 20 萬筆資料來說,記憶體消耗會相對較高。

4. ExcelDataReader

  • 記憶體消耗:ExcelDataReader 主要用於讀取 Excel 檔案,而不是寫入,它的記憶體消耗相對較低。它支援 .xlsx 格式的讀取,但並不直接支援寫入檔案,因此不適用於匯出資料。
  • 效能:效能快速,特別是在處理讀取 Excel 檔案時,但不適用於資料匯出。
  • 結論:ExcelDataReader 主要用於讀取資料,並不適用於從 SQL Server 匯出資料到 .xlsx 格式。

5. NPOI

  • 記憶體消耗:NPOI 在處理大型資料集時表現良好,它的記憶體消耗比 ClosedXML 要低,但會比 OpenXML 高一些。它不會加載整個工作簿到記憶體中,這使得它在記憶體消耗上比 ClosedXML 更有優勢。
  • 效能:效能相對較快,特別是當資料不需要複雜的格式化時。它的底層操作較為簡單,能夠有效處理大量資料,且記憶體消耗較為節省。
  • 缺點:API 相對較為底層,對於需要高階格式化和功能的需求,可能會顯得繁瑣。
  • 結論:NPOI 是一個不錯的選擇,特別是在需要低記憶體消耗的情況下,它在處理大量資料時表現不錯,且比 ClosedXML 更為輕量。

結論:

  • 最省記憶體且效能最佳:OpenXML SDK。這是處理大量資料時最節省記憶體並且效能最好的選擇。
  • 較簡單的高層次 API,效能較佳:NPOI。對於大量資料的匯出,它的記憶體消耗比 ClosedXML 低,並且效能也相對較好。
  • 需要高功能性(格式化、圖表等),且能接受費用:Aspose.Cells,但記憶體消耗會較高。
  • 如果你的主要關注點是處理 20 萬筆資料的效能與記憶體消耗,推薦使用 OpenXML SDK 或 NPOI,根據你的需求選擇適合的 API 層級。

(完)

[研究]ASP.NET 使用 NPOI 匯出寫入 Excel (.xlsx) 檔案

[研究]ASP.NET 使用 NPOI 匯出寫入 Excel (.xlsx) 檔案

[研究]ASP.NET 把 ClosedXML 匯出寫入改寫為使用 NPOI 匯出寫入 Excel (.xlsx) 檔案

2025-03-31

環境:Visual Studio 2022 + ASP.NET + WebForm + Web Application + C# + SQL Server 2019 + SQL Server Management Studio (SSMS) 19

********************************************************************************

Default.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs"
    Inherits="WebApplication1.Default" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <asp:Button ID="Button_Export_Xlsx_by_ClosedXML" runat="server"
            OnClick="Button_Export_Xlsx_by_ClosedXML_Click" Text="匯出(ClosedXML)"
            CssClass="btn btn-info btn-xs" /><br />
        <asp:Button ID="Button_Export_Xlsx_by_NPOI" runat="server"
            OnClick="Button_Export_Xlsx_by_NPOI_Click" Text="匯出(NPOI)"
            CssClass="btn btn-info btn-xs" /><br />
        <asp:Label ID="Label_MSG1" runat="server"></asp:Label>
        <br />
    </form>
</body>
</html>


Default.aspx.cs

using ClosedXML.Excel;
using System;
using System.Configuration;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Configuration;
using System.Web.UI;
using System.Web.UI.WebControls;

using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;

namespace WebApplication1
{
    public partial class Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        // 匯出查詢結果(.xlsx)
        #region == protected void Button_Export_Xlsx_by_ClosedXML_Click(object sender, EventArgs e) ==
        protected void Button_Export_Xlsx_by_ClosedXML_Click(object sender, EventArgs e)
        {
            //string mainFileName = "匯出檔案名稱";
            string mainFileName = "匯出";
            string fd = (string)ConfigurationManager.AppSettings["TempFolder"] + Path.DirectorySeparatorChar.ToString();
            string dateTimeString = DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss-fff");
            // 若檔案名稱有中文字或特殊符號,Response.AddHeader() 輸出檔案名稱要編碼
            string outFileName = HttpUtility.UrlEncode(
                mainFileName, System.Text.Encoding.UTF8) + "-" +
                dateTimeString + ".xlsx";

            // XLWorkbook.SaveAs() 輸出檔案名稱不要用 HttpUtility.UrlEncode 編碼
            // Response.TransmitFile() 輸出檔案名稱不要用 HttpUtility.UrlEncode 編碼
            string dirFileName = fd + mainFileName + "-" +
                dateTimeString + ".xlsx";
            try
            {
                Response.Clear();
                Response.Buffer = true;
                Response.Charset = "";

                #region === Export ===

                // [EXCEL] Excel打開是亂碼?快速找回資料的最好方法!
                // http://blog.e-happy.com.tw/excel-excel%E6%89%93%E9%96%8B%E6%98%AF%E4%BA%82%E7%A2%BC%EF%BC%9F%E5%BF%AB%E9%80%9F%E6%89%BE%E5%9B%9E%E8%B3%87%E6%96%99%E7%9A%84%E6%9C%80%E5%A5%BD%E6%96%B9%E6%B3%95%EF%BC%81/

                // Excel開啟CSV時的中文編碼問題補遺
                //https://blog.darkthread.net/blog/csv-encoding-again/

                //Response.ContentType = "application/vnd.oasis.opendocument.spreadsheet";    // ODF/.ods
                //Response.AddHeader("content-disposition", "attachment;filename=" + outFileName);
                using (MemoryStream myMemoryStream = new MemoryStream())
                {


                    string dataText = "";

                    var wb = new XLWorkbook();
                    var ws = wb.Worksheets.Add("工作表1");

                    // 定義欄位名稱列
                    string[] columnNameArray = { "欄位1", "欄位2", "欄位3" };
                    // Column number must be between 1 and 16384
                    for (int i = 0; i < columnNameArray.Count(); i++)
                    {
                        ws.Cell(1, i + 1).Value = columnNameArray[i];
                    }

                    // 讀取資料,資料寫入「工作表1」
                    //                    string queryString = @"
                    //--DECLARE  @FieldText nvarchar(50)
                    //--SET @FieldText=N'abc'

                    //SELECT * FROM [TestDB].[dbo].[Table1]
                    //";

                    //string queryString = SqlDataSource1.SelectCommand;
                    string queryString = @"SELECT * FROM [MyDB].[dbo].[MyTable]";

                    //queryString = SqlDataSource1.SelectCommand;
                    using (SqlConnection connection = new SqlConnection(
                        WebConfigurationManager.ConnectionStrings["MyDBConnectionString"].ConnectionString))
                    {
                        SqlCommand command = new SqlCommand(queryString, connection);
                        connection.Open();
                        using (SqlDataReader reader = command.ExecuteReader())
                        {
                            int rowIndex = 1;   // 1 是標題列
                            while (reader.Read())
                            {
                                rowIndex++;
                                for (int i = 0; i < reader.FieldCount; i++)
                                {
                                    //reader[i]的 i 要從 0 開始
                                    dataText = reader[i].ToString();

                                    // 規則運算式語言 - 快速參考 | Microsoft Docs
                                    // https://docs.microsoft.com/zh-tw/dotnet/standard/base-types/regular-expression-language-quick-reference

                                    // 換掉特殊字元 (自己評估)
                                    dataText = dataText.Replace("\r", "").Replace("\n", "").Replace("\t", "").Replace(",", ",").Replace("'", "′").Replace(@"""", "”");
                                    dataText = dataText.Replace("\a", "").Replace("\b", "").Replace("\t", "").Replace("\v", "").Replace("\f", "").Replace("\\", "");

                                    // Column number must be between 1 and 16384
                                    ws.Cell(rowIndex, i + 1).Value = dataText;
                                }
                            }
                        }

                        Response.Clear();
                        Response.Buffer = true;
                        Response.ContentEncoding = Encoding.GetEncoding(950);   //950就是所謂的BIG5
                                                                                //Response.AddHeader("Content-Disposition", "attachment;filename=\"" +
                                                                                //    HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8) + "-" +
                                                                                //    DateTime.Now.ToString("yyyyMMddHHmmss") + ".xlsx");
                        Response.AddHeader("Content-Disposition", "attachment;filename=\"" + outFileName);
                        //'Excel 2003 : "application/vnd.ms-excel"
                        //'Excel 2007 : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"

                        //Extension       MIME Type
                        //.xlsx               application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
                        //.xltx                application/vnd.openxmlformats-officedocument.spreadsheetml.template
                        //.potx               application/vnd.openxmlformats-officedocument.presentationml.template
                        //.ppsx              application/vnd.openxmlformats-officedocument.presentationml.slideshow
                        //.pptx               application/vnd.openxmlformats-officedocument.presentationml.presentation
                        //.sldx               application/vnd.openxmlformats-officedocument.presentationml.slide
                        //.docx              application/vnd.openxmlformats-officedocument.wordprocessingml.document
                        //.dotx               application/vnd.openxmlformats-officedocument.wordprocessingml.template
                        //.xlam              application/vnd.ms-excel.addin.macroEnabled.12
                        //.xlsb               application/vnd.ms-excel.sheet.binary.macroEnabled.12

                        //Response.ContentType = "application/ms-excel";
                        Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";

                        //Response.TransmitFile(@"C:\Program Files (x86)\IIS Express\Export.xlsx");
                        //dirFileName = dirFileName.Replace('\\', '/');   //轉換 Windows, Linux 不同路徑斜線
                        //dirFileName = dirFileName.Replace("/", "\\");  // 轉換斜線 / 成 \  ,因 \ 是特殊符號,要以 \\ 表示

                        // 方法1: 直接輸出
                        wb.SaveAs(myMemoryStream);
                        Response.BinaryWrite(myMemoryStream.ToArray());
                        // myMemoryStream.WriteTo(Response.OutputStream); //works too
                        Response.Flush();
                        Response.Close();

                        // 方法2: 若需要寫入檔案再輸出
                        wb.SaveAs(dirFileName);                 // 存檔,Debug 或其他用途
                                                                ////Response.WriteFile(savePath);
                                                                //Response.TransmitFile(dirFileName);   // 把存檔輸出

                        // 輸出結束
                        Response.End();

                        Label_MSG1.ForeColor = System.Drawing.Color.Green;
                        Label_MSG1.Text = "匯出成功。";
                        // 上面 Response 輸出檔案,所以下面 JavaScript alert 是不會跳出的
                        //Page.ClientScript.RegisterStartupScript(Page.GetType(), "message", "<script language='javascript' defer>alert('匯出成功。');</script>");
                    }
                }
                #endregion Export ODS
            }
            catch (Exception)
            {
                throw;
            }
        }
        #endregion

        // 匯出查詢結果(.xlsx)
        #region == protected void Button_Export_Xlsx_by_NPOI_Click(object sender, EventArgs e) ==
        protected void Button_Export_Xlsx_by_NPOI_Click(object sender, EventArgs e)
        {
            string mainFileName = "匯出";
            string dateTimeString = DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss-fff");
            string outFileName = HttpUtility.UrlEncode(mainFileName, System.Text.Encoding.UTF8) + "-" + dateTimeString + ".xlsx";

            try
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    IWorkbook workbook = new XSSFWorkbook();  // 使用 XSSFWorkbook (XLSX 格式)
                    ISheet sheet = workbook.CreateSheet("工作表1");

                    // 設定標題列
                    string[] columnNames = { "欄位1", "欄位2", "欄位3" };
                    IRow headerRow = sheet.CreateRow(0);
                    for (int i = 0; i < columnNames.Length; i++)
                    {
                        headerRow.CreateCell(i).SetCellValue(columnNames[i]);
                    }

                    // 讀取資料
                    string queryString = @"SELECT * FROM [MyDB].[dbo].[MyTable]";
                    using (SqlConnection connection = new SqlConnection(
                        WebConfigurationManager.ConnectionStrings["MyDBConnectionString"].ConnectionString))
                    {
                        SqlCommand command = new SqlCommand(queryString, connection);
                        connection.Open();
                        using (SqlDataReader reader = command.ExecuteReader())
                        {
                            int rowIndex = 1;
                            while (reader.Read())
                            {
                                IRow row = sheet.CreateRow(rowIndex++);
                                for (int i = 0; i < reader.FieldCount; i++)
                                {
                                    string dataText = reader[i].ToString()
                                        .Replace("\r", "").Replace("\n", "").Replace("\t", "")
                                        .Replace(",", ",").Replace("'", "′").Replace("\"", "”")
                                        .Replace("\\", "").Replace("\a", "").Replace("\b", "")
                                        .Replace("\v", "").Replace("\f", "");

                                    row.CreateCell(i).SetCellValue(dataText);
                                }
                            }
                        }
                    }

                    // **避免 MemoryStream 被 NPOI 關閉**
                    workbook.Write(ms);
                    ms.Flush();

                    // 設定 Response 參數
                    Response.Clear();
                    Response.Buffer = true;
                    Response.ContentEncoding = Encoding.UTF8;
                    Response.AddHeader("Content-Disposition", "attachment;filename=\"" + outFileName + "\"");
                    Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";

                    // 輸出檔案
                    Response.BinaryWrite(ms.ToArray());
                    Response.Flush();
                    Response.End();
                }
            }
            catch (Exception ex)
            {
                Label_MSG1.ForeColor = System.Drawing.Color.Red;
                Label_MSG1.Text = "匯出失敗:" + ex.Message;
            }
        }


        #endregion
    }
}

敝人實際20萬筆測試速度,有時ClosedXML快,有時 NPOI快。

(完)

相關

[研究]ASP.NET,System.OutOfMemoryException 問題改善

[研究]ASP.NET,System.OutOfMemoryException 問題改善

2025-03-28x

環境:Visual Studio 2022 + ASP.NET + WebForm + Web Application + C# + SQL Server 2019 + SQL Server Management Studio (SSMS) 19

ASP.NET, C#, WebForm, 下面程式在正式機出現 System.OutOfMemoryException,在測試機正常可以執行完,工作管理員檢查正式機還有 8 GB RAM,請教比較可能出現 記憶體用盡的地方和原因 ?

		try
            {
                Response.Clear();
                Response.Buffer = true;
                Response.Charset = "";

                #region === Export ===

                using (MemoryStream myMemoryStream = new MemoryStream())
                {
                    string dataText = "";

                    var wb = new XLWorkbook();
                    var ws = wb.Worksheets.Add("工作表1");

                    string[] columnNameArray = { "流水號", "產品ID"};
                    for (int i = 0; i < columnNameArray.Count(); i++)
                    {
                        ws.Cell(1, i + 1).Value = columnNameArray[i];
                    }
                    string queryString = @"SELECT * FROM Table1";
                    using (SqlConnection connection = new SqlConnection(
                        WebConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString))
                    {
                        SqlCommand command = new SqlCommand(queryString, connection);
                        connection.Open();
                        using (SqlDataReader reader = command.ExecuteReader())
                        {
                            int rowIndex = 1;   // 1 是標題列
                            while (reader.Read())
                            {
                                rowIndex++;
                                for (int i = 0; i < reader.FieldCount; i++)
                                {
                                    dataText = reader[i].ToString();

                                    dataText = dataText.Replace("\r", "").Replace("\n", "").Replace("\t", "").Replace(",", ",").Replace("'", "′").Replace(@"""", "”");
                                    dataText = dataText.Replace("\a", "").Replace("\b", "").Replace("\t", "").Replace("\v", "").Replace("\f", "").Replace("\\", "");

                                    ws.Cell(rowIndex, i + 1).Value = dataText;
                                }
                            }
                        }

                        Response.Clear();
                        Response.Buffer = true;
                        Response.ContentEncoding = Encoding.GetEncoding(950);  
                        Response.AddHeader("Content-Disposition", "attachment;filename=\"" + outFileName);
                        Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";

                        wb.SaveAs(myMemoryStream);
                        Response.BinaryWrite(myMemoryStream.ToArray());
                        Response.Flush();
                        Response.Close();
                        wb.SaveAs(dirFileName);                 
                        Response.End();

                        Label_MSG1.ForeColor = System.Drawing.Color.Green;
                        Label_MSG1.Text = "匯出成功。";
                    }
                }
                #endregion Export ODS
            }
            catch (Exception)
            {
                throw;
            }
        }

********************************************************************************

ChatGPT 說:

你的程式在正式機出現 System.OutOfMemoryException,但測試機可以正常執行,這可能與正式機的記憶體使用情況、資料量大小或 GC (Garbage Collection) 行為不同有關。以下是幾個可能的記憶體耗盡點與原因:

1. 讀取大量資料導致記憶體爆掉

你的 SQL 查詢 SELECT * FROM Table1 沒有限制返回的行數,這可能會導致:

SqlDataReader 讀取過多資料,特別是 Table1 資料量很大時。

XLWorkbook (wb) 物件持有過多資料,佔用大量記憶體。

解決方案:限制查詢結果

string queryString = @"SELECT TOP 10000 * FROM Table1"; // 減少資料量   

改用分批處理 例如,使用 OFFSET + FETCH NEXT 來分批載入資料。

********************************************************************************

2. MemoryStream 佔用過大記憶體

你的程式碼嘗試將 Excel 儲存到 MemoryStream (myMemoryStream),再將其轉為 Response.BinaryWrite(myMemoryStream.ToArray());,這會:

將整個 Excel 檔案載入記憶體。

當 myMemoryStream.ToArray() 被呼叫時,會分配一個新的 byte 陣列,可能導致額外的記憶體開銷。

解決方案:避免 ToArray(),改用 WriteTo(Response.OutputStream); 直接將 MemoryStream 內容寫入 Response:

wb.SaveAs(Response.OutputStream);   
Response.Flush();
Response.End();

********************************************************************************

3. Response.End() 可能導致未處理的物件佔用記憶體

Response.End(); 會拋出 ThreadAbortException,這可能導致:

物件 (wb, MemoryStream 等) 無法正常釋放。

GC 無法即時回收記憶體。

🔹 解決方案 改用 CompleteRequest():


Response.Flush();   
HttpContext.Current.ApplicationInstance.CompleteRequest();

********************************************************************************

4. XLWorkbook 物件沒有被正確釋放

EPPlus / ClosedXML (XLWorkbook) 會佔用大量記憶體,因為它在記憶體中維護整個 Excel 物件。

🔹 解決方案

顯式釋放 XLWorkbook

wb.Dispose();   

完整修正建議:

using (XLWorkbook wb = new XLWorkbook())
{
    // Excel 生成過程
    using (MemoryStream myMemoryStream = new MemoryStream())
    {
        wb.SaveAs(myMemoryStream);
        myMemoryStream.Position = 0;

        Response.Clear();
        Response.Buffer = true;
        Response.ContentEncoding = Encoding.GetEncoding(950);
        Response.AddHeader("Content-Disposition", $"attachment; filename={outFileName}");
        Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
        myMemoryStream.WriteTo(Response.OutputStream);
        
        Response.Flush();
        HttpContext.Current.ApplicationInstance.CompleteRequest();
    }
} // wb 物件會自動 Dispose()


這樣可以減少記憶體佔用並降低 OutOfMemoryException 發生的機率。

********************************************************************************

總結

正式機發生 OutOfMemoryException 可能的原因:

  • SQL 回傳過大資料集
  • MemoryStream.ToArray() 造成記憶體額外分配
  • Response.End() 造成資源未釋放
  • XLWorkbook 沒有被正確釋放

解法: 

  • 限制 SQL 回傳筆數或改為分批處理
  • 直接寫入 Response.OutputStream,避免 ToArray()
  • 改用 CompleteRequest() 而非 Response.End()
  • 確保 XLWorkbook 使用 using 或 Dispose() 釋放資源

********************************************************************************

完整改過後 CODE

try
{
    Response.Clear();
    Response.Buffer = true;
    Response.Charset = "";

    #region === Export ===
    using (MemoryStream myMemoryStream = new MemoryStream())
    {
        using (XLWorkbook wb = new XLWorkbook())
        {
            var ws = wb.Worksheets.Add("工作表1");

            // 設定欄位標題
            string[] columnNameArray = { "流水號", "產品ID" };
            for (int i = 0; i < columnNameArray.Length; i++)
            {
                ws.Cell(1, i + 1).Value = columnNameArray[i];
            }

            string queryString = @"SELECT * FROM Table1";
            using (SqlConnection connection = new SqlConnection(
                WebConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString))
            {
                using (SqlCommand command = new SqlCommand(queryString, connection))
                {
                    connection.Open();

                    using (SqlDataReader reader = command.ExecuteReader())
                    {
                        int rowIndex = 1; // 1 是標題列
                        while (reader.Read())
                        {
                            rowIndex++;
                            for (int i = 0; i < reader.FieldCount; i++)
                            {
                                string dataText = reader[i]?.ToString() ?? "";
                                // 過濾不必要的特殊字元
                                dataText = dataText.Replace("\r", "")
                                                   .Replace("\n", "")
                                                   .Replace("\t", "")
                                                   .Replace(",", ",")
                                                   .Replace("'", "′")
                                                   .Replace(@"""", "”")
                                                   .Replace("\\", "");

                                ws.Cell(rowIndex, i + 1).Value = dataText;
                            }
                        }
                    }
                }
            }

            // 儲存到 MemoryStream
            wb.SaveAs(myMemoryStream);
        } // wb.Dispose() 自動釋放資源

        myMemoryStream.Position = 0;

        // 設定 HTTP 回應標頭
        Response.ContentEncoding = Encoding.GetEncoding(950);
        Response.AddHeader("Content-Disposition", $"attachment;filename=\"{outFileName}\"");
        Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";

        // 直接寫入 Response.OutputStream,避免 ToArray() 佔用記憶體
        myMemoryStream.WriteTo(Response.OutputStream);

        // 確保資料完全發送
        Response.Flush();
        HttpContext.Current.ApplicationInstance.CompleteRequest();
    } // myMemoryStream.Dispose() 自動釋放資源

    Label_MSG1.ForeColor = System.Drawing.Color.Green;
    Label_MSG1.Text = "匯出成功。";

    #endregion
}
catch (Exception ex)
{
    // 記錄錯誤日誌(可選)
    Label_MSG1.ForeColor = System.Drawing.Color.Red;
    Label_MSG1.Text = "匯出失敗:" + ex.Message;
}

********************************************************************************

實際測試,效能似乎有改善,但是產生的 .xlsx 疑似有問題


Microsoft Excel

我們發現了...的部分內容有問題。您要我們盡可能嘗試復原嗎?如果您信任此活頁簿的來源,請按一下[是]。

待查測、、、

(完)

相關

[研究]ASP.NET, Response.Flush(); Response.Close(); Response.End(); HttpContext.Current.ApplicationInstance.CompleteRequest(); 四者差異?

[研究]ASP.NET,  Response.Flush(); Response.Close(); Response.End(); HttpContext.Current.ApplicationInstance.CompleteRequest(); 四者差異?

2025-03-31

Response.Flush();

Response.Close();

Response.End();

HttpContext.Current.ApplicationInstance.CompleteRequest(); 

四者差異?

ChatGPT 說:

這四個方法 (Response.Flush();、Response.Close();、Response.End();、HttpContext.Current.ApplicationInstance.CompleteRequest();) 都是處理 ASP.NET 請求回應過程中的一些特殊需求,並且它們的作用和行為有所不同。以下是這四個方法的詳細比較:

1️⃣ Response.Flush();

📌 作用:將 當前緩衝區的內容 立即發送到用戶端,但 不結束請求。請求可以繼續執行。

📌 使用場景:用於逐步輸出資料,例如:

  • 顯示進度條。
  • 體驗即時數據更新(如報告下載的進度顯示)。
  • 生成大檔案並逐步將內容發送到用戶端。

📌 可能的問題:

  • 過早的 Flush(); 會中z後續內容的發送或產生錯誤。
  • 如果緩衝區中有大量數據,可能會導致延遲或丟失。

✅ 示例:

Response.Write("開始處理...");
Response.Flush(); // 讓用戶端看到這段訊息
System.Threading.Thread.Sleep(5000); // 模擬長時間執行
Response.Write("處理完成!");
Response.Flush(); // 再次發送訊息

2️⃣ Response.Close();

📌 作用:強制 關閉與客戶端的連線,並且會停止進一步的資料寫入。此方法可能會中斷資料的傳送並導致回應不完整。

📌 使用場景:主要用於極端情況下強制中斷請求,例如:

  • 防止不必要的資料輸出或請求。
  • 中止傳輸中的資料。

📌 可能的問題:

  • 請求的回應可能會不完整,因為 Response.Close(); 會關閉連線。
  • 使用 Response.Close(); 並不會像 Response.End(); 那樣完全終止請求,它只是結束了與用戶端的連線,但頁面處理仍然會繼續。

❌ 不推薦這樣用:

Response.Write("這段內容可能不會完全送到用戶端!");   
Response.Close(); // 用戶端可能收到部分資料

3️⃣ Response.End();

📌 作用:

  • 立即結束當前請求並停止頁面的執行,這會觸發 ThreadAbortException,並強制終止後續的所有處理過程。
  • 它會把所有緩衝區的資料送到用戶端,但會立刻終止頁面的生命週期。

📌 使用場景:

  • 通常用於下載檔案後終止頁面,以避免 ASP.NET 自動輸出的額外 HTML。
  • 停止頁面執行,通常在結束報表生成或其他重處理過程後使用。

📌 可能的問題:

  • 會引發 ThreadAbortException, 這通常是 ASP.NET 用來結束請求時的機制,但這也會產生額外的開銷,並且如果在 catch 區塊中沒有處理這個異常,可能會讓程式碼無法繼續執行。
  • 應避免在異常處理程式碼中使用 Response.End();,因為這會導致 ThreadAbortException 無法捕獲。

✅ 示例:

Response.Clear();
Response.ContentType = "application/pdf";
Response.AddHeader("Content-Disposition", "attachment; filename=example.pdf");
Response.WriteFile("C:\\example.pdf");
Response.End(); // 結束頁面執行,避免額外的內容送出

4️⃣ HttpContext.Current.ApplicationInstance.CompleteRequest();

📌 作用:

  • 終止當前請求的處理過程,並且不會引發 ThreadAbortException。
  • 它會結束請求的處理,但不會中斷頁面輸出,因此可以在回應已生成後手動結束請求的處理。

📌 使用場景:在處理請求時,不需要再繼續執行頁面生命週期的其他步驟。常見於:

  • 下載檔案後終止請求。
  • 不希望繼續執行頁面邏輯的情況下使用。

📌 可能的問題:不會中斷已經輸出的回應,只是終止請求的進一步處理。

✅ 示例:

Response.Clear();
Response.ContentType = "application/pdf";
Response.AddHeader("Content-Disposition", "attachment; filename=example.pdf");
Response.WriteFile("C:\\example.pdf");
HttpContext.Current.ApplicationInstance.CompleteRequest(); // 結束請求,避免 ThreadAbortException


🚀 總結比較

方法作用是否結束請求?會拋出 ThreadAbortException?適用場景
Response.Flush();將緩衝區內容立即發送到用戶端❌ 否❌ 否用於逐步發送資料(即時進度顯示、串流檔案)
Response.Close();強制關閉連線,終止資料傳送✅ 是❌ 否強制終止連線,不推薦使用
Response.End();結束請求並強制結束頁面執行✅ 是✅ 是(ThreadAbortException)結束頁面執行,常用於下載檔案後終止請求
CompleteRequest();結束請求的處理,無異常✅ 是❌ 否結束請求處理,不會拋出 ThreadAbortException,通常用於下載後終止請求


🎯 哪個方法應該使用?

  • Response.Flush();:如果需要逐步發送資料,並且希望在頁面還繼續執行的情況下,將資料發送到用戶端。
  • Response.Close();:用於極端情況中,強制關閉與客戶端的連線,但通常不推薦使用,因為它可能會導致不完整的回應。
  • Response.End();:如果需要立即終止請求並停止頁面執行,推薦用於下載檔案後結束請求,但要小心 ThreadAbortException。
  • CompleteRequest();:當不希望中斷頁面輸出,只希望終止請求處理時,推薦使用此方法,並且不會有 ThreadAbortException。

這些方法的選擇應根據你的需求來決定,對應不同的使用場景來選擇最合適的操作方式。

(完)


2025年3月28日 星期五

[研究]mmc 匯入憑證是否要 administrator ? 或只要 administrators 群組中的帳號即可 ?

[研究]mmc 匯入憑證是否要 administrator ? 或只要 administrators 群組中的帳號即可 ?

2025-03-28

取決於憑證存放的位置:

  • 使用者憑證存放區 (Current User Store):只需要目前登入的使用者帳號即可,不需要管理員權限。
  • 本機電腦憑證存放區 (Local Computer Store):需要 Administrators 群組 成員帳號 (不一定要 Administrator 帳號,但需具備管理員權限)。
  • 受信任的根憑證授權單位 (Trusted Root Certification Authorities):若是 使用者憑證存放區 的 受信任的根憑證,則只需要目前登入的使用者權限。
********************************************************************************
根憑證 (Root CA) 和 CA 憑證的存放位置取決於應用場景,一般建議:

1.放在本機電腦憑證存放區 (Local Computer Store)
  • 伺服器環境 (Web 伺服器、AD 設定、VPN 等)
  • 多使用者系統 (讓所有使用者都能信任相同的 CA 憑證)
  • 需要提升安全性與管理性 (避免一般使用者自行更改)
好處:
  • 所有本機帳戶都能信任該憑證
  • 避免個別使用者自行安裝未授權的根 CA 憑證,提升安全性
  • 方便集中管理 (可透過群組原則 GPO 部署)

2.存放在使用者憑證存放區 (Current User Store)
  • 僅限特定使用者信任的憑證 (例如,個人 VPN 設定、測試用憑證)
  • 無需影響其他使用者 (如共享電腦或多人使用環境)
  • 無系統管理權限 (一般使用者無法存取本機電腦憑證存放區)
好處:
  • 影響範圍只限該使用者,不影響其他帳戶
  • 可用於測試環境,不影響正式系統
 結論
  •  企業或正式環境 → 本機電腦憑證存放區 (Local Computer Store),確保所有使用者都能信任憑證
  •  個人或測試用途 → 使用者憑證存放區 (Current User Store),避免影響系統全域設定
  • 如果需要多台電腦統一管理憑證,建議使用 群組原則 (GPO) 部署 CA 憑證至本機電腦憑證存放區,避免手動安裝的不一致問題。

    ********************************************************************************

    【一般使用者畫面】







    ********************************************************************************
    【系統管理員畫面】






    (完)

    [研究]SQL Server 2019 如何証明存取資料庫是使用加密連線?

    [研究]SQL Server 2019 如何証明存取資料庫是使用加密連線?

    2025-03-28

    方法 1:查詢 sys.dm_exec_connections 動態管理檢視表

    執行以下 SQL 指令來檢查當前連線是否加密:

    SELECT 
        c.session_id, 
        s.login_name, 
        c.encrypt_option, 
        c.client_net_address
    FROM sys.dm_exec_connections AS c
    JOIN sys.dm_exec_sessions AS s ON c.session_id = s.session_id;   
    


    ********************************************************************************

    方法 2:使用 PowerShell 測試加密連線

    指令 (不驗證憑證)

    $connectionString = "Server=.;Database=master;Encrypt=True;TrustServerCertificate=True;Integrated Security=True;"
    $connection = New-Object System.Data.SqlClient.SqlConnection $connectionString
    $connection.Open()
    Write-Host "加密連線成功"
    $connection.Close()

    實際測試


    PS C:\> $connectionString = "Server=.;Database=master;Encrypt=True;TrustServerCertificate=True;Integrated Security=True;"
    PS C:\> $connection = New-Object System.Data.SqlClient.SqlConnection $connectionString
    PS C:\> $connection.Open()
    PS C:\> Write-Host "加密連線成功"
    加密連線成功
    PS C:\> $connection.Close()
    PS C:\>
    

    說明:

    • Encrypt=True:要求 SQL Server 連線加密。
    • TrustServerCertificate=False:需要伺服器提供有效的 SSL 憑證,否則會失敗。
    • TrustServerCertificate=True:忽略憑證驗證,一般非公開的SQL Server不會用商用付費憑證、也沒有FQDN網址。(建議)
    • 如果連線成功,表示加密功能有效。

    ********************

    指令 (要驗證憑證)

    $connectionString = "Server=.;Database=master;Encrypt=True;TrustServerCertificate=False;Integrated Security=True;"
    $connection = New-Object System.Data.SqlClient.SqlConnection $connectionString
    $connection.Open()
    Write-Host "加密連線成功"
    $connection.Close()
    
    

    實際測試


    PS C:\> $connectionString = "Server=.;Database=master;Encrypt=True;TrustServerCertificate=False;Integrated Security=True;"
    PS C:\> $connection = New-Object System.Data.SqlClient.SqlConnection $connectionString
    PS C:\> $connection.Open()
    以 "0" 引數呼叫 "Open" 時發生例外狀況: "與伺服器的連接已成功建立,但在登入程序時發生錯誤。 (provider: SSL Provider, error: 0 - 此憑證
    鏈結是由不受信任的授權單位發出的。)"
    位於 線路:1 字元:1
    + $connection.Open()
    + ~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
        + FullyQualifiedErrorId : SqlException
    
    PS C:\> Write-Host "加密連線成功"
    加密連線成功
    PS C:\> $connection.Close()
    


    ********************************************************************************

    補充:檢查與設定 SQL Server 伺服器端設定

    如果想確認伺服器是否強制加密連線:

    1. 開啟 SQL Server Configuration Manager。
    2. 前往 SQL Server Network Configuration > Protocols for [你的 SQL Server 執行個體]。
    3. 右鍵點擊 Protocols for [你的 SQL Server 執行個體],選擇 Properties。
    4. 在 Flags 頁籤中,檢查 Force Encryption 是否設定為 Yes。

      • Force Encryption = Yes:伺服器會強制所有連線加密(但客戶端仍需信任伺服器的憑證)。
      • Force Encryption = No:伺服器不強制加密,但客戶端仍可選擇使用加密。




    ********************************************************************************

    (完)

    [研究]SQL Server 2019 如何查詢資料庫帳號變更稽核?

    [研究]SQL Server 2019 如何查詢資料庫帳號變更稽核?

    2025-03-27

    SQL Server 2019 如何查詢資料庫帳號變更稽核?

    ChatGPT 說:

    在 SQL Server 2019 中,可以使用 SQL Server Audit 或 Default Trace 來查詢資料庫帳號變更的稽核記錄,以下是幾種方法:

    ********************************************************************************

    SQL Server Audit 需要 Enterprise,敝人未測試,後來測試,ChatGPT 說的疑似有問題

    註:在 SQL Server 中,「伺服器稽核(Server Audit)」功能只有在 Enterprise 版和特定的高階版本(如 Datacenter 或 Developer)才可用,而 Standard 版及更低版本(如 Express)不支援 這個功能。

    方法 1:使用 SQL Server Audit(推薦方式)

    SQL Server Audit 提供較完整的稽核功能,可以記錄 登入帳號新增、刪除、變更密碼、授權變更 等操作。

    1. 建立 Server Audit(伺服器級稽核)

    首先,建立稽核物件,將稽核記錄存放於檔案中:

    CREATE SERVER AUDIT Audit_LoginChanges
    TO FILE ( FILEPATH = 'C:\AuditLogs\', MAXSIZE = 10MB)  
    WITH ( QUEUE_DELAY = 1000, ON_FAILURE = CONTINUE );
    




    2. 建立 Server Audit Specification(伺服器層級稽核規格)

    設定要稽核的事件:

    CREATE SERVER AUDIT SPECIFICATION Audit_Login_Changes_Spec
    FOR SERVER AUDIT Audit_LoginChanges
    ADD (SERVER_PRINCIPAL_CHANGE_GROUP)
    WITH (STATE = ON);
    

    其中 SERVER_PRINCIPAL_CHANGE_GROUP 會記錄登入帳號變更(如 CREATE LOGIN、ALTER LOGIN、DROP LOGIN 等)。

    3. 啟動 Audit

    ALTER SERVER AUDIT Audit_LoginChanges WITH (STATE = ON);  
    

    4. 查詢 Audit 記錄

    當帳號變更後,可以用以下 SQL 查詢:

    SELECT event_time, action_id, succeeded, session_server_principal_name, server_principal_name, statement
    FROM sys.fn_get_audit_file('C:\AuditLogs\*.sqlaudit', DEFAULT, DEFAULT)
    ORDER BY event_time DESC;

    其中

    •  event_time:事件時間
    •  action_id:動作類型(如 LGCR 表示 CREATE LOGIN,LGMD 表示 ALTER LOGIN)
    •  succeeded:是否成功(1 = 成功,0 = 失敗)
    •  session_server_principal_name:執行變更的帳號
    •  server_principal_name:被變更的帳號
    •  statement:執行的 SQL 指令

    ********************************************************************************

    方法 2:使用 Default Trace(預設追蹤)

    SQL Server 預設啟用了 Default Trace,可以用來查詢帳號變更。

    查詢 Default Trace

    SELECT 
        te.name AS EventName,
        t.DatabaseName,
        t.StartTime,
        t.ObjectName,
        t.ObjectType,
        t.EventClass,
        t.EventSubClass,
        t.TextData
    FROM sys.fn_trace_gettable(
        (SELECT REVERSE(SUBSTRING(REVERSE(path), CHARINDEX('\', REVERSE(path)), LEN(path))) + 'log.trc' 
         FROM sys.traces WHERE is_default = 1), DEFAULT) t
    JOIN sys.trace_events te ON t.EventClass = te.trace_event_id
    WHERE t.EventClass IN (104, 105, 108) -- 104: Create Login, 105: Drop Login, 108: Alter Login
    ORDER BY t.StartTime DESC;
    

    其中

    • 104(Create Login):建立登入帳號
    • 105(Drop Login):刪除登入帳號
    • 108(Alter Login):修改登入帳號
    •  Default Trace 只保留短期記錄,如果需要長期記錄,建議使用 SQL Server Audit。

    (下圖) 新增一個帳號後測試


    ********************************************************************************

    方法 3:查詢 SQL Server Error Log(部分變更可查)

    部分帳號變更(如 CREATE LOGIN 或 ALTER LOGIN)會記錄到 SQL Server Error Log。

    請參考

    [研究]SQL Server 2019 資料庫帳號登出登入稽核?
    https://shaurong.blogspot.com/2025/03/sql-server-2019.html

    這方法適用於 SQL Server 已啟用 LOGIN_CHANGE_PASSWORD 記錄的情況,但不如 SQL Server Audit 詳細。

    ********************************************************************************

    總結

    方法優點缺點
    SQL Server Audit(推薦)最完整,支援長期記錄,可用 GUI 或 SQL 查詢需設定 Audit
    Default Trace內建功能,不需額外設定只能查詢最近變更
    SQL Server Error Log不需設定,適用於密碼變更查詢記錄不完整

    如需完整監控帳號變更,建議使用 SQL Server Audit,可確保 符合稽核規範(如 GDPR、ISO 27001),並能長期儲存記錄。

    (完)

    [研究]查電腦是否有 中華電信(CHT)或 TWCA 的根憑證、中繼憑證?(Windows 2019)

    [研究]查電腦是否有 中華電信(CHT)或 TWCA 的根憑證、中繼憑證?(Windows 2019)

    2025-03-28

    ********************************************************************************

    方法 1:使用「憑證管理器 (certmgr.msc)」

    查看【目前的使用者】的憑證

    按 Win + R,輸入 certmgr.msc,按 Enter。

    若是要查看電腦層級的憑證

    1. 請使用 mmc 打開 Microsoft Management Console,
    2. 在「檔案」選單中,選擇「新增/移除嵌入式管理單元 (Add/Remove Snap-in)」。
    3. 選擇「憑證 (Certificates)」,按「新增 (Add)」。
    4. 選擇「電腦帳戶 (Computer account)」,按「下一步」,然後選擇「本機電腦 (Local Computer)」,按「完成」。
    5. 查看「受信任的根憑證」
    6. 在左側展開 「受信任的根憑證授權單位 (Trusted Root Certification Authorities)」 → 「憑證 (Certificates)」。
    7. 查找是否有 「Chunghwa Telecom」 或 「TWCA」 (台灣 CA) 等相關憑證。









    查看「中繼憑證」

    在左側展開 「中繼憑證授權單位 (Intermediate Certification Authorities)」 → 「憑證 (Certificates)」。

    查找是否有 CHT/TWCA 相關的中繼憑證。


    ********************************************************************************

    方法 2:使用 PowerShell 檢查憑證

    您可以使用 PowerShell 來查詢是否安裝了特定的根憑證或中繼憑證。

    1. 查詢 CHT 或 TWCA 所有受信任的根憑證


    PS C:\> Get-ChildItem -Path Cert:\LocalMachine\Root | Where-Object { $_.Subject -like "*Chunghwa Telecom*" }
    
    
       PSParentPath: Microsoft.PowerShell.Security\Certificate::LocalMachine\Root
    
    Thumbprint                                Subject
    ----------                                -------
    D99B104298594763F0B9A927B79269CB47DD158B  CN=ePKI Root Certification Authority - G2, O="Chunghwa Telecom Co., Ltd.", C=TW
    67650DF17E8E7E5B8240A4F4564BCFE23D69C6F0  OU=ePKI Root Certification Authority, O="Chunghwa Telecom Co., Ltd.", C=TW
    
    
    PS C:\> Get-ChildItem -Path Cert:\LocalMachine\Root | Where-Object { $_.Issuer -like "*TWCA*" }
    
    
       PSParentPath: Microsoft.PowerShell.Security\Certificate::LocalMachine\Root
    
    Thumbprint                                Subject
    ----------                                -------
    FD54E4643B49705A2AAAE50653C4F56C2DF8083D  CN=TWCA Global Root CA, OU=Root CA, O=TAIWAN-CA, C=TW
    910A43AFDD86271F30DD937EE6AD92B1324434D2  CN=TWCA InfoSec User CA, OU=User CA, O=TAIWAN-CA Inc., C=TW
    5BBE8E290DAB5C984C154500DD16379CB2704D20  CN=TWCA InfoSec User CA, OU=User CA, O=TAIWAN-CA Inc., C=TW
    415D5BF4EEAEB7075C2AFBC16896B23923ECA026  CN=TWCA Certificate Authority, OU=TWCA Develope, O=TWCA, C=TW
    DF646DCB7B0FD3A96AEE88C64E2D676711FF9D5F  CN=TWCA Root Certification Authority, OU=Root CA, O=TAIWAN-CA, C=TW
    CF9E876DD3EBFC422697A3B5A37AA076A9062348  CN=TWCA Root Certification Authority, OU=Root CA, O=TAIWAN-CA, C=TW
    9CBB4853F6A4F6D352A4E83252556013F5ADAF65  CN=TWCA Global Root CA, OU=Root CA, O=TAIWAN-CA, C=TW
    
    
    PS C:\>


    2. 查詢所有中繼憑證


    PS C:\> Get-ChildItem -Path Cert:\LocalMachine\CA | Where-Object { $_.Subject -like "*Chunghwa Telecom*" }
    
    
       PSParentPath: Microsoft.PowerShell.Security\Certificate::LocalMachine\CA
    
    Thumbprint                                Subject
    ----------                                -------
    C3CAF8570CB12DD0FC9737BFCD0F087C9BE6D281  CN=ePKI Root Certification Authority - G2, O="Chunghwa Telecom Co., Ltd.", C=TW
    81D2E3BF7CD3974C3076A500D7AAEDB31BE37522  CN=ePKI Root Certification Authority - G2, O="Chunghwa Telecom Co., Ltd.", C=TW
    
    
    PS C:\> Get-ChildItem -Path Cert:\LocalMachine\CA | Where-Object { $_.Subject -like "*TWCA*" }
    
    
       PSParentPath: Microsoft.PowerShell.Security\Certificate::LocalMachine\CA
    
    Thumbprint                                Subject
    ----------                                -------
    FD54E4643B49705A2AAAE50653C4F56C2DF8083D  CN=TWCA Global Root CA, OU=Root CA, O=TAIWAN-CA, C=TW
    8CEB8C4402609B142BD907FEDE61D78ABD6D6EBA  CN=TWCA InfoSec User CA, OU=User CA, O=TAIWAN-CA Inc., C=TW
    7723F095467EBBE467CBE4A7DB213975CF93C8B7  CN=TWCA Secure SSL Certification Authority, O=TAIWAN-CA, C=TW
    5BBE8E290DAB5C984C154500DD16379CB2704D20  CN=TWCA InfoSec User CA, OU=User CA, O=TAIWAN-CA Inc., C=TW
    0A72EFD660FD34F254E66A8595BA81E60A754E68  CN=TWCA Secure SSL Certification Authority, OU=Secure SSL Sub-CA, O=TAIWAN-CA, C=TW
    
    
    PS C:\>
    

    3. 查詢特定憑證 (例如憑證指紋 Thumbprint)

    如果您有 CHT 憑證的 Thumbprint,可以用以下方式確認:

    Get-ChildItem -Path Cert:\LocalMachine\Root | Where-Object { $_.Thumbprint -eq "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" }

    ********************************************************************************

    方法 3:使用 certutil 命令列工具

    1. 列出受信任的根憑證

    certutil -viewstore Root


    C:\>certutil -viewstore Root
    Root "受信任的根憑證授權單位"
    CertUtil: -viewstore 命令成功完成。
    
    C:\>




    2. 列出中繼憑證

    certutil -viewstore CA



    3. 查詢特定憑證名稱

    certutil -store Root | findstr /i "Chunghwa"

    certutil -store CA | findstr /i "Chunghwa"


    C:\>certutil -store Root | findstr /i "Chunghwa"
    
    C:\>certutil -store CA | findstr /i "Chunghwa"
    簽發者: OU=ePKI Root Certification Authority, O=Chunghwa Telecom Co., Ltd., C=TW
    主體: CN=ePKI Root Certification Authority - G2, O=Chunghwa Telecom Co., Ltd., C=TW
    簽發者: CN=ePKI Root Certification Authority - G2, O=Chunghwa Telecom Co., Ltd., C=TW
    簽發者: OU=ePKI Root Certification Authority, O=Chunghwa Telecom Co., Ltd., C=TW
    主體: CN=ePKI Root Certification Authority - G2, O=Chunghwa Telecom Co., Ltd., C=TW
    
    C:\>certutil -store Root | findstr /i "TWCA"
    簽發者: CN=TWCA Root Certification Authority, OU=Root CA, O=TAIWAN-CA, C=TW
    主體: CN=TWCA Global Root CA, OU=Root CA, O=TAIWAN-CA, C=TW
    簽發者: CN=TWCA Secure SSL Certification Authority, OU=Secure SSL Sub-CA, O=TAIWAN-CA, C=TW
    簽發者: CN=TWCA Root Certification Authority, OU=Root CA, O=TAIWAN-CA, C=TW
    主體: CN=TWCA InfoSec User CA, OU=User CA, O=TAIWAN-CA Inc., C=TW
    簽發者: CN=TWCA Global Root CA, OU=Root CA, O=TAIWAN-CA, C=TW
    主體: CN=TWCA InfoSec User CA, OU=User CA, O=TAIWAN-CA Inc., C=TW
    簽發者: CN=TWCA Certificate Authority, OU=TWCA Develope, O=TWCA, C=TW
    主體: CN=TWCA Certificate Authority, OU=TWCA Develope, O=TWCA, C=TW
    
    C:\>certutil -store CA | findstr /i "TWCA"
    簽發者: CN=TWCA Root Certification Authority, OU=Root CA, O=TAIWAN-CA, C=TW
    主體: CN=TWCA Global Root CA, OU=Root CA, O=TAIWAN-CA, C=TW
    簽發者: CN=TWCA Global Root CA, OU=Root CA, O=TAIWAN-CA, C=TW
    主體: CN=TWCA InfoSec User CA, OU=User CA, O=TAIWAN-CA Inc., C=TW
    簽發者: CN=TWCA Global Root CA, OU=Root CA, O=TAIWAN-CA, C=TW
    主體: CN=TWCA Secure SSL Certification Authority, O=TAIWAN-CA, C=TW
    簽發者: CN=TWCA Global Root CA, OU=Root CA, O=TAIWAN-CA, C=TW
    主體: CN=TWCA InfoSec User CA, OU=User CA, O=TAIWAN-CA Inc., C=TW
    簽發者: CN=TWCA Global Root CA, OU=Root CA, O=TAIWAN-CA, C=TW
    主體: CN=TWCA Secure SSL Certification Authority, OU=Secure SSL Sub-CA, O=TAIWAN-CA, C=TW
    
    C:\>
    

    (完)