---
title: "Gom nhiều quyền vào một con số: bitmask cho quyền người dùng và địa chỉ IP"
url: https://phatdev.net/vi/blog/bitwise-in-practice
lang: vi
updated: 2026-10-21
alternate:
  en: https://phatdev.net/en/blog/bitwise-in-practice
markdown: https://phatdev.net/vi/blog/bitwise-in-practice.md
---

# Gom nhiều quyền vào một con số: bitmask cho quyền người dùng và địa chỉ IP

> Lưu cả chục quyền người dùng trong một cột số duy nhất, kiểm tra địa chỉ IP có thuộc dải mạng cho phép không, và những cái bẫy của JavaScript khi tính bằng bit.

Chuyên mục: Backend | Đăng: 2026-10-19 | Cấp độ: Trung cấp | 8 phút đọc

Tags: bitwise, bitmask, permissions, subnet, cidr, javascript, bigint, python

Bảng người dùng của bạn có các cột `can_read`, `can_write`, `can_delete`, `is_admin`. Tuần sau sếp bảo thêm quyền xuất file, thế là bạn thêm cột thứ năm, sửa API, sửa giao diện, và hy vọng không quên chỗ nào. Có một cách lưu cả năm quyền đó trong một con số duy nhất, không cần đổi bảng khi thêm quyền mới.

Cách đó gọi là bitmask (mặt nạ bit). Nghe giống thứ chỉ dân viết driver mới dùng, nhưng mình gặp nó thường xuyên khi làm web: lưu quyền, bật tắt tính năng, kiểm tra một địa chỉ IP có nằm trong dải được phép không.

