Khung được kiểm soát

Demián Renzulli
Demián Renzulli
Simon Hangl
Simon Hangl

Phần tử <iframe> thường được dùng để nhúng các tài nguyên bên ngoài trong một bối cảnh duyệt web. Iframe thực thi các chính sách bảo mật của web bằng cách tách biệt nội dung được nhúng nhiều nguồn gốc khỏi trang lưu trữ và ngược lại. Mặc dù phương pháp này giúp tăng cường bảo mật bằng cách đảm bảo ranh giới bảo mật giữa các nguồn gốc, nhưng nó lại hạn chế một số trường hợp sử dụng. Ví dụ: người dùng có thể cần tải và quản lý nội dung một cách linh hoạt từ nhiều nguồn, chẳng hạn như giáo viên kích hoạt một sự kiện điều hướng để hiển thị trang web trên màn hình lớp học. Tuy nhiên, nhiều trang web chặn rõ ràng việc nhúng trên iframe bằng cách sử dụng các tiêu đề bảo mật như X-Frame-Options và Chính sách bảo mật nội dung (CSP). Ngoài ra, các quy định hạn chế về iframe ngăn các trang nhúng quản lý trực tiếp hoạt động điều hướng hoặc hành vi của nội dung được nhúng.

Controlled Frame API giải quyết hạn chế này bằng cách cho phép tải mọi nội dung web, ngay cả khi nội dung đó thực thi các chính sách nhúng hạn chế. API này chỉ có trong Ứng dụng web tách biệt (IWA). Các ứng dụng này kết hợp các biện pháp bảo mật bổ sung để bảo vệ cả người dùng và nhà phát triển khỏi những rủi ro tiềm ẩn.

Triển khai Khung hình được kiểm soát

Trước khi sử dụng Khung được kiểm soát, bạn cần thiết lập một IWA hoạt động. Sau đó, bạn có thể tích hợp Khung hình có kiểm soát vào các trang của mình.

Thêm chính sách về quyền

Để sử dụng Khung được kiểm soát, hãy bật quyền tương ứng bằng cách thêm trường permissions_policy có giá trị "controlled-frame" vào tệp kê khai IWA. Ngoài ra, hãy thêm khoá cross-origin-isolated. Khoá này không dành riêng cho Khung được kiểm soát, nhưng bắt buộc đối với tất cả IWA và xác định xem tài liệu có thể truy cập vào các API yêu cầu cách ly trên nhiều nguồn gốc hay không.

{
   ...
  "permissions_policy": {
     ...
     "controlled-frame": ["self"],
     "cross-origin-isolated": ["self"]
     ...
  }
   ...
}

Khoá controlled-frame trong tệp kê khai Ứng dụng web tách biệt (IWA) xác định danh sách cho phép chính sách về quyền, chỉ định những nguồn gốc có thể sử dụng Khung được kiểm soát. Mặc dù tệp kê khai hỗ trợ cú pháp đầy đủ của Chính sách về quyền (cho phép các giá trị như *, nguồn gốc cụ thể hoặc từ khoá như selfsrc), nhưng điều quan trọng cần lưu ý là bạn không thể uỷ quyền các API dành riêng cho IWA cho các nguồn gốc khác. Ngay cả khi danh sách cho phép có chứa ký tự đại diện hoặc nguồn gốc bên ngoài, những quyền này sẽ không có hiệu lực đối với các tính năng IWA như controlled-frame. Không giống như các ứng dụng web tiêu chuẩn, theo mặc định, IWA sẽ không có tính năng nào được kiểm soát theo chính sách, yêu cầu khai báo rõ ràng. Đối với các tính năng dành riêng cho IWA, điều này có nghĩa là chỉ những giá trị như self (nguồn gốc riêng của IWA) hoặc src (nguồn gốc của một khung được nhúng) mới có hiệu quả về chức năng.

Thêm phần tử Khung được kiểm soát

Chèn một phần tử <controlledframe> vào HTML để nhúng nội dung của bên thứ ba vào IWA.

<controlledframe id="controlledframe_1" src="https://example.com">
</controlledframe>

