Bài viết kỹ thuật

Bảo vệ trang tính XLSX trong Delphi: 15 tùy chọn cho phép

Bạn giao một workbook đã hoàn chỉnh cho đồng nghiệp và muốn họ lọc dữ liệu, chứ không sửa lại cấu trúc. Vì vậy bạn bảo vệ sheet. Trong các bản HotXLS cũ, thao tác đó chỉ ghi một thứ vào file: <sheetProtection sheet="1" objects="1" scenarios="1"/>, cố định cứng, lần nào cũng vậy. Sheet bị khóa, hash mật khẩu được gắn vào, và người dùng không làm được gì cả, kể cả sắp xếp và lọc mà bạn thật sự muốn để mở. Hộp thoại "Protect Sheet" của Excel có đúng mười lăm ô chọn vì lý do đó, còn engine trước đây không diễn đạt được ô nào. Khoảng trống đó chính là điều mô hình bảo vệ v2.91.0 lấp đầy

HotXLS là một component bảng tính VCL native cho Delphi và C++Builder, đọc và ghi XLS và XLSX mà không cần cài Excel. Bài viết này nói về phía XLSX của bảo vệ worksheet: enum TXLSXSheetProtectionOption mới, thuộc tính AllowOption bật tắt từng quyền, và một quy tắc mã hóa OOXML mà gần như ai tự viết phần tử <sheetProtection> cũng dễ vấp

Bảo vệ worksheet thực sự che chắn điều gì

Trước hết là ranh giới, vì nó quyết định mức độ bạn nên tin vào phần này. Bảo vệ worksheet trong định dạng bảng tính OOXML (ECMA-376) là chính sách tương tác, không phải mã hóa. Nó cho một ứng dụng tuân thủ biết thao tác nào phải từ chối khi sheet được bảo vệ. Giá trị ô vẫn nằm trong xl/worksheets/sheetN.xml dưới dạng văn bản thuần; giải nén file .xlsx là thấy ngay. Mật khẩu tùy chọn được lưu bằng một hash di sản ngắn, không phải khóa để xáo trộn dữ liệu. Ai đổi tên file, mở part và xóa dòng <sheetProtection> là đọc và sửa được mọi thứ

Vì vậy, bảo vệ trả lời câu hỏi “đừng để đồng nghiệp vô tình phá công thức của tôi”, chứ không phải “giữ dữ liệu này bí mật trước người có chủ đích”. Đây là hai vấn đề khác nhau với hai công cụ khác nhau. Nếu bạn cần tính bảo mật, bạn cần mã hóa ở cấp workbook được nói tới trong đầu ra XLSX được bảo vệ bằng AES, thứ thực sự mã hóa gói tin. Bảo vệ sheet và mã hóa workbook có thể ghép với nhau gọn gàng, nhưng chỉ cái thứ hai mới là một ổ khóa. Giữ ranh giới đó rõ ràng, phần còn lại của trang này chỉ là lớp kết nối

Mười lăm tùy chọn và thuộc tính AllowOption

Mỗi worksheet giờ mang theo một tập giá trị TXLSXSheetProtectionOption mô tả người dùng vẫn có thể làm gì khi sheet được bảo vệ. Các thành viên này ánh xạ một-một sang thuộc tính OOXML và các ô chọn trong hộp thoại Excel:

  • xlsxSpoEditObjects, xlsxSpoEditScenarios: sửa đối tượng vẽ và các kịch bản what-if
  • xlsxSpoFormatCells, xlsxSpoFormatColumns, xlsxSpoFormatRows: định dạng lại ô, cột, hàng
  • xlsxSpoInsertColumns, xlsxSpoInsertRows, xlsxSpoInsertHyperlinks: chèn cột, hàng, liên kết
  • xlsxSpoDeleteColumns, xlsxSpoDeleteRows: xóa cột, hàng
  • xlsxSpoSelectLockedCells, xlsxSpoSelectUnlockedCells: di chuyển vùng chọn sang ô đã khóa hoặc chưa khóa
  • xlsxSpoSort, xlsxSpoAutoFilter, xlsxSpoPivotTables: sắp xếp phạm vi, dùng menu thả xuống AutoFilter, làm việc với PivotTables

Bạn đọc và ghi từng bit qua thuộc tính chỉ mục AllowOption trên TXLSXWorksheet. AllowOption[Opt] = True nghĩa là thao tác đó được phép; đặt False thì cấm. Toàn bộ tập quyền cũng có thể truy cập cùng lúc qua SheetProtectionOptions, một TXLSXSheetProtectionOptions (một set of Pascal thuần), nên bạn có thể lưu, khôi phục hoặc thay thế toàn bộ

