定制 fedale/access-control-bundle 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

fedale/access-control-bundle

Composer 安装命令:

composer require fedale/access-control-bundle

包简介

Symfony bundle for dynamic access control and authorization management

README 文档

README

PHP Symfony

Rule-based HTTP access control for Symfony. The bundle inspects every incoming request and allows or denies it according to a set of rules matched on path, host, HTTP method, client IP and user roles. Rules can come from any source — Doctrine (built-in), a YAML/array, a remote API, MongoDB… — because the lookup sits behind a single interface. An optional PSR-6 cache layer keeps rule loading cheap.

Features

  • Allow/deny rules matched on path (regex), host (regex), HTTP methods, client IP / CIDR and roles.
  • First-match-wins evaluation, ordered by an explicit sort field.
  • Source-agnostic: rules are read through RuleProviderInterface. Doctrine is the default; any service can replace it.
  • Super-admin bypass and configurable anonymous-access gating.
  • Default policy (allow/deny) applied when no rule matches.
  • Optional PSR-6 caching of the rule set, configurable from YAML.
  • Built on Symfony's native ChainRequestMatcher — no deprecated APIs.

Requirements

  • PHP 8.2+
  • Symfony 6.4 or 7.x (http-kernel, http-foundation, security-bundle, security-core)
  • psr/cache ^3.0
  • Doctrine ORM is only required when you use the default doctrine rule provider (doctrine/orm, doctrine/doctrine-bundle). It is a require-dev/suggest dependency, not a hard one.

Installation

composer require fedale/access-control-bundle

Not on Packagist yet. Until the package is published, point Composer at the repository directly in your application's composer.json:

{
    "repositories": [
        { "type": "vcs", "url": "https://github.com/Fedale/FedaleAccessControlBundle" }
    ]
}

then run composer require fedale/access-control-bundle:dev-main.

If you use Symfony Flex, the bundle is registered automatically. Otherwise, add it to config/bundles.php:

// config/bundles.php
return [
    // ...
    Fedale\AccessControlBundle\FedaleAccessControlBundle::class => ['all' => true],
];

Quick start

  1. Create the configuration file:
# config/packages/fedale_access_control.yaml
fedale_access_control:
    enabled: true
    super_admin_role: ROLE_SUPER_ADMIN
    default_policy: deny      # block anything no rule matched
    provider: doctrine        # read rules from the database
  1. Create the database table and add a rule (see Doctrine provider). For example, a rule that blocks /admin for everyone except ROLE_ADMIN:
name path roles allow sort
admin-area ^/admin ["ROLE_ADMIN"] 1 0
  1. That's it. A request to /admin from an anonymous user now receives:
HTTP/1.1 403 Forbidden

Access denied

while a user with ROLE_ADMIN (or ROLE_SUPER_ADMIN) passes through.

Configuration reference

All options with their defaults:

# config/packages/fedale_access_control.yaml
fedale_access_control:

    # Master switch. When false the listener allows every request.
    enabled: true

    # When false, requests from unauthenticated users that match no rule are denied,
    # regardless of `default_policy`. See "Super admin & anonymous access".
    anonymous_access: false

    # Role that bypasses all rules. Set to an empty string to disable the bypass.
    super_admin_role: ROLE_SUPER_ADMIN

    # Policy applied when no rule matches the request: allow | deny
    default_policy: deny

    cache:
        # Enable/disable the PSR-6 cache decorator around the rule provider.
        enabled: true
        # Id of the PSR-6 cache pool used to store the rules.
        pool: cache.app
        # Time-to-live in seconds; null = no expiration (invalidate the pool manually).
        ttl: null

    # 'doctrine' (built-in) or the service id of a custom RuleProviderInterface
    # implementation (e.g. a YAML, API or Mongo source).
    provider: doctrine
Option Type Default Description
enabled bool true Turn the whole access control on/off.
anonymous_access bool false Allow unauthenticated users through on an unmatched request.
super_admin_role string ROLE_SUPER_ADMIN Role that bypasses every rule (empty string disables it).
default_policy allow|deny deny Decision when no rule matches.
cache.enabled bool true Wrap the provider with a caching decorator.
cache.pool string cache.app PSR-6 pool service id.
cache.ttl int|null null Cache lifetime in seconds; null means no expiration.
provider string doctrine doctrine or a custom provider service id.

How it works

