# 2026년 Symfony REST API 보안 완벽 가이드: OAuth2, 속도 제한, 면접 대비 > Symfony 7.3에서 구현하는 REST API 보안 실무 가이드. OAuth2 토큰 인트로스펙션, RateLimiter 컴포넌트, Voter 인가, 기술 면접 대비를 다룹니다. - Published: 2026-09-14 - Updated: 2026-09-14 - Author: Anthony Fillion-Maillet - Tags: symfony, security, oauth2, api, rate-limiting - Reading time: 12 min --- Symfony REST API 보안은 인증, 인가, 트래픽 제어를 결합한 다층 방어가 필수입니다. Symfony 7.3에서는 RFC 7662 기반의 OAuth2 토큰 인트로스펙션이 네이티브로 지원되며, RateLimiter 컴포넌트를 통한 악용 방지 기능이 강화되었습니다. > **Symfony API의 3계층 보안** > > 프로덕션 환경의 Symfony API에는 인증(누가 호출하는가), 인가(무엇에 접근할 수 있는가), 속도 제한(얼마나 자주 접근할 수 있는가)의 세 가지 방어 계층이 필요합니다. 하나라도 누락되면 크리덴셜 스터핑, 데이터 유출, 서비스 거부 공격에 노출됩니다. ## Symfony 7.3의 OAuth2 토큰 인트로스펙션 Symfony 7.3에서는 [OAuth2 토큰 인트로스펙션(RFC 7662)](https://datatracker.ietf.org/doc/html/rfc7662)이 빌트인으로 지원됩니다. 이 기능을 사용하면 인가 서버에 직접 쿼리하여 액세스 토큰을 검증할 수 있으며, 토큰을 로컬에서 디코딩할 필요가 없습니다. 토큰 형식이 불투명하거나 서드파티 ID 제공자가 관리하는 경우에 특히 유용합니다. `AccessTokenHandler` 설정에서 인트로스펙션 엔드포인트를 지정합니다: ```yaml # config/packages/security.yaml security: firewalls: api: pattern: ^/api stateless: true access_token: token_handler: introspection: client_id: '%env(OAUTH_CLIENT_ID)%' client_secret: '%env(OAUTH_CLIENT_SECRET)%' introspection_url: '%env(OAUTH_INTROSPECTION_URL)%' ``` 인트로스펙션 응답에는 `active`, `scope`, `client_id`, 선택적으로 `username`이 포함됩니다. Symfony는 이를 보안 속성에 자동으로 매핑합니다. ```php // src/Controller/Api/ProfileController.php namespace App\Controller\Api; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; #[Route('/api/profile', methods: ['GET'])] #[IsGranted('ROLE_USER')] class ProfileController extends AbstractController { public function __invoke(): JsonResponse { // 토큰은 인트로스펙션을 통해 이미 검증됨 $user = $this->getUser(); return $this->json([ 'id' => $user->getId(), 'email' => $user->getEmail(), 'scopes' => $user->getRoles(), ]); } } ``` 이 방식을 사용하면 JWT 서명 검증의 부담을 애플리케이션에서 제거할 수 있습니다. 키 로테이션, 토큰 폐기, 스코프 검증은 모두 인가 서버가 처리합니다. ## RateLimiter 컴포넌트를 활용한 속도 제한 구현 [RateLimiter 컴포넌트](https://symfony.com/doc/current/rate_limiter.html)는 토큰 버킷 또는 슬라이딩 윈도우 알고리즘을 사용하여 엔드포인트를 악용으로부터 보호합니다. 설정은 `framework.yaml`에서 진행합니다: ```yaml # config/packages/framework.yaml framework: rate_limiter: # 일반 API 속도 제한: 분당 100개 요청 api_limiter: policy: sliding_window limit: 100 interval: '1 minute' # 인증 엔드포인트용 엄격한 제한 login_limiter: policy: token_bucket limit: 5 rate: { interval: '1 minute', amount: 5 } ``` 이벤트 구독자를 사용하여 컨트롤러에 속도 제한을 적용합니다: ```php // src/EventSubscriber/RateLimitSubscriber.php namespace App\EventSubscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\RateLimiter\RateLimiterFactory; final class RateLimitSubscriber implements EventSubscriberInterface { public function __construct( private readonly RateLimiterFactory $apiLimiter, ) {} public static function getSubscribedEvents(): array { return [KernelEvents::REQUEST => ['onRequest', 10]]; } public function onRequest(RequestEvent $event): void { if (!$event->isMainRequest()) { return; } $request = $event->getRequest(); // API 라우트에만 적용 if (!str_starts_with($request->getPathInfo(), '/api')) { return; } // 클라이언트 IP 또는 인증된 사용자를 리미터 키로 사용 $key = $request->getClientIp(); $limiter = $this->apiLimiter->create($key); if (!$limiter->consume()->isAccepted()) { throw new TooManyRequestsHttpException(); } } } ``` 인증된 API의 경우 IP 기반 키를 사용자 식별자로 대체하여 동일 네트워크의 정상 트래픽이 악의적인 사용자의 영향을 받지 않도록 합니다. ## 외부 의존성 없는 JWT 검증 인가 서버가 알려진 공개 키로 JWT를 발급하는 경우, Symfony는 `OidcUserInfoTokenHandler`를 사용하여 토큰을 로컬에서 검증할 수 있습니다: ```yaml # config/packages/security.yaml security: firewalls: api: pattern: ^/api stateless: true access_token: token_handler: oidc_user_info: base_uri: '%env(OIDC_ISSUER)%' claim: email ``` OIDC 제공자 없이 자체 발급 JWT를 사용하는 경우 `lexik/jwt-authentication-bundle`을 사용합니다: ```php // src/Security/JwtTokenAuthenticator.php namespace App\Security; use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; final class JwtTokenAuthenticator implements AccessTokenHandlerInterface { public function __construct( private readonly JWTTokenManagerInterface $jwtManager, ) {} public function getUserBadgeFrom(string $accessToken): UserBadge { // 서명을 자동으로 디코딩하고 검증 $payload = $this->jwtManager->parse($accessToken); return new UserBadge($payload['sub']); } } ``` JWT 번들은 RS256/ES256 서명 검증, 만료 확인, 발급자 검증을 처리합니다. 공개 키는 `config/jwt/public.pem`에 저장하고 `lexik_jwt_authentication.yaml`에서 참조합니다. ## Voter를 통한 세분화된 API 엔드포인트 인가 Symfony Voter는 단순한 역할 체크를 넘어서는 세분화된 인가를 제공합니다. 일반적인 패턴으로 리소스 소유권을 확인하는 구현이 있습니다: ```php // src/Security/Voter/ArticleVoter.php namespace App\Security\Voter; use App\Entity\Article; use App\Entity\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; final class ArticleVoter extends Voter { public const EDIT = 'ARTICLE_EDIT'; public const DELETE = 'ARTICLE_DELETE'; protected function supports(string $attribute, mixed $subject): bool { return in_array($attribute, [self::EDIT, self::DELETE], true) && $subject instanceof Article; } protected function voteOnAttribute( string $attribute, mixed $subject, TokenInterface $token ): bool { $user = $token->getUser(); if (!$user instanceof User) { return false; } /** @var Article $article */ $article = $subject; // 관리자는 모든 작업 가능 if (in_array('ROLE_ADMIN', $user->getRoles(), true)) { return true; } // 작성자는 자신의 글 편집/삭제 가능 return $article->getAuthor() === $user; } } ``` 컨트롤러에서 Voter를 사용합니다: ```php #[Route('/api/articles/{id}', methods: ['PUT'])] public function update(Article $article, Request $request): JsonResponse { $this->denyAccessUnlessGranted(ArticleVoter::EDIT, $article); // 업데이트 로직 } ``` Voter는 인가 로직을 중앙화하고 테스트 가능하게 만듭니다. 보안 컴포넌트에 관한 [Symfony 면접 질문](/technologies/symfony/interview-questions/events-subscribers)에서도 자주 출제되는 주제입니다. ## 일반적인 API 취약점과 대응 방안 [OWASP API Security Top 10](https://owasp.org/API-Security/editions/2023/en/0x11-t10/)은 API에서 반복적으로 발생하는 취약점을 식별합니다. Symfony는 이 중 다수에 대해 빌트인 보호를 제공합니다: | 취약점 | Symfony 대응 방안 | |--------|------------------| | 객체 수준 인가 결함 | 소유권 체크가 포함된 Voter | | 인증 결함 | 로그인 스로틀링, 안전한 비밀번호 해싱 | | 과도한 데이터 노출 | DTO 직렬화 그룹 | | 속도 제한 부재 | RateLimiter 컴포넌트 | | 대량 할당 | 폼 검증, DTO 매핑 | | 보안 설정 오류 | Symfony 보안 체커, 환경 변수 | 대량 할당 방지를 위해 요청 데이터에서 엔티티를 직접 하이드레이션하지 않습니다: ```php // src/Dto/CreateArticleDto.php namespace App\Dto; use Symfony\Component\Validator\Constraints as Assert; final class CreateArticleDto { public function __construct( #[Assert\NotBlank] #[Assert\Length(max: 255)] public readonly string $title, #[Assert\NotBlank] public readonly string $content, // 의도적으로 생략: author, createdAt, status // 이들은 애플리케이션이 설정하며, 클라이언트가 설정하는 것이 아님 ) {} } ``` DTO 화이트리스트 접근 방식을 통해 `isAdmin`이나 `createdAt`과 같이 클라이언트가 제어해서는 안 되는 필드 설정을 방지할 수 있습니다. ## API 소비자를 위한 CORS 설정 Cross-Origin Resource Sharing 헤더는 브라우저에서 API를 호출할 수 있는 도메인을 제어합니다. `nelmio/cors-bundle`은 선언적 설정을 제공합니다: ```yaml # config/packages/nelmio_cors.yaml nelmio_cors: defaults: allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] allow_methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] allow_headers: ['Content-Type', 'Authorization'] max_age: 3600 paths: '^/api/': origin_regex: true allow_origin: ['^https://.*\.example\.com$'] allow_credentials: true ``` `allow_origin: ['*']`와 `allow_credentials: true`의 조합은 피해야 합니다. 이 조합은 악성 사이트를 통한 크리덴셜 탈취에 API를 노출시킵니다. 명시적인 도메인 화이트리스트 또는 정규식 패턴을 사용하십시오. ## Symfony API 보안 기술 면접 질문 다음 질문들은 시니어 Symfony 개발자 면접에서 자주 출제됩니다. 답변은 Symfony 7.3 기능을 반영합니다. **Q: Symfony는 OAuth2 액세스 토큰 검증을 어떻게 처리합니까?** Symfony 7.3은 세 가지 접근 방식을 지원합니다: 서명 검증을 포함한 로컬 JWT 검증, OIDC 사용자 정보 엔드포인트 호출, [RFC 7662 토큰 인트로스펙션](https://symfony.com/blog/new-in-symfony-7-3-security-improvements)입니다. 인트로스펙션은 불투명 토큰이나 애플리케이션이 인가 서버를 제어하지 않는 시나리오에 적합합니다. `AccessTokenHandler`는 이 모든 것을 통합된 인터페이스 뒤에 추상화합니다. **Q: Symfony에서 방화벽과 접근 제어의 차이점은 무엇입니까?** 방화벽은 URL 패턴별로 인증 메커니즘을 정의합니다. 접근 제어 규칙은 인증이 성공한 후의 인가 요구 사항을 정의합니다. 방화벽은 유효한 JWT를 요구하고, 접근 제어는 `/api/admin/*`에 대해 `ROLE_ADMIN`을 요구하는 식입니다. 이들은 순차적으로 작동합니다: 방화벽이 인증하고, 그 다음 접근 제어가 인가합니다. **Q: 로그인 엔드포인트에 대한 무차별 대입 공격을 어떻게 방지합니까?** Symfony의 빌트인 로그인 스로틀링은 사용자명과 IP별로 실패한 시도를 제한합니다. 방화벽에서 `login_throttling`을 설정합니다: ```yaml security: firewalls: main: login_throttling: max_attempts: 5 interval: '15 minutes' ``` API 인증의 경우 토큰 엔드포인트에서 RateLimiter 컴포넌트와 결합합니다. 다중 인스턴스 배포에서는 시도 횟수를 Redis에 저장합니다. **Q: Symfony Voter와 단순 역할 체크를 언제 사용해야 하는지 설명해 주십시오.** Voter는 사용자 역할뿐만 아니라 리소스에 따라 달라지는 복잡한 인가 로직을 구현합니다. Voter를 사용하는 경우: 사용자가 리소스를 소유해야 하는 경우, 리소스에 상태 머신이 있는 경우(초안 vs 게시됨), 인가가 비즈니스 규칙에 의존하는 경우(구독 티어)입니다. 정적 권한("관리자만 설정에 접근 가능" 등)에는 역할 체크로 충분합니다. 추가적인 Symfony 보안 질문은 [Symfony 면접 준비 가이드](/blog/symfony/symfony-interview-questions)를 참조하십시오. ## 안전한 Symfony API 구축: 핵심 요점 - 토큰 형식이 불투명한 서드파티 ID 제공자에는 OAuth2 토큰 인트로스펙션을 설정합니다 - 심층 방어를 위해 인프라 수준(리버스 프록시)과 애플리케이션 수준(RateLimiter 컴포넌트) 모두에서 속도 제한을 적용합니다 - 리소스 수준 인가에는 Voter를 사용하고, 정적 권한에는 역할 체크를 예약합니다 - 대량 할당을 방지하기 위해 명시적 프로퍼티가 있는 DTO에 요청 데이터를 매핑합니다 - 시크릿은 환경 변수에 저장하고, 버전 관리에 커밋되는 `config/*.yaml` 파일에는 절대 저장하지 않습니다 - `WebTestCase` 기능 테스트로 허용된 접근과 거부된 접근 모두를 검증하여 보안 규칙을 테스트합니다 - CI 파이프라인에서 `symfony security:check`로 의존성을 감사합니다 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/symfony/symfony-rest-api-security-oauth2-rate-limiting-2026