Thuộc tính partition không bắt buộc sẽ định cấu hình việc phân vùng bộ nhớ cho nội dung được nhúng, cho phép bạn cô lập dữ liệu như cookie và bộ nhớ cục bộ để duy trì dữ liệu trên các phiên.

Ví dụ: Phân vùng bộ nhớ trong

Tạo một Khung được kiểm soát bằng cách sử dụng một phân vùng lưu trữ trong bộ nhớ có tên là "session1". Dữ liệu được lưu trữ trong phân vùng này (ví dụ: cookie và localStorage) sẽ bị xoá khi khung bị huỷ hoặc phiên ứng dụng kết thúc.

<controlledframe id="controlledframe_1" src="https://example.com">
</controlledframe>

Ví dụ: Phân vùng bộ nhớ liên tục

Tạo Khung hình có kiểm soát bằng một phân vùng bộ nhớ liên tục có tên là "user_data". Tiền tố "persist:" đảm bảo rằng dữ liệu được lưu trữ trong phân vùng này sẽ được lưu vào ổ đĩa và có sẵn trong các phiên ứng dụng.

<controlledframe id="frame_2" src="..." partition="persist:user_data">
</controlledframe>

Nhận tham chiếu phần tử

Lấy một giá trị tham chiếu đến phần tử <controlledframe> để bạn có thể tương tác với phần tử này như bất kỳ phần tử HTML tiêu chuẩn nào:

const controlledframe = document.getElementById('controlledframe_1');

Các tình huống và trường hợp sử dụng thường gặp

Theo nguyên tắc chung, hãy chọn công nghệ phù hợp nhất với nhu cầu của bạn mà không cần quá phức tạp. Trong những năm gần đây, Ứng dụng web tiến bộ (PWA) đã thu hẹp khoảng cách với các ứng dụng gốc, mang đến trải nghiệm web mạnh mẽ. Nếu một ứng dụng web cần nhúng nội dung của bên thứ ba, thì trước tiên, bạn nên khám phá phương pháp <iframe> thông thường. Nếu các yêu cầu vượt quá khả năng của iframe, thì Controlled Frame trên IWA có thể là lựa chọn thay thế tốt nhất. Các trường hợp sử dụng phổ biến được mô tả trong các phần sau.

Nhúng nội dung web của bên thứ ba

Nhiều ứng dụng cần có khả năng tải và hiển thị nội dung của bên thứ ba trong giao diện người dùng. Tuy nhiên, khi có nhiều chủ sở hữu ứng dụng web tham gia (một trường hợp phổ biến với các ứng dụng được nhúng), thì việc thiết lập các chính sách nhất quán từ đầu đến cuối trở nên khó khăn. Ví dụ: chế độ cài đặt bảo mật có thể ngăn <iframe> truyền thống nhúng một số loại nội dung nhất định, ngay cả khi các doanh nghiệp có nhu cầu chính đáng để làm như vậy. Không giống như các phần tử <iframe>, Controlled Frame được thiết kế để bỏ qua những hạn chế này, cho phép các ứng dụng tải và hiển thị nội dung ngay cả khi nội dung đó rõ ràng cấm hoạt động nhúng tiêu chuẩn.

Trường hợp sử dụng

  • Bài thuyết trình trên lớp học: Giáo viên sử dụng màn hình cảm ứng trong lớp học để chuyển đổi giữa các tài nguyên giáo dục thường chặn việc nhúng iframe.
  • Biển báo kỹ thuật số trong cửa hàng bán lẻ hoặc trung tâm mua sắm: Một ki-ốt trong trung tâm mua sắm sẽ lần lượt hiển thị các trang web của nhiều cửa hàng. Controlled Frames đảm bảo các trang này tải đúng cách ngay cả khi chúng hạn chế việc nhúng.

Mã mẫu

Các Controlled Frame API sau đây rất hữu ích cho việc quản lý nội dung được nhúng.

Điều hướng: Controlled Frame cung cấp nhiều phương thức để quản lý và kiểm soát theo phương pháp có lập trình hoạt động điều hướng và nhật ký điều hướng của nội dung được nhúng.