The bundle registers a listener on the kernel.request event (priority -10). For every main request it calls AccessDecisionManager::decide(), which evaluates:

  1. Bundle disabled? (enabled: false) → allow.
  2. Super admin? (current user is granted super_admin_role) → allow.
  3. Find the first matching rule (rules are evaluated in sort order, first match wins):
    • rule found and allow = falsedeny;
    • rule found, allow = true, no roles required → allow;
    • rule found, allow = true, roles required → allow if the user is granted any of them, otherwise deny.
  4. No rule matched (fallback):
    • user is not authenticated and anonymous_access = falsedeny;
    • otherwise apply default_policy (allow → allow, deny → deny).

When the decision is deny, the listener dispatches an AccessDeniedEvent and then short-circuits the request with a 403 Forbidden response. If no listener provides its own response, the body is content-negotiated: clients that send Accept: application/json get {"error":"Access denied"}, everyone else gets the plain text Access denied.

The bundle stays outside Symfony's firewall: a denied request always yields a 403, it is never redirected to the login page. Authentication-driven redirects remain the firewall's job — this layer only answers "may this request proceed?".

Reacting to a denial (AccessDeniedEvent)

To log denials, emit metrics, or return a custom response (a branded error page, a redirect, a different JSON shape), listen for AccessDeniedEvent. Setting a response on the event overrides the default one:

use Fedale\AccessControlBundle\Event\AccessDeniedEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\Response;

#[AsEventListener]
final class AccessDeniedSubscriber
{
    public function __invoke(AccessDeniedEvent $event): void
    {
        // $event->getRequest() is available for inspection/logging.
        $event->setResponse(new Response('Custom forbidden page', Response::HTTP_FORBIDDEN));
    }
}

Defining rules

A rule is described by the AccessRule DTO. Whatever the source, it produces objects with these fields:

Field Type Meaning
id int Identifier (from the source).
name string Human-readable label.
reason ?string Optional explanation (e.g. why access is blocked); null when unset.
path string Regular expression matched against the request path (e.g. ^/admin).
host ?string Optional regular expression matched against the host.
roles string[] Required roles; empty means "no role required".
methods string[] Allowed HTTP methods (e.g. ["GET", "POST"]); empty means any.
ips string[] Allowed IPs or CIDR ranges (e.g. ["10.0.0.0/8"]); empty means any.
allow bool true = allow (subject to roles), false = deny.
sort int Evaluation order (ascending). Lower runs first.
active bool Inactive rules are ignored.

Matching is delegated to Symfony's ChainRequestMatcher: path and host are treated as regular expressions, while methods and ips only constrain the request when non-empty. Invalid path/host regular expressions are rejected as soon as the rule is built, with an InvalidArgumentException naming the offending rule.

Client IP behind a proxy. The ips field matches against Request::getClientIp(). If your app sits behind a reverse proxy or load balancer, configure Symfony's trusted proxies (framework.trusted_proxies / trusted_headers) — otherwise getClientIp() returns the proxy's address and your IP rules will match the wrong client.

Rule sources (providers)

The decision logic never talks to a database directly — it depends only on RuleProviderInterface:

namespace Fedale\AccessControlBundle\Contract;

use Fedale\AccessControlBundle\Dto\AccessRule;

interface RuleProviderInterface
{
    /** @return iterable<AccessRule> */
    public function getRules(): iterable;
}

The provider option selects which implementation is wired.

Doctrine (default provider)

With provider: doctrine the rules are read from the access_control table, mapped by the AccessControlEntity. Only active rules are returned, ordered by sort.

The bundle registers its own Doctrine ORM mapping automatically (via prependExtension) when provider: doctrine and DoctrineBundle is installed — you do not need to add a doctrine.orm.mappings entry pointing into vendor/ in your application. The mapping is skipped automatically if you switch to a custom provider.

Create the schema with a migration (recommended):

php bin/console make:migration
php bin/console doctrine:migrations:migrate

or, in development:

php bin/console doctrine:schema:update --force

Add rules like any other entity:

use Fedale\AccessControlBundle\Bridge\Doctrine\Entity\AccessControlEntity;

$rule = (new AccessControlEntity())
    ->setName('admin-area')
    ->setReason('Admins only')
    ->setPath('^/admin')
    ->setRoles(['ROLE_ADMIN'])
    ->setMethods([])          // any method
    ->setIps([])              // any IP
    ->setAllow(true)
    ->setSort(0)
    ->setActive(true);

$entityManager->persist($rule);
$entityManager->flush();

