权限控制 #

一、授权概述 #

1.1 授权概念 #

授权是确定已认证用户是否有权限执行特定操作的过程。

text
┌─────────────────────────────────────────────────────┐
│                  授权机制                            │
├─────────────────────────────────────────────────────┤
│                                                     │
│   用户请求 → 检查角色 → 检查权限 → 允许/拒绝        │
│                                                     │
│   授权方式:                                        │
│   • 角色检查 (ROLE_*)                               │
│   • 投票器 (Voter)                                  │
│   • 表达式 (Expression)                             │
│   • ACL (访问控制列表)                              │
│                                                     │
└─────────────────────────────────────────────────────┘

1.2 授权组件 #

组件 说明
AccessDecisionManager 访问决策管理器
Voter 投票器
RoleHierarchy 角色层次
ExpressionLanguage 表达式语言

二、角色控制 #

2.1 角色定义 #

yaml
# config/packages/security.yaml
security:
    role_hierarchy:
        ROLE_USER: []
        ROLE_EDITOR: [ROLE_USER]
        ROLE_ADMIN: [ROLE_EDITOR, ROLE_USER]
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

2.2 控制器角色检查 #

php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

class AdminController extends AbstractController
{
    #[Route('/admin', name: 'app_admin')]
    #[IsGranted('ROLE_ADMIN')]
    public function index(): Response
    {
        return $this->render('admin/index.html.twig');
    }

    #[Route('/admin/users', name: 'app_admin_users')]
    #[IsGranted('ROLE_ADMIN', message: '您没有访问权限')]
    public function users(): Response
    {
        return $this->render('admin/users.html.twig');
    }

    #[Route('/profile', name: 'app_profile')]
    public function profile(): Response
    {
        $this->denyAccessUnlessGranted('ROLE_USER');
        
        return $this->render('profile/index.html.twig');
    }
}

2.3 服务中角色检查 #

php
<?php

namespace App\Service;

use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

class AdminService
{
    public function __construct(
        private AuthorizationCheckerInterface $authorizationChecker
    ) {}

    public function doSomething(): void
    {
        if (!$this->authorizationChecker->isGranted('ROLE_ADMIN')) {
            throw new \Exception('Access denied');
        }
        
        // 执行管理员操作
    }
}

2.4 模板中角色检查 #

twig
{% if is_granted('ROLE_ADMIN') %}
    <a href="{{ path('app_admin') }}">管理后台</a>
{% endif %}

{% if is_granted('ROLE_EDITOR') %}
    <a href="{{ path('app_editor') }}">编辑内容</a>
{% endif %}

{% if is_granted('ROLE_USER') %}
    <p>欢迎, {{ app.user.name }}</p>
{% endif %}

三、投票器 #

3.1 创建投票器 #

php
<?php

namespace App\Security\Voter;

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
{
    public const EDIT = 'POST_EDIT';
    public const DELETE = 'POST_DELETE';
    public const VIEW = 'POST_VIEW';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::EDIT, self::DELETE, self::VIEW])
            && $subject instanceof Post;
    }

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

        if (!$user instanceof User) {
            return false;
        }

        return match ($attribute) {
            self::VIEW => $this->canView($subject, $user),
            self::EDIT => $this->canEdit($subject, $user),
            self::DELETE => $this->canDelete($subject, $user),
            default => false,
        };
    }

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

        return $post->isPublished();
    }

    private function canEdit(Post $post, User $user): bool
    {
        return $user === $post->getAuthor() || $this->security->isGranted('ROLE_ADMIN');
    }

    private function canDelete(Post $post, User $user): bool
    {
        return $this->canEdit($post, $user);
    }
}

3.2 使用投票器 #

php
<?php

namespace App\Controller;

use App\Entity\Post;
use App\Security\Voter\PostVoter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

class PostController extends AbstractController
{
    #[Route('/post/{id}/edit', name: 'app_post_edit')]
    #[IsGranted(PostVoter::EDIT, 'post')]
    public function edit(Post $post): Response
    {
        return $this->render('post/edit.html.twig', [
            'post' => $post,
        ]);
    }

    #[Route('/post/{id}/delete', name: 'app_post_delete')]
    public function delete(Post $post): Response
    {
        $this->denyAccessUnlessGranted(PostVoter::DELETE, $post);
        
        // 删除文章
    }
}

3.3 模板中使用投票器 #

twig
{% if is_granted('POST_EDIT', post) %}
    <a href="{{ path('app_post_edit', {id: post.id}) }}">编辑</a>
{% endif %}

{% if is_granted('POST_DELETE', post) %}
    <a href="{{ path('app_post_delete', {id: post.id}) }}">删除</a>
{% endif %}

四、表达式安全 #

4.1 使用表达式 #

php
<?php

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