Thuộc tính src nhận hoặc đặt URL của nội dung hiển thị trong khung, hoạt động theo cách tương tự như thuộc tính HTML.

controlledframe.src = "https://example.com";

Phương thức back() quay lại một bước trong nhật ký của khung. Lời hứa được trả về sẽ phân giải thành một giá trị boolean cho biết liệu thao tác điều hướng có thành công hay không.

document.getElementById('backBtn').addEventListener('click', () => {
controlledframe.back().then((success) => {
console.log(`Back navigation ${success ? 'succeeded' : 'failed'}`); }).catch((error) => {
   console.error('Error during back navigation:', error);
   });
});

Phương thức forward() chuyển tiếp một bước trong nhật ký của khung. Lời hứa được trả về sẽ phân giải thành một giá trị boolean cho biết liệu thao tác điều hướng có thành công hay không.

document.getElementById('forwardBtn').addEventListener('click', () => {
controlledframe.forward().then((success) => {
   console.log(`Forward navigation ${success ? 'succeeded' : 'failed'}`);
}).catch((error) => {
    console.error('Error during forward navigation:', error);
  });
});

Phương thức reload() tải lại trang hiện tại trong khung.

document.getElementById('reloadBtn').addEventListener('click', () => {
   controlledframe.reload();
});

Ngoài ra, Controlled Frames cung cấp các sự kiện cho phép bạn theo dõi toàn bộ vòng đời của các yêu cầu điều hướng – từ khi bắt đầu và chuyển hướng đến khi tải nội dung, hoàn tất hoặc huỷ bỏ.

  • loadstart: Kích hoạt khi một thao tác điều hướng bắt đầu trong khung hình.
  • loadcommit: Kích hoạt khi yêu cầu điều hướng đã được xử lý và nội dung tài liệu chính bắt đầu tải.
  • contentload: Kích hoạt khi tài liệu chính và các tài nguyên thiết yếu của tài liệu đó đã tải xong (tương tự như DOMContentLoaded).
  • loadstop: Kích hoạt khi tất cả tài nguyên cho trang (bao gồm cả khung phụ, hình ảnh) đã tải xong.
  • loadabort: Được kích hoạt nếu một thao tác điều hướng bị huỷ (ví dụ: do hành động của người dùng hoặc một thao tác điều hướng khác bắt đầu).
  • loadredirect: Kích hoạt khi lệnh chuyển hướng phía máy chủ xảy ra trong quá trình điều hướng.
controlledframe.addEventListener('loadstart', (event) => {
   console.log('Navigation started:', event.url);
   // Example: Show loading indicator
 });
controlledframe.addEventListener('loadcommit', (event) => {
   console.log('Navigation committed:', event.url);
 });
controlledframe.addEventListener('contentload', (event) => {
   console.log('Content loaded for:', controlledframe.src);
   // Example: Hide loading indicator, maybe run initial script
 });
controlledframe.addEventListener('loadstop', (event) => {
   console.log('All resources loaded for:', controlledframe.src);
 });
controlledframe.addEventListener('loadabort', (event) => {
   console.warn(`Navigation aborted: ${event.url}, Reason: ${event.detail.reason}`);
 });
controlledframe.addEventListener('loadredirect', (event) => {
   console.log(`Redirect detected: ${event.oldUrl} -> ${event.newUrl}`);
});

Bạn cũng có thể giám sát và có khả năng chặn các hoạt động tương tác hoặc yêu cầu cụ thể do nội dung được tải trong khung được kiểm soát khởi tạo, chẳng hạn như các nỗ lực mở hộp thoại, yêu cầu cấp quyền hoặc mở cửa sổ mới.

  • dialog: Kích hoạt khi nội dung được nhúng cố gắng mở một hộp thoại (cảnh báo, xác nhận, lời nhắc). Bạn sẽ nhận được thông tin chi tiết và có thể phản hồi.
  • consolemessage: Kích hoạt khi một thông báo được ghi vào bảng điều khiển trong khung.
  • permissionrequest: Kích hoạt khi nội dung được nhúng yêu cầu một quyền (ví dụ: vị trí địa lý và thông báo). Bạn sẽ nhận được thông tin chi tiết và có thể cho phép hoặc từ chối yêu cầu.
  • newwindow: Kích hoạt khi nội dung được nhúng cố gắng mở một cửa sổ hoặc thẻ mới (ví dụ: bằng window.open hoặc một đường liên kết có target="_blank"). Bạn sẽ nhận được thông tin chi tiết và có thể xử lý hoặc chặn hành động này.
