PR링크:
https://github.com/l-lyun/band-platform/pull/11/changes
[FEAT] Spring Security 기본 설정 by l-lyun · Pull Request #11 · l-lyun/band-platform
#️⃣ 연관된 이슈 resolves: #4 📝 작업 내용 Spring Security 의존성 추가 및 중복 validation 의존성 정리 공개 API와 인증 필요 API 경로 분리 BCrypt 기반 PasswordEncoder Bean 추가 인증 실패 시 A01 공통 에러 응
github.com
스프링 시큐리티에 대해 간단히나마 알아보려고 한다.
너무나 방대한 내용이기 때문에 codex가 어떻게 구현했고, 그게 어떻게 적용되는지 우선적으로 알아보려한다.
스프링 시큐리티는 따로 문서 작성을 하면 좋을 것 같다.
이 글에서는 스프링 시큐리티가 무엇인지, 어떤 역할을 하는지 간단히 살펴보고
내가 이번 PR에서 어떤 작업을 했는지 살펴보겠다.
스프링 시큐리티의 역할
authentication, authorization, and protection against common attacks
스프링 시큐리티는 인증, 인가, 일반적인 공격에 대한 보호 기능을 제공하는 프레임워크이다.
스프링 시큐리티는 Servlet Filter라는 기술을 기반으로 동작하는데,
클라이언트가 요청을 보내면 서블릿 컨테이너는 요청을 처리하기 위한 여러 필터가 동작하고,
최종적으로 DispatcherServlet이 실행된다.
필터는 클라이언트 요청이 서블릿에 도달하기 전까지 여러 부가 로직을 수행할 수 있고,
요청이 서블릿에 도달하지 못하도록 차단할 수 있다.

DelegatingFilterProxy
서블릿 컨테이너는 필터를 관리하지만, 스프링 빈은 직접 알지 못하기 때문에
이를 해결하기위해 스프링은 DelegatingFilterProxy라는 다리 역할의 필터를 제공한다.
서블릿 컨테이너에 DelegatingFilterProxy를 등록해주면,
실제 처리는 필터 빈에서 처리하여 스프링 DI 등 기술을 자유롭게 사용할 수 있다.

