跳到主要內容

如何使用投票器來檢查使用者權限

編輯此頁面

投票器是 Symfony 管理權限最強大的方式。它們讓您可以集中所有權限邏輯,然後在許多地方重複使用。

然而,如果您不重複使用權限或您的規則很基本,您可以始終將該邏輯直接放在您的控制器中。 這裡有一個範例說明這看起來會像什麼樣子,如果您只想讓「擁有者」可以存取路由

1
2
3
4
5
6
7
// src/Controller/PostController.php
// ...

// inside your controller action
if ($post->getOwner() !== $this->getUser()) {
    throw $this->createAccessDeniedException();
}

從這個意義上說,本頁面中使用的以下範例是投票器的最小範例。

以下是 Symfony 如何與投票器協作:每次您在 Symfony 的授權檢查器上使用 isGranted() 方法,或在控制器中呼叫 denyAccessUnlessGranted() (使用授權檢查器),或透過存取控制時,都會呼叫所有投票器。

最終,Symfony 會根據應用程式中定義的策略,採用所有投票器的回應並做出最終決定 (允許或拒絕存取資源),策略可以是:肯定、共識、一致或優先。

投票器介面

自訂投票器需要實作 VoterInterface 或擴充 Voter,這讓建立投票器變得更容易

1
2
3
4
5
6
7
8
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;

abstract class Voter implements VoterInterface
{
    abstract protected function supports(string $attribute, mixed $subject): bool;
    abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool;
}

提示

對於執行大量權限檢查的應用程式而言,多次檢查每個投票器可能會很耗時。 為了在這些情況下提高效能,您可以讓您的投票器實作 CacheableVoterInterface。 這讓存取決策管理器可以記住投票器支援的屬性和主體類型,以便每次只呼叫必要的投票器。

設定:在控制器中檢查存取權限

假設您有一個 Post 物件,並且需要決定目前使用者是否可以編輯檢視該物件。 在您的控制器中,您將使用如下程式碼檢查存取權限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Controller/PostController.php

// ...
use Symfony\Component\Security\Http\Attribute\IsGranted;

class PostController extends AbstractController
{
    #[Route('/posts/{id}', name: 'post_show')]
    // check for "view" access: calls all voters
    #[IsGranted('view', 'post')]
    public function show(Post $post): Response
    {
        // ...
    }

    #[Route('/posts/{id}/edit', name: 'post_edit')]
    // check for "edit" access: calls all voters
    #[IsGranted('edit', 'post')]
    public function edit(Post $post): Response
    {
        // ...
    }
}

#[IsGranted] 屬性或 denyAccessUnlessGranted() 方法 (以及 isGranted() 方法) 會呼叫「投票器」系統。 目前,沒有投票器會針對使用者是否可以「檢視」或「編輯」Post 進行投票。 但是您可以建立自己的投票器,使用您想要的任何邏輯來決定。

建立自訂投票器

假設決定使用者是否可以「檢視」或「編輯」Post 物件的邏輯非常複雜。 例如,User 始終可以編輯或檢視他們建立的 Post。 而且如果 Post 被標記為「公開」,則任何人都可以檢視它。 這種情況下的投票器會如下所示

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// src/Security/PostVoter.php
namespace App\Security;

use App\Entity\Post;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends Voter
{
    // these strings are just invented: you can use anything
    const VIEW = 'view';
    const EDIT = 'edit';

    protected function supports(string $attribute, mixed $subject): bool
    {
        // if the attribute isn't one we support, return false
        if (!in_array($attribute, [self::VIEW, self::EDIT])) {
            return false;
        }

        // only vote on `Post` objects
        if (!$subject instanceof Post) {
            return false;
        }

        return true;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();

        if (!$user instanceof User) {
            // the user must be logged in; if not, deny access
            return false;
        }

        // you know $subject is a Post object, thanks to `supports()`
        /** @var Post $post */
        $post = $subject;

        return match($attribute) {
            self::VIEW => $this->canView($post, $user),
            self::EDIT => $this->canEdit($post, $user),
            default => throw new \LogicException('This code should not be reached!')
        };
    }

    private function canView(Post $post, User $user): bool
    {
        // if they can edit, they can view
        if ($this->canEdit($post, $user)) {
            return true;
        }

        // the Post object could have, for example, a method `isPrivate()`
        return !$post->isPrivate();
    }

    private function canEdit(Post $post, User $user): bool
    {
        // this assumes that the Post object has a `getOwner()` method
        return $user === $post->getOwner();
    }
}

就是這樣! 投票器完成了! 接下來,設定它

總而言之,以下是兩個抽象方法的預期

Voter::supports(string $attribute, mixed $subject)
當呼叫 isGranted() (或 denyAccessUnlessGranted()) 時,第一個引數會作為 $attribute 傳遞到此處 (例如 ROLE_USERedit),第二個引數 (如果有的話) 會作為 $subject 傳遞 (例如 nullPost 物件)。 您的工作是確定您的投票器是否應該對屬性/主體組合進行投票。 如果您傳回 true,則會呼叫 voteOnAttribute()。 否則,您的投票器就完成了:應該由其他投票器處理。 在此範例中,如果屬性是 viewedit,且物件是 Post 實例,則傳回 true
voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token)
如果您從 supports() 傳回 true,則會呼叫此方法。 您的工作是傳回 true 以允許存取,並傳回 false 以拒絕存取。 $token 可用於尋找目前的使用者物件 (如果有的話)。 在此範例中,包含所有複雜的業務邏輯來確定存取權限。

