Chặn truy cập theo vị trí IP

Tác giả:Lisa Farrell · 2026-01-18

Giải pháp kiểm soát truy cập phía frontend dựa trên vị trí IP: khi trang được tải, trang sẽ gửi yêu cầu đến https://my.ipin.io/info để lấy mã quốc gia của người truy cập (country). Khi country được nhận diện là TW (hoặc các khu vực khác do bạn cấu hình), trang sẽ không tiếp tục cung cấp nội dung bình thường nữa mà trực tiếp kết thúc luồng truy cập ngay trên frontend.

Nhiều nhu cầu “hạn chế truy cập theo khu vực” thực ra không nhằm xây dựng bảo mật mạnh (kiểu chống hacker), mà thuộc về các tình huống frontend phổ biến hơn:

  • Một số quốc gia/khu vực tạm thời không cung cấp dịch vụ (do tuân thủ, bản quyền, thanh toán, logistics, v.v.)
  • Muốn kiểm tra khu vực ngay khi người dùng vào trang, rồi mới quyết định có hiển thị nội dung/nút/liên kết tải xuống hay không
  • Chiến dịch vận hành/marketing chỉ nhắm đến một số khu vực; các khu vực khác cần được thông báo trực tiếp “ngoài phạm vi dịch vụ” hoặc “không khả dụng”

Cách làm này đặc biệt phù hợp cho frontend: trình duyệt gửi trực tiếp GET https://my.ipin.io/info, API sẽ trả về thông tin địa lý dựa trên “IP khởi tạo yêu cầu”, ví dụ:

{"ip":"185.220.236.7","country":"TW","region":"Taiwan","city":"Taipei"}

Sau đó chỉ cần kiểm tra `country === "TW"` là có thể thực hiện hành động “chặn truy cập” (ví dụ: hiển thị thông báo cấm truy cập, hiển thị nội dung 404, hoặc dùng cấu trúc trang lỗi do bạn tự thiết kế để ghi đè lên trang gốc). Điểm mấu chốt ở đây là: chúng ta không bàn về “chuyển hướng”, mà trình bày rõ ràng kết quả truy cập dưới dạng lỗi hoặc bị từ chối, để người dùng nhìn thấy trạng thái cuối cùng là “không thể truy cập”.

1) Logic và nguyên lý hoạt động (2 đoạn)

Đoạn 1: Luồng xử lý
Người dùng mở trang → trình duyệt lập tức yêu cầu https://my.ipin.io/info → nhận `country` → nếu trùng với danh sách chặn (ví dụ `TW`) thì chặn ngay phần hiển thị tiếp theo của trang: không render nội dung bình thường nữa mà trực tiếp xuất “cấm truy cập / 404 / lỗi tùy chỉnh” như một trạng thái cuối; nếu không trùng thì tiếp tục tải bình thường các chức năng và nội dung.

Đoạn 2: Vì sao frontend phù hợp với API này
Vì trình duyệt gọi trực tiếp API bên thứ ba, nên bên thứ ba nhìn thấy “IP công khai” của người dùng; quốc gia/khu vực trả về sẽ tương ứng với chính người truy cập — đây đúng là hiệu ứng bạn cần.

2) Triển khai mã (càng đơn giản càng tốt, ưu tiên frontend)

2.1 Đơn giản nhất: trùng là “cấm truy cập” (khuyến nghị)

Đặt đoạn này trong <head> của trang (càng sớm càng tốt). Khi trùng, nó ghi đè nội dung trang và hiển thị thông báo “cấm truy cập” rõ ràng. Bạn có thể thay văn bản bằng thông báo tuân thủ hoặc phạm vi dịch vụ của bạn.

Ví dụ A: chặn nếu country === "TW"

