Files
2025-11-29 17:59:04 +08:00

637 lines
18 KiB
Markdown

# Symfony Security Advanced Configuration
## Authentication Systems
### JWT Authentication
```php
// Install required packages
// composer require lexik/jwt-authentication-bundle
// config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'
token_ttl: 3600
// config/packages/security.yaml
security:
firewalls:
login:
pattern: ^/api/login
stateless: true
json_login:
check_path: /api/login_check
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api:
pattern: ^/api
stateless: true
jwt: ~
```
### OAuth2 Implementation
```php
// Using KnpU OAuth2 Client Bundle
// composer require knpuniversity/oauth2-client-bundle
// config/packages/knpu_oauth2_client.yaml
knpu_oauth2_client:
clients:
google:
type: google
client_id: '%env(GOOGLE_CLIENT_ID)%'
client_secret: '%env(GOOGLE_CLIENT_SECRET)%'
redirect_route: connect_google_check
redirect_params: {}
// Controller for OAuth
#[Route('/connect/google', name: 'connect_google')]
public function connectGoogle(ClientRegistry $clientRegistry): Response
{
return $clientRegistry
->getClient('google')
->redirect(['email', 'profile']);
}
#[Route('/connect/google/check', name: 'connect_google_check')]
public function connectGoogleCheck(Request $request, ClientRegistry $clientRegistry): Response
{
$client = $clientRegistry->getClient('google');
$user = $client->fetchUser();
// Handle user creation/authentication
// ...
}
```
### Two-Factor Authentication
```php
// composer require scheb/2fa-bundle scheb/2fa-totp
// Entity with 2FA
#[ORM\Entity]
class User implements UserInterface, TwoFactorInterface
{
#[ORM\Column(nullable: true)]
private ?string $totpSecret = null;
public function isTotpAuthenticationEnabled(): bool
{
return $this->totpSecret !== null;
}
public function getTotpAuthenticationUsername(): string
{
return $this->email;
}
public function getTotpAuthenticationConfiguration(): ?TotpConfigurationInterface
{
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
}
}
// config/packages/security.yaml
security:
firewalls:
main:
two_factor:
auth_form_path: 2fa_login
check_path: 2fa_login_check
```
## Custom Authenticators
### API Key Authenticator
```php
namespace App\Security;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class ApiKeyAuthenticator extends AbstractAuthenticator
{
public function __construct(
private UserRepository $userRepository
) {}
public function supports(Request $request): ?bool
{
return $request->headers->has('X-API-KEY');
}
public function authenticate(Request $request): Passport
{
$apiKey = $request->headers->get('X-API-KEY');
if (null === $apiKey) {
throw new CustomUserMessageAuthenticationException('No API key provided');
}
return new SelfValidatingPassport(
new UserBadge($apiKey, function($apiKey) {
$user = $this->userRepository->findOneBy(['apiKey' => $apiKey]);
if (!$user) {
throw new CustomUserMessageAuthenticationException('Invalid API Key');
}
return $user;
})
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return new JsonResponse([
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
], Response::HTTP_UNAUTHORIZED);
}
}
```
## Advanced Voters
### Hierarchical Voters
```php
namespace App\Security\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class DocumentVoter extends Voter
{
public const VIEW = 'DOCUMENT_VIEW';
public const EDIT = 'DOCUMENT_EDIT';
public const DELETE = 'DOCUMENT_DELETE';
public const SHARE = 'DOCUMENT_SHARE';
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::VIEW, self::EDIT, self::DELETE, self::SHARE])
&& $subject instanceof Document;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
/** @var Document $document */
$document = $subject;
// Check hierarchical permissions
if ($this->hasHierarchicalAccess($user, $document, $attribute)) {
return true;
}
return match($attribute) {
self::VIEW => $this->canView($document, $user),
self::EDIT => $this->canEdit($document, $user),
self::DELETE => $this->canDelete($document, $user),
self::SHARE => $this->canShare($document, $user),
default => false,
};
}
private function hasHierarchicalAccess(User $user, Document $document, string $attribute): bool
{
// Check department hierarchy
if ($user->isDepartmentHead() && $document->getDepartment() === $user->getDepartment()) {
return true;
}
// Check organization hierarchy
if ($user->isOrganizationAdmin()) {
return true;
}
return false;
}
private function canView(Document $document, User $user): bool
{
// Public documents
if ($document->isPublic()) {
return true;
}
// Owner can view
if ($document->getOwner() === $user) {
return true;
}
// Shared with user
if ($document->getSharedUsers()->contains($user)) {
return true;
}
// Team member can view team documents
if ($document->getTeam() && $document->getTeam()->hasMember($user)) {
return true;
}
return false;
}
private function canEdit(Document $document, User $user): bool
{
// Owner can edit
if ($document->getOwner() === $user) {
return true;
}
// Check edit permissions
return $document->hasEditPermission($user);
}
private function canDelete(Document $document, User $user): bool
{
// Only owner and admins can delete
return $document->getOwner() === $user || $user->hasRole('ROLE_ADMIN');
}
private function canShare(Document $document, User $user): bool
{
// Owner and users with share permission
return $document->getOwner() === $user || $document->hasSharePermission($user);
}
}
```
## Role Hierarchy & Dynamic Roles
### Dynamic Role Provider
```php
namespace App\Security;
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
class DynamicRoleHierarchy implements RoleHierarchyInterface
{
public function __construct(
private RoleRepository $roleRepository
) {}
public function getReachableRoleNames(array $roles): array
{
$reachableRoles = $roles;
foreach ($roles as $role) {
$roleEntity = $this->roleRepository->findOneBy(['name' => $role]);
if ($roleEntity) {
// Add inherited roles
foreach ($roleEntity->getInheritedRoles() as $inheritedRole) {
$reachableRoles[] = $inheritedRole->getName();
}
// Add permission-based roles
foreach ($roleEntity->getPermissions() as $permission) {
$reachableRoles[] = 'ROLE_' . strtoupper($permission->getName());
}
}
}
return array_unique($reachableRoles);
}
}
```
## Security Event Listeners
### Login Success Handler
```php
namespace App\Security;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class LoginSuccessHandler implements AuthenticationSuccessHandlerInterface
{
public function __construct(
private EntityManagerInterface $em,
private LoggerInterface $logger,
private IpGeolocationService $geolocation
) {}
public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response
{
$user = $token->getUser();
// Log successful login
$loginLog = new LoginLog();
$loginLog->setUser($user);
$loginLog->setIpAddress($request->getClientIp());
$loginLog->setUserAgent($request->headers->get('User-Agent'));
$loginLog->setTimestamp(new \DateTimeImmutable());
// Get geolocation
$location = $this->geolocation->locate($request->getClientIp());
$loginLog->setLocation($location);
// Check for suspicious activity
if ($this->isSuspiciousLogin($user, $location)) {
$this->notifyUserOfSuspiciousActivity($user, $loginLog);
}
// Update last login
$user->setLastLoginAt(new \DateTimeImmutable());
$user->setLastLoginIp($request->getClientIp());
$this->em->persist($loginLog);
$this->em->flush();
// Log event
$this->logger->info('User logged in', [
'user' => $user->getUserIdentifier(),
'ip' => $request->getClientIp()
]);
return new RedirectResponse('/dashboard');
}
private function isSuspiciousLogin(User $user, ?Location $location): bool
{
// Check if login from new country
$lastLogins = $this->em->getRepository(LoginLog::class)
->findLastLogins($user, 10);
foreach ($lastLogins as $login) {
if ($login->getLocation() && $login->getLocation()->getCountry() === $location->getCountry()) {
return false;
}
}
return true;
}
}
```
## Access Control Lists (ACL)
### Custom ACL Implementation
```php
namespace App\Security\Acl;
class AclManager
{
public function __construct(
private EntityManagerInterface $em
) {}
public function grantAccess(
object $domainObject,
UserInterface $user,
array $permissions
): void {
$acl = new Acl();
$acl->setObjectClass(get_class($domainObject));
$acl->setObjectId($domainObject->getId());
$acl->setUser($user);
$acl->setPermissions($permissions);
$this->em->persist($acl);
$this->em->flush();
}
public function revokeAccess(
object $domainObject,
UserInterface $user
): void {
$acl = $this->em->getRepository(Acl::class)->findOneBy([
'objectClass' => get_class($domainObject),
'objectId' => $domainObject->getId(),
'user' => $user
]);
if ($acl) {
$this->em->remove($acl);
$this->em->flush();
}
}
public function isGranted(
string $permission,
object $domainObject,
UserInterface $user
): bool {
$acl = $this->em->getRepository(Acl::class)->findOneBy([
'objectClass' => get_class($domainObject),
'objectId' => $domainObject->getId(),
'user' => $user
]);
return $acl && in_array($permission, $acl->getPermissions());
}
}
```
## Security Headers & CORS
### Security Headers Subscriber
```php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class SecurityHeadersSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::RESPONSE => 'onKernelResponse',
];
}
public function onKernelResponse(ResponseEvent $event): void
{
$response = $event->getResponse();
// Content Security Policy
$response->headers->set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
);
// XSS Protection
$response->headers->set('X-XSS-Protection', '1; mode=block');
// Prevent MIME sniffing
$response->headers->set('X-Content-Type-Options', 'nosniff');
// Clickjacking protection
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
// HTTPS enforcement
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
// Referrer Policy
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
}
}
```
### CORS Configuration
```yaml
# config/packages/nelmio_cors.yaml
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
allow_headers: ['Content-Type', 'Authorization', 'X-API-KEY']
expose_headers: ['Link', 'X-Total-Count']
max_age: 3600
paths:
'^/api/':
allow_origin: ['*']
allow_headers: ['*']
allow_methods: ['POST', 'PUT', 'GET', 'DELETE', 'OPTIONS']
max_age: 3600
```
## Rate Limiting
```php
namespace App\Security;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
class RateLimitingService
{
public function __construct(
private RateLimiterFactory $apiLimiter,
private RateLimiterFactory $loginLimiter
) {}
public function checkApiLimit(string $apiKey): void
{
$limiter = $this->apiLimiter->create($apiKey);
if (!$limiter->consume(1)->isAccepted()) {
throw new TooManyRequestsHttpException(
$limiter->getRetryAfter()->getTimestamp() - time(),
'API rate limit exceeded'
);
}
}
public function checkLoginLimit(string $username, string $ip): void
{
$limiter = $this->loginLimiter->create($username . '_' . $ip);
if (!$limiter->consume(1)->isAccepted()) {
throw new TooManyRequestsHttpException(
$limiter->getRetryAfter()->getTimestamp() - time(),
'Too many login attempts'
);
}
}
}
// config/packages/rate_limiter.yaml
framework:
rate_limiter:
api:
policy: 'sliding_window'
limit: 100
interval: '60 minutes'
login:
policy: 'fixed_window'
limit: 5
interval: '15 minutes'
```
## Encryption & Hashing
### Field-level Encryption
```php
namespace App\Security;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
class EncryptionService
{
private string $key;
public function __construct(string $encryptionKey)
{
$this->key = $encryptionKey;
}
public function encrypt(string $data): string
{
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
$encrypted = openssl_encrypt($data, 'aes-256-cbc', $this->key, 0, $iv);
return base64_encode($encrypted . '::' . $iv);
}
public function decrypt(string $data): string
{
list($encrypted_data, $iv) = explode('::', base64_decode($data), 2);
return openssl_decrypt($encrypted_data, 'aes-256-cbc', $this->key, 0, $iv);
}
}
// Doctrine Type for encrypted fields
class EncryptedStringType extends Type
{
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
return $this->encryptionService->encrypt($value);
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return $this->encryptionService->decrypt($value);
}
}
```
## Security Best Practices
1. **Always use HTTPS in production**
2. **Implement CSRF protection for forms**
3. **Use parameterized queries to prevent SQL injection**
4. **Validate and sanitize all user input**
5. **Implement proper session management**
6. **Use strong password policies**
7. **Implement account lockout mechanisms**
8. **Log security events**
9. **Regular security audits**
10. **Keep dependencies updated**