이 과정은 서블릿 컨테이너(스프링은 톰캣)에서는 Filter 인터페이스를 구현한 객체뿐인데,
스프링 시큐리티 실제 보안 필터 체인은 스프링 빈으로 등록되어있다.
따라서 서블린 컨테이너에서 직접 스프링 빈을 관리하지 않고 있기 때문에!
DelegatingFilterProxy에서 doFilter()를 실행하면 스프링 AC에서 실제 필터 빈을 찾아 실행한다.
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Filter delegate = getFilterBean(someBeanName);
delegate.doFilter(request, response, chain);
}
만약 이 필터 처리를 디스패처 서블릿 도달 이전에 진행하지 않는다면,
백엔드 개발자가 원하는 인증처리를 하기가 어려울 것이다.
구체적인 원리는 점차... 알아보도록 하겠다.
DelegatingFilterProxy (Spring Framework 7.0.8 API)
Proxy for a standard Servlet Filter, delegating to a Spring-managed bean that implements the Filter interface. Supports a "targetBeanName" filter init-param in web.xml, specifying the name of the target bean in the Spring application context. web.xml will
docs.spring.io
FilterChainProxy
스프링 시큐리티의 핵심 엔진 필터다.
내부적으로 여러 SecurityFilterChain을 가지고 있으며, 요청에 알맞게 보안 처리를 진행한다.
이 부분은 나중에 구현이 어떻게 되어있는지 보면서 자세히 알아보자.
흔히 알고 있듯 URL에 따라 보안 정책 구분이 가능하고, 어떤 필터 체인을 실행할 지 설정이 가능하다.
Security Filters
SecurityFilterChain 내부에 실제로 들어있는, 빈으로 등록되어있는 필터들이다.
인증, 인가, CSRF 보호 등 실제 보안 기능을 수행한다.
프로젝트에서 적용
스프링 시큐리티는 3가지 역할을 담당할 것이다.
1. 인증(Authentication)
- JwtAuthentication에서 Access Token 검증
2. 인가(Authorization)
- SecurityConfig의 permitAll, authenticated 설정
3. Filter Chain
- Config를 통한 보안 설정과 Jwt 필터
우리 프로젝트에서는 세션 로그인을 사용하지 않고,
JWT를 활용해 인증할 예정이기 때문에 기본 로그인 필터를 그래도 사용하지 않고
JwtAuthenticationFilter를 통해 토큰을 검증하는 구조로 구현했다.
앞서 살펴본 SecurityFilterChain를 구현한 SecurityConfig 클래스를 먼저 살펴보자.
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http,
JsonAuthenticationEntryPoint authenticationEntryPoint,
JsonAccessDeniedHandler accessDeniedHandler
) throws Exception {
return http
// 세션 기반 인증 시 보호 기능, 세션 미사용
.csrf(AbstractHttpConfigurer::disable)
// 스프링 시큐리티 기본 폼 로그인 미사용
.formLogin(AbstractHttpConfigurer::disable)
// 요청마다 로그인 정보를 Authorization에 담아 보내는 방식 미사용
.httpBasic(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
// 스프링 시큐리티 서버 세션 미사용
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.cors(Customizer.withDefaults())
.exceptionHandling(exception -> exception
// 필터 체인 안에서 발생한 예외 처리 클래스
.authenticationEntryPoint(authenticationEntryPoint)
// 인증은 되었으나 인가가 없는 유저 접근 처리 클래스
.accessDeniedHandler(accessDeniedHandler)
)
// 접근 가능 API 엔드포인트 목록, 현재 리팩토링되었음
.authorizeHttpRequests(authorization -> authorization
.requestMatchers(HttpMethod.POST, "/api/users", "/api/auth/signup", "/api/auth/login").permitAll()
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/error").permitAll()
.anyRequest().authenticated()
)
.build();
}
}
메서드 체이닝이 잘 되어있는데 이해를 위해 블로그에서는 주석으로 설명을 추가했다.
FilterChainProxy 내부에서 SecurityFilterChain을 사용해 어떤 보안 필터를 적용할지 결정한다고 했었다.
이 코드에서 SecurityFilterChain을 빈으로 등록하면, 스프링 시큐리티는 이 설정을 기반으로 체인을 구성한다.
클라이언트 요청이 들어오면 DelegatingFilterProxy는 완성된 FilterChainProxy 빈에게 요청을 위임한다.
이후 FilterChainProxy 빈은 요청에 알맞은 SecurityFilterChain을 선택하고
체인에 포함된 필터들이 순서대로 작동하는 것이다.
// 필터 체인 안에서 발생한 예외 처리 클래스
.authenticationEntryPoint(authenticationEntryPoint)
// 인증은 되었으나 인가가 없는 유저 접근 처리 클래스
.accessDeniedHandler(accessDeniedHandler)
여기 라인들에 주목해보자. 각각 예외를 처리하는 클래스들 또한 잘 만들어줬다.
부트캠프 하면서 인증/인가 공통 에러처리를 빼먹었어서 연동 과정이 어려웠던 기억이 있는데
역시 AI가 처음부터 잘 잡아주었다. 실제 어떻게 동작하는지 알아보자.
@Component
@RequiredArgsConstructor
public class JsonAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final SecurityErrorResponseWriter errorResponseWriter;
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException
) throws IOException {
errorResponseWriter.write(response, ErrorCode.AUTH_REQUIRED);
}
}
@Component
@RequiredArgsConstructor
public class JsonAccessDeniedHandler implements AccessDeniedHandler {
private final SecurityErrorResponseWriter errorResponseWriter;
@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException
) throws IOException {
errorResponseWriter.write(response, ErrorCode.AUTH_FORBIDDEN);
}
}
@Component
@RequiredArgsConstructor
public class SecurityErrorResponseWriter {
private final ObjectMapper objectMapper;
public void write(HttpServletResponse response, ErrorCode errorCode) throws IOException {
response.setStatus(errorCode.status().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
objectMapper.writeValue(response.getWriter(), ErrorResponse.from(errorCode));
}
}
인증 실패시 JsonAuthenticationEntryPoint, 인가 실패시 AccessDeniedHandler가
작동하도록 SecurityConfig에 등록해두었고,
두 클래스에서는 SecurityErrorResponseWriter에 우리 프로젝트 공통 응답대로 에러를 내려주도록 만들었다.
JsonAuthenticationEntryPoint → 인증 실패 상황 감지 → AUTH_REQUIRED 전달
JsonAccessDeniedHandler → 권한 없음 상황 감지 → AUTH_FORBIDDEN 전달
직접 등록하지 않으면 스프링 시큐리티의 기본 처리 방식이 작동하기 때문에 예상치 못한 에러 응답이 나간다.
REST API 환경에서는 일관된 JSON 응답이 필요하기 때문에 이 2개의 클래스가 참으로 중요하다고 느낀다.
이번 작업을 통해 JWT 토큰 인증 방식을 적용하기 위한 기반이 잘 만들어졌다고 생각한다.
아직 세팅과 공통 포멧의 영역이기 때문에 AI가 정말 잘 짜준다고 생각이 들었다.
(마지막 3개 클래스 롬복 어노테이션은 이후에 추가되었는데 내가 달았다.)
스프링 시큐리티의 내용은 정말 방대하기 때문에 오늘 글을 작성하면서도 꾸준한 학습이 필요하다고 느꼈다.
다음 내용은 JWT, RTR 방식으로 글을 써보려 한다.
'spring' 카테고리의 다른 글
| [Spring] 토비의 스프링 Vol.1 2장 - 테스트 (2) (0) | 2026.06.18 |
|---|---|
| [Spring] 토비의 스프링 Vol.1 2장 - 테스트 (1) (0) | 2026.06.17 |
| [Spring] 토비의 스프링 Vol.1 1장 - 오브젝트와 의존관계 (2) (0) | 2026.06.11 |
| [Spring] 토비의 스프링 Vol.1 1장 - 오브젝트와 의존관계 (1) (0) | 2026.06.09 |
| [Spring] 토비의 스프링 강의 섹션 3 - 스프링 도입 (0) | 2026.05.04 |