controlledframe.addEventListener('dialog', (event) => {
   console.log(Dialog opened: Type=${event.messageType}, Message=${event.messageText});
   // You will need to respond, e.g., event.dialog.ok() or .cancel()
 });

controlledframe.addEventListener('consolemessage', (event) => {
   console.log(Frame Console [${event.level}]: ${event.message});
 });

controlledframe.addEventListener('permissionrequest', (event) => {
   console.log(Permission requested: Type=${event.permission});
   // You must respond, e.g., event.request.allow() or .deny()
   console.warn('Permission request needs handling - Denying by default');
   if (event.request && event.request.deny) {
     event.request.deny();
   }
});

controlledframe.addEventListener('newwindow', (event) => {
   console.log(New window requested: URL=${event.targetUrl}, Name=${event.name});
   // Decide how to handle this, e.g., open in a new controlled frame and call event.window.attach(), ignore, or block
   console.warn('New window request needs handling - Blocking by default');
 });

Ngoài ra, còn có các sự kiện thay đổi trạng thái thông báo cho bạn về những thay đổi liên quan đến trạng thái kết xuất của khung hình được kiểm soát, chẳng hạn như các sửa đổi đối với kích thước hoặc mức thu phóng của khung hình đó.

  • sizechanged: Kích hoạt khi kích thước nội dung của khung thay đổi.
  • zoomchange: Kích hoạt khi mức thu phóng của nội dung khung thay đổi.
controlledframe.addEventListener('sizechanged', (event) => {
  console.log(Frame size changed: Width=${event.width}, Height=${event.height});
});

controlledframe.addEventListener('zoomchange', (event) => {
  console.log(Frame zoom changed: Factor=${event.newZoomFactor});
});

Phương thức lưu trữ: Khung được kiểm soát cung cấp các API để quản lý dữ liệu được lưu trữ trong phân vùng của khung.

Sử dụng clearData() để xoá tất cả dữ liệu đã lưu trữ. Thao tác này đặc biệt hữu ích khi đặt lại khung sau một phiên người dùng hoặc đảm bảo trạng thái sạch. Phương thức này trả về một Promise sẽ phân giải khi hoạt động hoàn tất. Bạn cũng có thể cung cấp các lựa chọn cấu hình không bắt buộc:

  • types: Một mảng gồm các chuỗi chỉ định loại dữ liệu cần xoá (ví dụ: ['cookies', 'localStorage', 'indexedDB']). Nếu bỏ qua, tất cả các loại dữ liệu có thể áp dụng thường sẽ bị xoá.
  • options: Kiểm soát quy trình xoá, chẳng hạn như chỉ định một dải thời gian bằng cách sử dụng thuộc tính since (dấu thời gian tính bằng mili giây kể từ thời gian bắt đầu của hệ thống) để chỉ xoá dữ liệu được tạo sau thời gian đó.

Ví dụ: Xoá tất cả bộ nhớ liên kết với Khung được kiểm soát

function clearAllPartitionData() {
   console.log('Clearing all data for partition:', controlledframe.partition);
   controlledframe.clearData()
     .then(() => {
       console.log('Partition data cleared successfully.');
     })
     .catch((error) => {
       console.error('Error clearing partition data:', error);
     });
}

Ví dụ: Chỉ xoá cookie và localStorage được tạo trong giờ qua

