網路上有人提到這個問題,整理了一下自己提出的回應寫成這篇提供參考。一般做法通常使用資料表紀錄人與權限的關聯,甚至是有群組和角色等更複雜的機制,在檢查是否具有權限時可能要 Join 許多 Table 來達成。而此篇文章則以不同的方式來設計,簡單的以位元的方式來表示使用者具有之權限,並使用 php 與 mysql 實作範例,範例不包含群組和角色等機制。

首先建立一權限資料表包含 index 欄位表示第幾個 bit,型別為 int;name 欄位表示權限的名稱,型別為 varchar,並建立一些權限,例如:

Table: authorities

indexname
1Authority1
2Authority2

bigint 版本

接著於使用者資料表包含一欄位表示具有之權限,例如 authority, 型別為 bigint unsigned,並建立一些使用者,例如:

Table: users

idauthority
17
21
318446744073709551615

Case 1

要檢驗使用者1是否具有 Authority2 權限邏輯如下:

1
2
3
4
              二進位   十進位
使用者1: 0000 0111 (7)
Authority2: 0000 0010 (2)
AND: 0000 0010

AND 結果為 0000 0010,與權限位元一致,則表示具有權限。

Case 2

要檢驗使用者2 是否具有 Authority2 權限邏輯如下:

1
2
3
4
              二進位   十進位
使用者2: 0000 0001 (1)
Authority2: 0000 0010 (2)
AND: 0000 0000

AND結果為 0000 0000,則表示不具有權限。

Case 3

要檢驗使用者1 是否具有 Authority1 和 Authority3 權限邏輯如下:

1
2
3
4
5
6
7
8
9
              二進位   十進位
Authority1: 0000 0001 (1)
Authority3: 0000 0100 (3)
OR: 0000 0101

二進位 十進位
使用者1: 0000 0111 (7)
Authorities: 0000 0101
AND: 0000 0101

要同時判斷是否具有多個權限時,先將所有權限作位元運算之 OR,結果為 0000 0101;接著再與使用者權限作 AND,結果仍然為 0000 0101,則表示具有權限。

Case 4

要檢驗使用者2 是否具有 Authority1 和 Authority3 權限邏輯如下:

1
2
3
4
               二進位   十進位
使用者2: 0000 0001 (1)
Authorities: 0000 0101
AND: 0000 0001

權限 OR 部分與 case3 相同,接著與使用者權限 AND 結果為 0000 0001,與權限位元不一致,則表示不具有權限。

由於 bigint 佔 64 個 bit,所以此版本最多支援 64 種權限,而此範例的使用者3 權限 18446744073709551615 相當於所有 bit 皆為 1,也就是具有所有權限。

實際的使用 PHP 實作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
$result = mysql_query('SELECT * FROM authorities');
while ($row = mysql_fetch_assoc($result))
$authorities[$row['name']] = 1 << ($row['index'] - 1); // 先將 index 轉成位元實際的數值

$result = mysql_query('SELECT * FROM users');
echo "<pre>";
while ($row = mysql_fetch_assoc($result)) {
echo "user" . $row['id'] . ":\n";
var_dump(has_authority($row['authority'], $authorities['Authority1']));
var_dump(has_authority($row['authority'], $authorities['Authority2']));
var_dump(has_authority($row['authority'], $authorities['Authority4']));
// 判斷多個權限,將權限作 OR
var_dump(has_authority($row['authority'], $authorities['Authority1'] | $authorities['Authority3']));
var_dump(has_authority($row['authority'], $authorities['Authority1'] | $authorities['Authority4']));
echo "\n";
}
echo "</pre>";

function has_authority($user_authority, $authority)
{
return ($user_authority & $authority) == $authority;
}
?>

輸出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
user1:
bool(true)
bool(true)
bool(false)
bool(true)
bool(false)

user2:
bool(true)
bool(false)
bool(false)
bool(false)
bool(false)

user3:
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)

varbinary 版本

使用者資料表之 authority 型別改成使用 varbinary,以下使用十六進位制表示

idauthority
101
207
3FF FF FF FF FF FF FF FF
4FF FF FF FF FF FF FF FF 01

邏輯與 bigint 相同,但是此版本可超過 64 bit 長度的限制,可自訂 varbinary 長度,換成 blob 型別亦可。

實際的使用 PHP 實作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php
$result = mysql_query('SELECT * FROM authorities');
while ($row = mysql_fetch_assoc($result))
$authorities[$row['name']] = $row['index']; // 此版本先不轉換

$result = mysql_query('SELECT * FROM users');

echo "<pre>";
while ($row = mysql_fetch_assoc($result)) {
echo "user" . $row['id'] . ":\n";
var_dump(has_authority($row['authority'], $authorities['Authority1']));
var_dump(has_authority($row['authority'], $authorities['Authority2']));
var_dump(has_authority($row['authority'], $authorities['Authority4']));
var_dump(has_authority($row['authority'], $authorities['Authority65'])); // 可超過 64
// 判斷多個權限,此版本使用 index array,於 function 中處理 OR
var_dump(has_authority($row['authority'], array($authorities['Authority1'], $authorities['Authority3'])));
var_dump(has_authority($row['authority'], array($authorities['Authority1'], $authorities['Authority4'])));
echo "\n";
}
echo "</pre>";

function has_authority($user_authority, $indexes)
{
if (!is_array($indexes))
$indexes = array($indexes);

// 將 index array 轉成 byte array 與處理 OR 運算
foreach ($indexes as $index)
{
$byte_index = ceil(($index) / 8) - 1;
$authority = 1 << (($index - 1) % 8);
if (isset($authorities[$byte_index]))
$authorities[$byte_index] |= $authority;
else
$authorities[$byte_index] = $authority;
}

foreach ($authorities as $byte_index => $authority)
{
$byte = ord($user_authority[$byte_index]);
if ($byte == 0)
return false;

if (($authority & $byte) != $authority)
return false;
}

return true;
}
?>

輸出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
user1:
bool(true)
bool(true)
bool(false)
bool(false)
bool(true)
bool(false)

user2:
bool(true)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)

user3:
bool(true)
bool(true)
bool(true)
bool(false)
bool(true)
bool(true)

user4:
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)