class OrderController extends AbstractController
{
    #[Route('/order/{id}', name: 'app_order_show')]
    #[IsGranted(
        attribute: 'ROLE_USER',
        subject: 'order',
        message: '您没有权限查看此订单'
    )]
    public function show(Order $order): Response
    {
        return $this->render('order/show.html.twig', [
            'order' => $order,
        ]);
    }

    #[Route('/order/{id}/cancel', name: 'app_order_cancel')]
    #[IsGranted(
        attribute: 'order.can_cancel',
        subject: 'order'
    )]
    public function cancel(Order $order): Response
    {
        // 取消订单
    }
}

4.2 复杂表达式 #

php
<?php

use Symfony\Component\ExpressionLanguage\Expression;

#[Route('/special', name: 'app_special')]
#[IsGranted(
    new Expression('is_granted("ROLE_ADMIN") or is_granted("ROLE_EDITOR")')
)]
public function special(): Response
{
    return $this->render('special/index.html.twig');
}

#[Route('/owner/{id}', name: 'app_owner')]
#[IsGranted(
    new Expression('user === subject.getOwner()'),
    subject: 'entity'
)]
public function ownerAction(Entity $entity): Response
{
    return $this->render('owner/index.html.twig');
}

4.3 访问控制表达式 #

yaml
# config/packages/security.yaml
security:
    access_control:
        - { path: ^/admin, allow_if: "is_granted('ROLE_ADMIN') and request.getClientIp() == '127.0.0.1'" }
        - { path: ^/api/admin, allow_if: "is_granted('ROLE_API_ADMIN') and request.isXmlHttpRequest()" }

五、自定义投票器 #

5.1 完整投票器示例 #

php
<?php

namespace App\Security\Voter;

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

class OrderVoter extends Voter
{
    public const VIEW = 'ORDER_VIEW';
    public const CANCEL = 'ORDER_CANCEL';
    public const REFUND = 'ORDER_REFUND';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::VIEW, self::CANCEL, self::REFUND])
            && $subject instanceof Order;
    }

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

        if (!$user instanceof User) {
            return false;
        }

        return match ($attribute) {
            self::VIEW => $this->canView($subject, $user),
            self::CANCEL => $this->canCancel($subject, $user),
            self::REFUND => $this->canRefund($subject, $user),
            default => false,
        };
    }

    private function canView(Order $order, User $user): bool
    {
        return $order->getUser() === $user || $this->isAdmin($user);
    }

    private function canCancel(Order $order, User $user): bool
    {
        if ($this->isAdmin($user)) {
            return true;
        }

        if ($order->getUser() !== $user) {
            return false;
        }

        return $order->getStatus() === 'pending';
    }

    private function canRefund(Order $order, User $user): bool
    {
        if (!$this->isAdmin($user)) {
            return false;
        }

        return in_array($order->getStatus(), ['paid', 'completed']);
    }

    private function isAdmin(User $user): bool
    {
        return in_array('ROLE_ADMIN', $user->getRoles());
    }
}

六、访问决策管理器 #

6.1 决策策略 #

yaml
# config/packages/security.yaml
security:
    access_decision_manager:
        strategy: affirmative
        allow_if_all_abstain: false
        allow_if_equal_granted_denied: true

6.2 策略说明 #

策略 说明
affirmative 任一投票器通过即允许
consensus 多数投票器通过即允许
unanimous 所有投票器通过才允许

七、安全上下文 #

7.1 获取当前用户 #

php
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Security\Core\Security;

class ProfileController extends AbstractController
{
    public function index(Security $security): Response
    {
        $user = $security->getUser();
        
        return $this->render('profile/index.html.twig', [
            'user' => $user,
        ]);
    }
}

7.2 模板中获取用户 #

twig
{% if app.user %}
    <p>用户: {{ app.user.name }}</p>
    <p>邮箱: {{ app.user.email }}</p>
    
    {% for role in app.user.roles %}
        <span class="badge">{{ role }}</span>
    {% endfor %}
{% endif %}

八、权限缓存 #

8.1 缓存投票结果 #

php
<?php

namespace App\Security\Voter;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\CacheableVoterInterface;

class CachedVoter implements CacheableVoterInterface
{
    public function supportsAttribute(string $attribute): bool
    {
        return in_array($attribute, ['SOME_ATTRIBUTE']);
    }

    public function supportsType(string $subjectType): bool
    {
        return $subjectType === 'App\Entity\SomeEntity';
    }

    public function vote(TokenInterface $token, mixed $subject, array $attributes): int
    {
        // 投票逻辑
    }
}

九、最佳实践 #

9.1 权限设计原则 #

text
权限设计原则:
├── 最小权限原则
├── 角色层次清晰
├── 使用投票器处理复杂逻辑
├── 避免硬编码权限
├── 权限缓存优化
└── 日志记录权限变更

9.2 安全建议 #

text
安全建议:
├── 所有敏感操作都要检查权限
├── 使用表达式处理复杂权限
├── 自定义投票器封装业务逻辑
├── 定期审计权限配置
└── 记录权限检查日志

十、总结 #

本章学习了:

  • 授权机制概述
  • 角色控制和层次
  • 投票器创建和使用
  • 表达式安全
  • 自定义投票器
  • 访问决策管理器
  • 安全上下文
  • 权限缓存

下一章将学习 事件系统

最后更新:2026-03-28