Mặc định rất quan trọng và có chủ đích: một worksheet mới tạo sẽ bắt đầu với mọi tùy chọn đều được phép. Constructor gieo SheetProtectionOptions bằng toàn bộ dải, [Low(TXLSXSheetProtectionOption)..High(TXLSXSheetProtectionOption)]. Từ đó bạn thu hẹp dần bằng cách loại bỏ các thao tác muốn cấm, thay vì xây một tập quyền từ số không. Chính lựa chọn đó khiến quy tắc mã hóa của writer, ở phần dưới, khớp với hành vi của Excel

Bảo vệ sheet nhưng vẫn để mở sort và filter

Đây là trường hợp thường gặp từ đầu đến cuối: bảo vệ một báo cáo đã hoàn tất để không thể chỉnh lại bố cục, nhưng vẫn cho người đọc sắp xếp và lọc dữ liệu. Lưu ý rằng Protect và các tùy chọn là độc lập. Protect chuyển sheet sang trạng thái được bảo vệ và lưu hash mật khẩu tùy chọn; nó không đụng tới tập tùy chọn. Bạn điều chỉnh AllowOption riêng, và các chuyển đổi có hiệu lực khi sheet đã được bảo vệ và lưu lại

var
  wb: TXLSXWorkbook;
  sh: TXLSXWorksheet;
begin
  wb := TXLSXWorkbook.Create;
  try
    sh := wb.Sheets.Add('Protected');
    sh.Cells[1, 1].Value := 'Region'; sh.Cells[1, 2].Value := 'Units';
    sh.Cells[2, 1].Value := 'North';  sh.Cells[2, 2].Value := 120;
    sh.Cells[3, 1].Value := 'South';  sh.Cells[3, 2].Value := 98;

    // Protect with a password. This only sets the protected state + hash;
    // the option set is left at its all-permitted default.
    sh.Protect('HotXLS-2026');

    // Narrow: keep sort + AutoFilter, forbid reshaping and reformatting.
    sh.AllowOption[xlsxSpoSort]          := True;
    sh.AllowOption[xlsxSpoAutoFilter]    := True;
    sh.AllowOption[xlsxSpoFormatCells]   := False;
    sh.AllowOption[xlsxSpoFormatColumns] := False;
    sh.AllowOption[xlsxSpoFormatRows]    := False;
    sh.AllowOption[xlsxSpoInsertRows]    := False;
    sh.AllowOption[xlsxSpoDeleteRows]    := False;

    if wb.SaveAs('protection.xlsx') <> 1 then
      Writeln('SaveAs failed');
  finally
    wb.Free;
  end;
end;

Hai điều cần rút ra từ đoạn mã này. Dòng SortAutoFilter được ghi rõ dù cả hai đều mặc định là True; đó là ghi chú cho người bảo trì tiếp theo, không phải yêu cầu chức năng. Và vì mặc định là cho phép, những dòng làm thay đổi file đầu ra duy nhất là các dòng đặt một tùy chọn thành False. Đó không phải là ngẫu nhiên của API này, mà là định dạng wire OOXML lộ ra, và phần tiếp theo nói về điều đó

Quy tắc mã hóa: thiếu nghĩa là cho phép, attr=0 nghĩa là cấm

Đây là chi tiết dễ ngược trực giác nhất trong toàn bộ tính năng, và cũng là chỗ phần <sheetProtection> viết tay hay sai nhất. Trong OOXML, mỗi thuộc tính cho từng thao tác là một cờ cấm, và việc nó không xuất hiện là được phép. Thuộc tính bị thiếu có nghĩa là thao tác đó được cho phép. Thuộc tính được ghi là "0" có nghĩa là thao tác đó bị cấm khi sheet được bảo vệ. Không hề có formatCells="1" trong một file hợp lệ với nghĩa “được phép định dạng”; bạn chỉ cần bỏ thuộc tính đó ra. (Giá trị mặc định của một thuộc tính vắng mặt là mặc định boolean true của OOXML, và các thuộc tính này được đặt tên để “true” nghĩa là thao tác tương ứng được phép)

Bộ ghi của HotXLS làm đúng như vậy. Nó phát ra sheet="1" để bật bảo vệ, rồi duyệt qua tập tùy chọn và chỉ ghi attr="0" cho những tùy chọn bạn đặt False. Các thao tác được phép không đóng góp gì vào đầu ra. Vì vậy workbook ở phần trước sẽ được tuần tự hóa thành đại loại như sau, chỉ mang theo các thao tác bị cấm cùng hash mật khẩu:

// Conceptual output for the snippet above (attributes elided for brevity):
// <sheetProtection sheet="1"
//   formatCells="0" formatColumns="0" formatRows="0"
//   insertRows="0" deleteRows="0"
//   password="...4-hex..."/>
// Note what is NOT there: no sort, no autoFilter, no selectLockedCells.
// Their absence is exactly what tells Excel those actions stay allowed.