![Hàng công tắc bật tắt và khung mặt nạ phủ lên một vài công tắc](https://phatdev.net/uploads/blog/bitwise-in-practice/bit-flags-1920.webp)

## Một con số, nhiều công tắc

Máy tính lưu số bằng các chữ số 0 và 1, mỗi chữ số gọi là một bit. Bạn hãy coi mỗi bit là một công tắc: 1 là bật, 0 là tắt.

Gán mỗi quyền một công tắc: đọc là công tắc số 0, ghi là số 1, xoá là số 2, quản trị là số 3. Con số `5` viết dưới dạng bit là `0101`, nghĩa là công tắc đọc và công tắc xoá đang bật. Chỉ một con số, nhưng chứa đủ bốn quyền.

Bạn đã gặp cách này rồi mà có thể không để ý. Lệnh `chmod 755` trên Linux chính là bitmask: đọc là 4, ghi là 2, chạy là 1, và `7 = 4 + 2 + 1` nghĩa là được cả ba.

## Bốn thao tác đủ dùng hằng ngày

Gọi `mask` là con số đang giữ các công tắc, và `flag` là công tắc bạn quan tâm. Bốn việc cần biết:

- Bật một quyền: `mask | flag`
- Tắt một quyền: `mask & ~flag`
- Đảo một quyền: `mask ^ flag`
- Hỏi "có quyền này không": `(mask & flag) === flag`

Bạn không cần hiểu từng ký hiệu ngay. Chỉ cần nhớ rằng `&` hoạt động như một cái khung mặt nạ: đặt lên con số, chỉ nhìn thấy công tắc nào nằm trong khung.

## Làm thế nào

1. Liệt kê quyền, gán mỗi quyền một số là luỹ thừa của 2: 1, 2, 4, 8, 16...
2. Đặt tên cho từng số trong code (`READ`, `WRITE`), đừng viết số trần.
3. Lưu cả con số vào một cột số nguyên trong database.
4. Mọi chỗ kiểm tra quyền đều đi qua hàm `has`, `grant`, `revoke` dùng chung, không viết lại phép bit rải rác.
5. Khi cần thêm quyền, chỉ thêm ở cuối (số tiếp theo trong dãy) và không bao giờ đổi số của quyền đã có. Nếu đổi, dữ liệu cũ trong database sẽ có nghĩa khác.

Một điểm dễ trượt: kiểm tra "có đủ cả hai quyền" khác với "có ít nhất một trong hai". Nhầm hai kiểu này nghĩa là cấp quyền rộng hơn dự định.

## Cho bạn nào viết code

Mình chạy lại toàn bộ trên Node 20, Python 3.12 và MariaDB 11.8.

JavaScript:

```js
export const Perm = Object.freeze({
  READ:   1 << 0, // 0001
  WRITE:  1 << 1, // 0010
  DELETE: 1 << 2, // 0100
  ADMIN:  1 << 3, // 1000
});

export const has    = (mask, flag) => (mask & flag) === flag;
export const grant  = (mask, flag) => mask | flag;
export const revoke = (mask, flag) => mask & ~flag;
export const toggle = (mask, flag) => mask ^ flag;

let user = Perm.READ | Perm.WRITE;   // 0011
user = grant(user, Perm.DELETE);     // 0111
user = revoke(user, Perm.WRITE);     // 0101
user = toggle(user, Perm.ADMIN);     // 1101
```

Các bước ra đúng `0011`, `0111`, `0101`, `1101`. Với người dùng ở trạng thái `0101`, `has(user, READ | WRITE)` ra `false` vì thiếu quyền ghi, còn `(user & (READ | WRITE)) !== 0` ra `true` vì có ít nhất một quyền. Hai kiểu hỏi khác nhau, chọn đúng kiểu bạn muốn.

Bẫy nguy hiểm nhất là dấu ngoặc. Trong JavaScript, `===` được tính trước `&`. Viết `user & Perm.WRITE === Perm.WRITE` bị hiểu thành `user & (Perm.WRITE === Perm.WRITE)`, tức `user & true`, tức `user & 1`. Mình thử với người dùng đã bị thu quyền ghi: hàm `has` trả `false` đúng, còn biểu thức thiếu ngoặc trả giá trị "đúng" vì công tắc đọc đang bật. Một lỗi phân quyền thật sự. Luôn bọc `(mask & flag)` trong ngoặc.

Python có sẵn `IntFlag` làm đúng việc này: `Perm.WRITE in user` để hỏi, `user |= Perm.DELETE` để cấp, `user &= ~Perm.WRITE` để thu, và `Perm(5)` dựng lại tập quyền từ con số lưu trong database (in ra `<Perm.READ|DELETE: 5>`).

Lưu trong database chỉ cần một cột `perms INT UNSIGNED`. Mình thử trên một database tạm với bốn người dùng:

```sql
SELECT name FROM users WHERE perms & 2 = 2;                -- ai có quyền ghi
UPDATE users SET perms = perms | 8 WHERE name = 'dung';    -- cấp quản trị
UPDATE users SET perms = perms & ~2 WHERE name = 'an';     -- thu quyền ghi
```

Ở MySQL và MariaDB, `&` được tính trước `=` nên câu đầu đúng như ý (khác với JavaScript). Cái giá: điều kiện `perms & 2 = 2` là phép tính trên cột nên database không dùng được index thường, và `EXPLAIN` báo `type: ALL`, tức đọc cả bảng. Bảng vài nghìn người dùng thì không sao. Với hàng triệu dòng cần lọc nhanh, hãy dùng cột riêng có index. Lý do chi tiết có trong bài [Truy vấn chậm? Cách đọc EXPLAIN và đặt index đúng chỗ](https://phatdev.net/vi/blog/mysql-index-explain).

## Dùng bit để kiểm tra địa chỉ IP

Địa chỉ IPv4 thực chất là một số 32 bit. Khi bạn thấy `192.168.10.77/24`, con số 24 nghĩa là 24 bit đầu chỉ ra "khu mạng", 8 bit sau chỉ ra "căn nhà trong khu đó". Muốn biết một IP có thuộc khu mạng đó không, bạn đặt cái khung mặt nạ gồm 24 bit 1 lên IP và so với khu mạng.

![Lưới 32 ô chia hai nhóm: phần mạng và phần máy](https://phatdev.net/uploads/blog/bitwise-in-practice/subnet-mask-1920.webp)

```js
export function ipToInt(ip) {
  const p = ip.split(".").map(Number);
  if (p.length !== 4 || p.some((x) => !Number.isInteger(x) || x < 0 || x > 255)) throw new Error("IPv4 không hợp lệ");
  return ((p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]) >>> 0;
}

export function prefixToMask(prefix) {
  if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) throw new Error("prefix 0..32");
  return prefix === 0 ? 0 : (0xFFFFFFFF << (32 - prefix)) >>> 0;
}

export function inCidr(ip, cidr) {
  const [base, p] = cidr.split("/");
  const mask = prefixToMask(Number(p));
  return ((ipToInt(ip) & mask) >>> 0) === ((ipToInt(base) & mask) >>> 0);
}
```

Chạy với `192.168.10.77/24` ra mạng `192.168.10.0`, mặt nạ `255.255.255.0`, 254 máy. Với `10.0.0.5/13` ra mặt nạ `255.248.0.0`. Mình còn so hàm này với module `ipaddress` của Python trên 2.000 ca ngẫu nhiên, không ca nào lệch.

Hai chỗ trong đoạn này không được bỏ. Một là `>>> 0`, vì JavaScript trả số có dấu 32 bit, nên không có nó thì `200.1.1.1` AND với `255.0.0.0` ra số âm `-939524096` và phép so sánh sai. Hai là nhánh `prefix === 0`, vì JavaScript lấy số bit dịch theo modulo 32, nên `x << 32` thành `x << 0` và `/0` ra mặt nạ toàn 1 thay vì toàn 0. Với IPv6 hoặc việc quan trọng, hãy dùng thư viện có sẵn (`ipaddress` của Python, `net.BlockList` của Node).

## Vì sao JavaScript hay làm bạn bất ngờ

JavaScript ép mọi phép bit về số nguyên 32 bit có dấu. Mình thử các trường hợp hay gặp:

- `1 << 31` ra `-2147483648`, và `1 << 32` ra `1`, không phải 4294967296.
- `2 ** 32 | 0` ra `0`. Số vượt 32 bit bị cắt âm thầm, nên dấu thời gian tính bằng mili giây đưa qua `| 0` sẽ hỏng.
- `~5` ra `-6`, không phải `10`.

Cần quá 32 bit thì dùng `BigInt`: `1n << 40n` ra `1099511627776n`. Lưu ý không trộn được BigInt với số thường, `1n | 1` ném `TypeError`. Python không dính các chuyện này vì số nguyên không giới hạn, nhưng `~5` cũng ra `-6`, và bạn phải tự `& 0xFF` nếu muốn mô phỏng độ rộng cố định.

## Lỗi hay gặp

Lỗi đau nhất là chèn quyền mới vào giữa dãy rồi đánh số lại cho gọn. Dữ liệu đã lưu giữ nguyên con số cũ, chỉ có ý nghĩa của số bị đổi, nên mọi người dùng cũ đổi quyền một cách âm thầm. Vì vậy mới có luật chỉ thêm ở cuối.

Một lỗi quen thuộc khác là dùng số không phải luỹ thừa của 2, ví dụ `3`, cho một quyền. Số `3` là hai công tắc cùng bật, nên quyền đó tự dính vào quyền khác.

Còn chuyện hay gặp trong bài toán IP: ai đó dùng `| 0` hay `>> 0` "cho gọn" rồi tự hỏi vì sao IP lớn bị so sai. Câu trả lời nằm ở phần JavaScript ở trên.

## Nên làm gì tiếp theo

Nếu bảng của bạn đang có nhiều cột true/false về quyền hoặc tính năng, thử gom chúng vào một cột bitmask cho dữ liệu nhỏ, và giữ hàm `has`, `grant`, `revoke` ở một chỗ duy nhất. Nếu quyền cần lọc nhanh trên bảng lớn, giữ cột riêng có index. Hai cách không loại trừ nhau, chọn theo chỗ cần.

## Nguồn tham khảo

- [MDN: Bitwise AND and other bitwise operators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_AND)
- [MDN: Operator precedence](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence)
- [MDN: BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt)
- [Python docs: enum.IntFlag](https://docs.python.org/3/library/enum.html#enum.IntFlag)
- [Python docs: ipaddress](https://docs.python.org/3/library/ipaddress.html)
- [Node.js docs: net.BlockList](https://nodejs.org/api/net.html#class-netblocklist)
- [MariaDB: Operator precedence](https://mariadb.com/kb/en/operator-precedence/)

## Bài liên quan

- [Truy vấn chậm? Cách đọc EXPLAIN và đặt index đúng chỗ](https://phatdev.net/vi/blog/mysql-index-explain.md)
- [Bắt đầu với Python năm 2026: cài đặt, môi trường riêng và chạy chương trình đầu tiên](https://phatdev.net/vi/blog/python-start-2026.md)
- [Tự động hoá việc văn phòng bằng Python: gộp Excel, làm báo cáo và gửi mail](https://phatdev.net/vi/blog/python-office-automation.md)