<script>
(async function () {
  try {
    const info = await fetch("https://my.ipin.io/info").then(r => r.json());
    if (info.country === "TW") {
      document.documentElement.innerHTML =
        "<head><title>403 Forbidden</title></head>" +
        "<body style='font-family:system-ui;padding:40px'>" +
        "<h1>403 Cấm truy cập</h1>" +
        "<p>Dịch vụ hiện không khả dụng tại khu vực của bạn.</p>" +
        "</body>";
    }
  } catch (e) {
    // Nếu thất bại thì mặc định cho phép (bạn có thể đổi thành mặc định chặn)
  }
})();
</script>

Ví dụ B: chặn nếu KHÔNG phải TW (chỉ cho phép TW)

<script>
(async function () {
  try {
    const info = await fetch("https://my.ipin.io/info").then(r => r.json());
    if (info.country !== "TW") {
      document.documentElement.innerHTML =
        "<head><title>403 Forbidden</title></head>" +
        "<body style='font-family:system-ui;padding:40px'>" +
        "<h1>403 Cấm truy cập</h1>" +
        "<p>Trang này chỉ khả dụng tại một số khu vực được chọn.</p>" +
        "</body>";
    }
  } catch (e) {
    // Nếu thất bại thì mặc định cho phép (bạn có thể đổi thành mặc định chặn)
  }
})();
</script>

Ví dụ C: chặn nếu TW, US... (danh sách chặn)

<script>
(async function () {
  const blocked = ["TW", "US"]; // Trùng bất kỳ phần tử nào thì chặn
  try {
    const info = await fetch("https://my.ipin.io/info").then(r => r.json());
    if (blocked.includes(info.country)) {
      document.documentElement.innerHTML =
        "<head><title>403 Forbidden</title></head>" +
        "<body style='font-family:system-ui;padding:40px'>" +
        "<h1>403 Cấm truy cập</h1>" +
        "<p>Dịch vụ hiện không khả dụng tại khu vực của bạn.</p>" +
        "</body>";
    }
  } catch (e) {}
})();
</script>

2.2 Hiển thị 404 Not Found (cách thể hiện “không khả dụng/không tồn tại”)

Một số sản phẩm muốn với một số khu vực thì trang trông như “không tồn tại” thay vì nói rõ “bị hạn chế”. Khi đó bạn có thể ghi đè nội dung bằng cấu trúc trang lỗi mang ngữ nghĩa 404. Lưu ý: đây vẫn chỉ là hiển thị phía frontend và không thực sự thay đổi mã trạng thái HTTP mà máy chủ trả về, nhưng với trải nghiệm truy cập trang thì đã đủ trực quan.

<script>
(async function () {
  try {
    const { country } = await fetch("https://my.ipin.io/info").then(r => r.json());
    if (country === "TW") {
      document.documentElement.innerHTML =
        "<head><title>404 Not Found</title></head>" +
        "<body style='font-family:system-ui;padding:40px'>" +
        "<h1>404 Not Found</h1>" +
        "<p>The requested URL was not found on this server.</p>" +
        "</body>";
    }
  } catch (e) {}
})();
</script>

2.3 Danh sách chặn dạng mảng (gần với nghiệp vụ thực tế)

Trong thực tế thường không chỉ chặn một khu vực mà sẽ duy trì một danh sách chặn. Ví dụ dưới đây vẫn giữ tối giản: trùng bất kỳ phần tử nào thì hiển thị ngay “cấm truy cập”.

<script>
(async function () {
  const blocked = ["TW"]; // Mã quốc gia/khu vực cần chặn
  try {
    const info = await fetch("https://my.ipin.io/info").then(r => r.json());
    if (blocked.includes(info.country)) {
      document.documentElement.innerHTML =
        "<head><title>403 Forbidden</title></head>" +
        "<body style='font-family:system-ui;padding:40px'>" +
        "<h1>403 Cấm truy cập</h1>" +
        "<p>Dịch vụ hiện không khả dụng tại khu vực của bạn.</p>" +
        "</body>";
    }
  } catch (e) {}
})();
</script>

2.4 React (bản ngắn nhất có thể dùng: trùng thì hiển thị trang lỗi)

import { useEffect, useState } from "react";