The setters normalise input for you (trim strings, de-duplicate roles/IPs, upper-case methods).

Custom provider (YAML, API, Mongo, …)

To read rules from anywhere else, implement RuleProviderInterface and point provider at your service id. Here is a provider that returns rules defined in plain PHP:

// src/Security/StaticRuleProvider.php
namespace App\Security;

use Fedale\AccessControlBundle\Contract\RuleProviderInterface;
use Fedale\AccessControlBundle\Dto\AccessRule;

final class StaticRuleProvider implements RuleProviderInterface
{
    public function getRules(): iterable
    {
        yield new AccessRule(
            id: 1,
            name: 'admin-area',
            reason: 'Admins only',
            path: '^/admin',
            host: null,
            roles: ['ROLE_ADMIN'],
            methods: [],
            ips: [],
            allow: true,
            sort: 0,
            active: true,
        );

        yield new AccessRule(
            id: 2,
            name: 'block-internal',
            reason: 'Internal API is private',
            path: '^/internal',
            host: null,
            roles: [],
            methods: [],
            ips: [],
            allow: false,
            sort: 10,
            active: true,
        );
    }
}

If autowiring/autoconfiguration is on (the Symfony default), the service id equals the class name. Select it:

# config/packages/fedale_access_control.yaml
fedale_access_control:
    provider: 'App\Security\StaticRuleProvider'

With a custom provider you do not need Doctrine at all.

The selected provider must resolve to a real service; otherwise the RuleProviderInterface alias is missing and the container fails to boot.

Caching

When cache.enabled is true, the selected provider is transparently wrapped in a CachedRuleProvider that stores the materialised rule list in a PSR-6 pool. This works for any source (Doctrine, custom, …) because it decorates the interface, not the implementation.

fedale_access_control:
    cache:
        enabled: true
        pool: cache.app   # any PSR-6 pool service
        ttl: 3600         # refresh hourly; null = never expires

Invalidation. With ttl: null the cache never expires, so after changing your rules you must clear it. Either clear the configured pool, or delete the single cache key used by the bundle:

use Fedale\AccessControlBundle\Cache\CachedRuleProvider;

$pool->deleteItem(CachedRuleProvider::CACHE_KEY);

To turn caching off entirely, set cache.enabled: false.

Super admin & anonymous access

  • Super admin — any user granted super_admin_role (default ROLE_SUPER_ADMIN) bypasses every rule. Set super_admin_role: '' to remove the bypass.
  • Anonymous access — only affects requests that match no rule. With anonymous_access: false, unauthenticated users are denied on an unmatched request even if default_policy: allow. Authenticated users instead fall through to default_policy.

Example — site that is open by default, but never to anonymous users on unknown routes:

fedale_access_control:
    anonymous_access: false
    default_policy: allow

Explicit allow rules (e.g. for ^/login) are always evaluated first, so public routes keep working.

Architecture / extension points

Everything is wired through small contracts under src/Contract/, so you can override any piece:

Interface Responsibility
RuleProviderInterface Where rules come from (Doctrine, YAML, API, …).
RuleRequestMatcherInterface Whether a single rule matches a request.
AccessMatcherInterface Pick the first matching rule for a request.
AccessDecisionManagerInterface Turn a request into an allow/deny decision.

On every denial the listener dispatches AccessDeniedEvent, so you can hook in logging, metrics or a custom response without replacing any of the above (see Reacting to a denial).

Default implementations live in src/Matcher/, src/Security/ and src/Cache/. To replace one, register your own service aliased to the relevant interface.

Console

List the effective rules as the bundle sees them (read-only, works with any provider):

php bin/console fedale:access-control:list
 sort  id  name        path       host  methods  ips         roles       policy  active
 0     1   admin-area  ^/admin    *     *        *           ROLE_ADMIN  allow   yes
 10    2   block-int   ^/internal *     *        10.0.0.0/8  -           deny    yes

With the Doctrine provider only active rules are listed; with caching enabled you see the materialised (cached) list. Enabling/disabling rules is plain entity manipulation — see Doctrine provider.

A Symfony Profiler data collector (showing which rule matched and the resulting decision) is a natural next extension but is not bundled yet, to keep the dependency surface small.

Testing

composer install
vendor/bin/phpunit

The suite covers unit (matchers, decision manager, cache), DI wiring, a functional test booting a real HttpKernel, and ORM mapping validation.

License & contributing

Released under the MIT License. Issues and pull requests are welcome.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 1
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-15

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固