設定投票器

若要將投票器注入到安全性層,您必須將其宣告為服務並標記為 security.voter。 但是如果您使用預設的 services.yaml 設定,這會自動為您完成! 當您使用 view/edit 呼叫 isGranted() 並傳遞 Post 物件時,將會呼叫您的投票器,並且您可以控制存取權限。

在投票器內檢查角色

如果您想從投票器內部呼叫 isGranted() 怎麼辦?例如,您想查看目前使用者是否具有 ROLE_SUPER_ADMIN。 這可以透過在您的投票器內使用存取決策管理器來實現。 例如,您可以使用它來始終允許具有 ROLE_SUPER_ADMIN 的使用者存取

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
// src/Security/PostVoter.php

// ...
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;

class PostVoter extends Voter
{
    // ...

    public function __construct(
        private AccessDecisionManagerInterface $accessDecisionManager,
    ) {
    }

    protected function voteOnAttribute($attribute, mixed $subject, TokenInterface $token): bool
    {
        // ...

        // ROLE_SUPER_ADMIN can do anything! The power!
        if ($this->accessDecisionManager->decide($token, ['ROLE_SUPER_ADMIN'])) {
            return true;
        }

        // ... all the normal voter logic
    }
}

警告

在先前的範例中,請避免使用以下程式碼來檢查是否已授予角色權限

1
2
3
4
5
6
7
// DON'T DO THIS
use Symfony\Component\Security\Core\Security;
// ...

if ($this->security->isGranted('ROLE_SUPER_ADMIN')) {
    // ...
}

投票器內的 Security::isGranted() 方法有一個顯著的缺點:它不保證檢查是在與投票器中相同的權杖上執行的。 權杖儲存體中的權杖可能已經變更或可能在此期間變更。 請始終改用 AccessDecisionManager

如果您使用預設的 services.yaml 設定,您就完成了! Symfony 將在實例化您的投票器時自動傳遞 security.helper 服務 (由於自動連線)。

變更存取決策策略

通常,任何給定時間只有一個投票器會投票 (其餘的會「棄權」,這表示他們從 supports() 傳回 false)。 但在理論上,您可以讓多個投票器針對一個動作和物件進行投票。 例如,假設您有一個投票器檢查使用者是否為網站的成員,另一個投票器檢查使用者是否年滿 18 歲。

為了處理這些情況,存取決策管理器使用您可以設定的「策略」。 有四種策略可用

affirmative (預設)
只要有一個投票器授予存取權限,就會授予存取權限;
consensus
如果授予存取權限的投票器多於拒絕的投票器,則會授予存取權限。 如果票數相同,則決策基於 allow_if_equal_granted_denied 設定選項 (預設為 true);
unanimous
只有在沒有投票器拒絕存取權限時,才會授予存取權限。
priority
這會根據第一個不棄權的投票器的服務優先順序授予或拒絕存取權限;

無論選擇哪種策略,如果所有投票器都棄權投票,則決策基於 allow_if_all_abstain 設定選項 (預設為 false)。

在上述情況下,為了授予使用者讀取貼文的存取權限,兩個投票器都應該授予存取權限。 在這種情況下,預設策略不再有效,應改用 unanimous。 您可以在安全性設定中設定此項

1
2
3
4
5
# config/packages/security.yaml
security:
    access_decision_manager:
        strategy: unanimous
        allow_if_all_abstain: false

自訂存取決策策略

如果沒有內建策略適合您的使用案例,請定義 strategy_service 選項以使用自訂服務 (您的服務必須實作 AccessDecisionStrategyInterface)

1
2
3
4
5
# config/packages/security.yaml
security:
    access_decision_manager:
        strategy_service: App\Security\MyCustomAccessDecisionStrategy
        # ...

自訂存取決策管理器

如果您需要提供完全自訂的存取決策管理器,請定義 service 選項以使用自訂服務作為存取決策管理器 (您的服務必須實作 AccessDecisionManagerInterface)

1
2
3
4
5
# config/packages/security.yaml
security:
    access_decision_manager:
        service: App\Security\MyCustomAccessDecisionManager
        # ...

變更傳回的訊息和狀態碼

預設情況下,#[IsGranted] 屬性將會拋出 AccessDeniedException,並傳回 http 403 狀態碼,訊息為 Access Denied

但是,您可以透過指定傳回的訊息和狀態碼來變更此行為

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Controller/PostController.php

// ...
use Symfony\Component\Security\Http\Attribute\IsGranted;

class PostController extends AbstractController
{
    #[Route('/posts/{id}', name: 'post_show')]
    #[IsGranted('show', 'post', 'Post not found', 404)]
    public function show(Post $post): Response
    {
        // ...
    }
}

提示

如果狀態碼與 403 不同,則會改為拋出 HttpException

本作品,包括程式碼範例,均根據 Creative Commons BY-SA 3.0 授權條款授權。
目錄
    版本