export default function App() {
  const [blocked, setBlocked] = useState(false);

  useEffect(() => {
    (async () => {
      try {
        const info = await fetch("https://my.ipin.io/info").then(r => r.json());
        if (info.country === "TW") setBlocked(true);
      } catch (e) {}
    })();
  }, []);

  if (blocked) {
    return (
      <div style={{ fontFamily: "system-ui", padding: 40 }}>
        <h1>403 Cấm truy cập</h1>
        <p>Dịch vụ hiện không khả dụng tại khu vực của bạn.</p>
      </div>
    );
  }

  return <div>...</div>;
}

2.5 Vue (tương tự ngắn nhất: trùng thì hiển thị nội dung lỗi)

<script>
export default {
  data() {
    return { blocked: false };
  },
  async mounted() {
    try {
      const info = await fetch("https://my.ipin.io/info").then(r => r.json());
      if (info.country === "TW") this.blocked = true;
    } catch (e) {}
  }
}
</script>

<template>
  <div v-if="blocked" style="font-family:system-ui;padding:40px">
    <h1>403 Cấm truy cập</h1>
    <p>Dịch vụ hiện không khả dụng tại khu vực của bạn.</p>
  </div>

  <div v-else>...</div>
</template>

3) Triển khai trong WordPress / Joomla / Magento / Shopify (cách frontend + code)

Ý tưởng ở đâu cũng giống nhau: chèn một đoạn “JS siêu đơn giản” vào Head dùng chung của toàn site (hoặc file layout). Vì mục tiêu là “kiểm soát frontend + hiển thị lỗi”, nên chỉ cần đảm bảo script chạy đủ sớm, bạn có thể hoàn tất kiểm tra khu vực trước khi phần thân trang render; nếu trùng thì hiển thị thẳng “cấm truy cập/404/thông báo lỗi” như trạng thái cuối.

Khuyến nghị dùng thống nhất đoạn này (bản “cấm truy cập” tối giản):

<script>
(async function () {
  try {
    const info = await fetch("https://my.ipin.io/info").then(r => r.json());
    if (info.country === "TW") {
      document.documentElement.innerHTML =
        "<head><title>403 Forbidden</title></head>" +
        "<body style='font-family:system-ui;padding:40px'>" +
        "<h1>403 Cấm truy cập</h1>" +
        "<p>Dịch vụ hiện không khả dụng tại khu vực của bạn.</p>" +
        "</body>";
    }
  } catch (e) {}
})();
</script>

3.1 WordPress

  • Cách A (nhanh nhất): dùng plugin chèn script header/footer và dán vào khu vực Header (áp dụng toàn site)
  • Cách B: chèn script trước </head> trong file theme `header.php`

3.2 Joomla! / Joomla

  • Trong template hiện tại, tìm <head> trong `index.php` rồi dán script trước </head>
  • Hoặc dùng vị trí thêm code/HTML tùy chỉnh vào head trong phần quản trị/template (tùy template)

3.3 Magento (cách chèn ở frontend)

  • Nếu chỉ cần “chặn ở tầng hiển thị”, bạn có thể thêm script vào head toàn cục của theme (khu vực head của theme/layout)
  • Trong quản trị Magento thường có cấu hình kiểu “HTML Head” hoặc tương tự (tùy phiên bản/theme) để chèn nội dung vào <head> cho toàn site

> Vì mục tiêu là “hạn chế tối đa code phía server”, nên ở đây không mở rộng cách làm XML/module—chỉ cần chèn được script vào head là chạy được.

3.4 Shopify

  • Online Store → Themes → Edit code
  • Tìm `theme.liquid` và dán script trước </head> (áp dụng toàn site)

3.5 Các hệ thống khác (mẫu chung)

  • Bất kỳ hệ thống nào cho phép chỉnh sửa <head> toàn cục: đặt script càng sớm càng tốt trong head
  • Chỉ muốn giới hạn một số trang: chỉ chèn script vào template của các trang đó

4) Tổng kết

Cốt lõi của bài viết gói gọn trong một câu:

> Trình duyệt gọi https://my.ipin.io/info để lấy { country }. Nếu country === "TW" thì kết thúc truy cập trang ngay bằng cách hiển thị “cấm truy cập / 404 / lỗi tùy chỉnh”, từ đó triển khai giải pháp chặn truy cập theo khu vực IP ở phía frontend.

Giải pháp này chi phí thấp, triển khai nhanh, phù hợp cho “thông báo tuân thủ / thông báo không khả dụng theo khu vực / kiểm soát luồng truy cập ở frontend”. Trong nhiều nghiệp vụ, việc “hiển thị lỗi” ở lớp frontend đã đủ để đáp ứng mục tiêu sản phẩm và vận hành: người dùng hiểu rõ trang không khả dụng tại khu vực của họ, đồng thời tránh tiếp tục lộ nội dung chính và các điểm tương tác. Nếu bạn cần mức chặn “khó обход hơn”, hãy bổ sung thêm lớp chặn ở server hoặc CDN.

5) Câu hỏi thường gặp (FAQ)

Q1: Chặn ở frontend có thực sự “cấm truy cập” được không?

Chính xác hơn: nó giúp lớp hiển thị trả về một kết quả từ chối rõ ràng (ví dụ trang 403/404/thông báo lỗi), ngăn người dùng thông thường tiếp tục sử dụng tính năng của trang. Tuy nhiên, người có kinh nghiệm có thể tắt JavaScript hoặc sửa script, nên đây không phải “tường bảo mật không thể bypass”. Nếu bạn cần “cấm tuyệt đối”, hãy chặn thêm ở server hoặc CDN. Nếu mục tiêu là thông báo tuân thủ và trạng thái không khả dụng, chặn ở frontend thường đã đủ.

Q2: Vì sao phải đặt script trong <head> và càng sớm càng tốt?

Vì càng kiểm tra sớm thì càng có thể kết thúc luồng truy cập trước khi nội dung chính render, giảm hiện tượng “thoáng thấy nội dung rồi mới bị chặn”. Đồng thời tránh tải tài nguyên không cần thiết (script, ảnh, component). Đây là vị trí hợp lý cho trải nghiệm người dùng và hiệu năng.

Q3: Nếu yêu cầu tới https://my.ipin.io/info bị lỗi thì sao?

Chiến lược phổ biến nhất là “lỗi thì mặc định cho phép”, để sự cố API bên ngoài không làm chặn nhầm người dùng bình thường. Một chiến lược khác là “lỗi thì mặc định từ chối”, phù hợp với bối cảnh tuân thủ nghiêm ngặt. Bạn có thể chọn theo rủi ro nghiệp vụ. Dù chọn cách nào, nên giữ logic đơn giản: lấy được country thì kiểm tra, không lấy được thì áp dụng chính sách mặc định.

Q4: Có thể gặp vấn đề CORS không?

Có thể. Nếu API không cho phép truy cập cross-origin từ trình duyệt, fetch sẽ bị chặn. Khi đó bạn cần đảm bảo API bật CORS (ví dụ cho phép domain của bạn), nếu không trình duyệt sẽ không đọc được JSON trả về. Với hướng “frontend-first”, CORS là điểm quan trọng nhất cần xác nhận trước.

Q5: Giá trị country theo tiêu chuẩn nào?

Thường là mã quốc gia/khu vực 2 ký tự (thường là ISO 3166-1 alpha-2). TW là ví dụ điển hình. Khi sử dụng thực tế, nên thống nhất mã in hoa và giữ danh sách chặn cùng định dạng để tránh lỗi do khác biệt chữ hoa/thường.

Q6: Làm sao đổi từ “chặn một quốc gia” sang “chỉ cho phép một số quốc gia”?

Chỉ cần đảo logic:
- allowlist: chỉ cho phép ["US","JP"], còn lại hiển thị “cấm truy cập/lỗi”
- blocklist: chỉ chặn ["TW"], còn lại cho phép
Cả hai đều là kiểm tra country có nằm trong danh sách hay không; khác nhau nằm ở hành vi mặc định.