function clearRecentCookiesAndStorage() {
   const oneHourAgo = Date.now() - (60 * 60 * 1000);
   const dataTypesArray = ['cookies', 'localStorage'];
   const dataTypesToClearObject = {};
   for (const type of dataTypesArray) {
      dataTypesToClearObject[type] = true;
   }
   const clearOptions = { since: oneHourAgo };
   console.log(`Clearing ${dataTypesArray.join(', ')} since ${new    Date(oneHourAgo).toISOString()}`); controlledframe.clearData(clearOptions, dataTypesToClearObject) .then(() => {
   console.log('Specified partition data cleared successfully.');
}).catch((error) => {
   console.error('Error clearing specified partition data:', error);
});
}

Mở rộng hoặc sửa đổi các ứng dụng bên thứ ba

Ngoài việc nhúng đơn giản, Khung được kiểm soát còn cung cấp các cơ chế để IWA nhúng có thể kiểm soát nội dung web được nhúng của bên thứ ba. Bạn có thể thực thi tập lệnh trong nội dung được nhúng, chặn các yêu cầu mạng và ghi đè trình đơn theo bối cảnh mặc định – tất cả đều trong một môi trường biệt lập, an toàn.

Trường hợp sử dụng

  • Thực thi việc xây dựng thương hiệu trên các trang web của bên thứ ba: Chèn CSS và JavaScript tuỳ chỉnh vào các trang web được nhúng để áp dụng một giao diện trực quan thống nhất.
  • Hạn chế hành vi điều hướng và liên kết: Chặn hoặc vô hiệu hoá một số hành vi của thẻ <a> bằng cách chèn tập lệnh.
  • Tự động khôi phục sau khi gặp sự cố hoặc không hoạt động: Giám sát nội dung được nhúng để phát hiện các trạng thái lỗi (ví dụ: màn hình trống, lỗi tập lệnh) và tải lại hoặc đặt lại phiên theo cách lập trình sau khi hết thời gian chờ.

Mã mẫu

Chèn tập lệnh: Sử dụng executeScript() để chèn JavaScript vào khung được kiểm soát, cho phép bạn tuỳ chỉnh hành vi, thêm lớp phủ hoặc trích xuất dữ liệu từ các trang được nhúng của bên thứ ba. Bạn có thể cung cấp mã nội tuyến dưới dạng một chuỗi hoặc tham chiếu một hoặc nhiều tệp kịch bản (bằng cách sử dụng đường dẫn tương đối trong gói IWA). Phương thức này trả về một lời hứa phân giải thành kết quả thực thi của tập lệnh – thường là giá trị của câu lệnh cuối cùng.

document.getElementById('scriptBtn').addEventListener('click', () => {
   controlledframe.executeScript({
      code: `document.body.style.backgroundColor = 'lightblue';
             document.querySelectorAll('a').forEach(link =>    link.style.pointerEvents = 'none');
             document.title; // Return a value
            `,
      // You can also inject files:
      // files: ['./injected_script.js'],
}) .then((result) => {
   // The result of the last statement in the script is usually returned.
   console.log('Script execution successful. Result (e.g., page title):', result); }).catch((error) => {
   console.error('Script execution failed:', error);
   });
});

Chèn kiểu: Sử dụng insertCSS() để áp dụng các kiểu tuỳ chỉnh cho các trang được tải trong Khung được kiểm soát.

document.getElementById('cssBtn').addEventListener('click', () => {
  controlledframe.insertCSS({
    code: `body { font-family: monospace; }`
    // You can also inject files:
    // files: ['./injected_styles.css']
  })
  .then(() => {
    console.log('CSS injection successful.');
  })
  .catch((error) => {
    console.error('CSS injection failed:', error);
  });
});

Chặn yêu cầu mạng: Sử dụng WebRequest API để quan sát và có thể sửa đổi các yêu cầu mạng từ trang được nhúng, chẳng hạn như chặn yêu cầu, thay đổi tiêu đề hoặc ghi nhật ký việc sử dụng.

// Get the request object
const webRequest = controlledframe.request;

// Create an interceptor for a specific URL pattern
const interceptor = webRequest.createWebRequestInterceptor({
  urlPatterns: ["*://evil.com/*"],
  blocking: true,
  includeHeaders: "all"
});

// Add a listener to block the request
interceptor.addEventListener("beforerequest", (event) => {
  console.log('Blocking request to:', event.url);
  event.preventDefault();
});