Nếu bạn xuất phát từ chuỗi hard-coded cũ và chờ thấy mọi thuộc tính đều được ghi ra, cách này trông có vẻ thưa, gần như sai. Nhưng nó đúng. Một file liệt kê sort="1"autoFilter="1" sẽ có cùng ý nghĩa với một trình đọc tuân thủ, nhưng chính Excel lại ghi theo dạng tối giản chỉ liệt kê các mục bị cấm, và việc bám theo cách đó giúp diff nhỏ và vòng round-trip bớt ồn. Các thuộc tính objectsscenarios tuân theo cùng một quy tắc: chúng mặc định được phép, nên chỉ xuất hiện dưới dạng "0" khi bạn cấm chúng, trái ngược với objects="1" scenarios="1" cũ vốn được phát ra vô điều kiện

Đọc lại bảo vệ: độ trung thực round-trip

Một mô hình quyền mà bạn có thể ghi nhưng không đọc là ngõ một chiều, và triệu chứng thường gặp là chu kỳ nạp-sửa-lưu âm thầm mở rộng quyền. HotXLS chặn điều đó. Khi ParseWorksheetXml gặp phần tử <sheetProtection>, nó đặt sheet ở trạng thái được bảo vệ, lấy hash mật khẩu nếu có, rồi giải mã từng thuộc tính theo từng thao tác trở lại AllowOption theo cùng quy ước nhưng ngược chiều: thuộc tính tồn tại và bằng "0" thì cấm thao tác đó; thuộc tính vắng mặt giữ tùy chọn ở mặc định được phép

var
  wb: TXLSXWorkbook;
  sh: TXLSXWorksheet;
begin
  wb := TXLSXWorkbook.Create;
  try
    wb.LoadFromFile('protection.xlsx');
    sh := wb.Sheets[1];                  // XLSX sheets are 1-based
    if sh.IsProtected then
    begin
      Writeln('Protected; password hash present: ',
        sh.SheetProtectHash <> '');
      Writeln('Sort allowed:       ', sh.AllowOption[xlsxSpoSort]);
      Writeln('AutoFilter allowed: ', sh.AllowOption[xlsxSpoAutoFilter]);
      Writeln('FormatCells allowed:', sh.AllowOption[xlsxSpoFormatCells]);
    end;
  finally
    wb.Free;
  end;
end;

Nạp file mà writer tạo ra, bạn sẽ nhận lại SortAutoFilterTrue, FormatCellsFalse, tức đúng tập bạn đã lưu, nguyên vẹn. Đó chính là mục tiêu của tính đối xứng này: sửa một ô trong sheet được bảo vệ nhưng cho phép một phần, rồi lưu lại, và mười bốn quyền bạn không đụng tới vẫn còn nguyên thay vì sập về mặc định cũ kiểu tất cả hoặc không gì cả

Lưu ý thực tế và giới hạn

Một vài điều nên biết trước khi bạn gắn cơ chế này vào pipeline báo cáo:

  • Mật khẩu yếu theo thiết kế: Bảo vệ worksheet XLSX lưu một hash di sản 16 bit, cùng loại Excel đã dùng hàng chục năm, giữ lại để tương thích. Nó ngăn sửa nhầm, chứ không chống được kẻ tấn công. Đừng xem nó là cơ chế giữ bí mật. Nếu cần bảo vệ thật, hãy mã hóa workbook
  • Đặt tùy chọn trước khi bảo vệ là hoàn toàn ổn: AllowOption có thể được gán dù sheet đang được bảo vệ hay không; các toggle chỉ mô tả điều gì protection sẽ cho phép khi Protect có hiệu lực. UnProtect xóa trạng thái được bảo vệ và hash, nhưng giữ nguyên tập tùy chọn cho lần sau
  • Ngữ nghĩa ô đã khóa vẫn áp dụng: Bảo vệ chỉ chặn sửa các ô có thuộc tính Locked được đặt (mặc định của workbook). Việc để một vùng nhập liệu có thể sửa được là việc của style ô, không phải của tùy chọn bảo vệ; hai lớp này kết hợp giống hệt trong Excel
  • Đây là engine XLSX: Mô hình tùy chọn phản ánh các thuộc tính Allow* cũ của engine XLS, nhưng tên enum và thuộc tính ở đây (xlsxSpo*, AllowOption) thuộc về TXLSXWorksheet trong lxHandleX. Nếu bạn cũng điều khiển bố cục in trên cùng các sheet này, bài hướng dẫn về bảo vệ và thiết lập trang sẽ cho thấy các thiết lập này đứng cạnh vùng in và header như thế nào, còn xác thực dữ liệu, AutoFilter và bảng ghép rất tự nhiên với việc để mở xlsxSpoAutoFilter trên một báo cáo đã khóa

Mô hình bảo vệ chi tiết cùng phần còn lại của engine đọc/ghi XLSX được phát hành trong HotXLS Component cho Delphi và C++Builder; trang sản phẩm có đầy đủ API của worksheet, bao gồm toàn bộ tham chiếu tùy chọn bảo vệ