// Add a listener to modify request headers
interceptor.addEventListener("beforesendheaders", (event) => {
  console.log('Modifying headers for:', event.url);
  const newHeaders = new Headers(event.headers);
  newHeaders.append('X-Custom-Header', 'MyValue');
  event.setRequestHeaders(newHeaders);
});

Thêm trình đơn theo bối cảnh tuỳ chỉnh: Sử dụng API contextMenus để thêm, xoá và xử lý trình đơn nhấp chuột phải tuỳ chỉnh trong khung được nhúng. Ví dụ này cho thấy cách thêm một trình đơn "Sao chép vùng chọn" tuỳ chỉnh vào bên trong Khung được kiểm soát. Khi văn bản được chọn và người dùng nhấp chuột phải, trình đơn sẽ xuất hiện. Khi nhấp vào nút này, văn bản đã chọn sẽ được sao chép vào bảng nhớ tạm, cho phép người dùng tương tác đơn giản và thân thiện trong nội dung được nhúng.

const menuItemProperties = {
  id: "copy-selection",
  title: "Copy selection",
  contexts: ["selection"],
  documentURLPatterns: [new URLPattern({ hostname: '*.example.com'})]
};

// Create the context menu item using a promise
try {
  await controlledframe.contextMenus.create(menuItemProperties);
  console.log(`Context menu item "${menuItemProperties.id}" created successfully.`);
} catch (error) {
  console.error(`Failed to create context menu item:`, error);
}

// Add a standard event listener for the 'click' event
controlledframe.contextMenus.addEventListener('click', (event) => {
    if (event.menuItemId === "copy-selection" && event.selectionText) {
        navigator.clipboard.writeText(event.selectionText)
          .then(() => console.log("Text copied to clipboard."))
          .catch(err => console.error("Failed to copy text:", err));
    }
});

Bản minh hoạ

Hãy xem bản minh hoạ Controlled Frame để biết thông tin tổng quan về các phương thức của Controlled Frame.

Bản minh hoạ Controlled Frame

Ngoài ra, IWA Kitchen Sink có một ứng dụng với nhiều thẻ, mỗi thẻ minh hoạ một API IWA khác nhau, chẳng hạn như Khung được kiểm soát, Direct Sockets và nhiều API khác.

IWA Kitchen Sink

Kết luận

Khung được kiểm soát cung cấp một cách mạnh mẽ và an toàn để nhúng, mở rộng và tương tác với nội dung web của bên thứ ba trong Ứng dụng web tách biệt (IWA). Bằng cách khắc phục những hạn chế của iframe, chúng cho phép các chức năng mới như thực thi tập lệnh bên trong nội dung được nhúng, chặn các yêu cầu mạng và triển khai trình đơn theo bối cảnh tuỳ chỉnh – tất cả đều duy trì các ranh giới cách ly nghiêm ngặt. Tuy nhiên, vì các API này cung cấp khả năng kiểm soát sâu đối với nội dung được nhúng, nên chúng cũng đi kèm với các ràng buộc bảo mật bổ sung và chỉ có trong IWA. IWA được thiết kế để đảm bảo chắc chắn hơn cho cả người dùng và nhà phát triển. Đối với hầu hết các trường hợp sử dụng, trước tiên, nhà phát triển nên cân nhắc sử dụng các phần tử <iframe> tiêu chuẩn. Các phần tử này đơn giản hơn và đủ dùng trong nhiều trường hợp. Bạn nên đánh giá Controlled Frames khi các giải pháp dựa trên iframe bị chặn do các hạn chế về việc nhúng hoặc thiếu các chức năng kiểm soát và tương tác cần thiết. Cho dù bạn đang xây dựng trải nghiệm tại ki-ốt, tích hợp các công cụ của bên thứ ba hay thiết kế hệ thống trình bổ trợ theo mô-đun, Controlled Frame API đều cho phép kiểm soát chi tiết trong một môi trường có cấu trúc, được cấp quyền và bảo mật. Nhờ đó, API này trở thành một công cụ quan trọng trong thế hệ tiếp theo của các ứng dụng web nâng cao.

Các tài nguyên khác