Server#BE

Spring Security - Security 구조, 폼 로그인

스프링 시큐리티의 인증/인가 구조, 주요 객체 역할, Filter Chain 동작 방식, 폼 로그인 구현, 인증 실패 응답 통일까지

2026년 5월 28일2분 읽기조회 12
공유

1. Spring Security란?

Spring Security는 자바 기반 웹 애플리케이션의 인증과 인가, 보안 위협 방어를 처리하는 프레임워크다. 개발자가 직접 구현하지 않아도 몇 가지 설정만으로 보안 처리를 할 수 있도록 도와주는 라이브러리 모음집이다.

핵심 개념 두 가지:

  • 인증(Authentication): 이 사용자가 누구인지 식별 → 로그인
  • 인가(Authorization): 이 사용자가 해당 리소스에 접근할 권한이 있는지 식별 → 권한 검사

보안 위협도 함께 방어한다.

  • CSRF: 인증된 사용자가 자신의 의지와 무관하게 해커가 의도한 행동을 하도록 하는 공격
  • XSS: 웹 서버 사용자에 대한 입력값 검증이 미흡할 때 발생하는 취약점
  • CORS: 다른 출처(Origin)에서 리소스에 접근하는 것을 제어하는 정책

2. 스프링 시큐리티 구조

로그인 요청이 들어왔을 때 내부적으로 어떤 흐름으로 처리되는지 먼저 파악하는 게 중요하다.

인증(Authentication) 흐름

사용자 로그인 요청
    ↓
AuthenticationFilter (UsernamePasswordAuthenticationFilter)
인증 관련 필터가 요청을 가로채고 Authentication 객체 생성
    ↓
AuthenticationManager (ProviderManager)
요청을 처리할 적절한 AuthenticationProvider를 찾아주는 역할
    ↓
AuthenticationProvider
실제 인증 로직을 처리. 인증 방식별로 여러 개 존재 가능
    ↓
UserDetailsService
DB에서 사용자 정보를 조회하고 UserDetails 객체로 반환
    ↓
SecurityContext
인증 성공 시 Authentication 객체를 SecurityContext에 저장

인가(Authorization) 흐름

인증된 사용자가 보호된 리소스 접근 시도
    ↓
FilterSecurityInterceptor
요청을 가로채고 권한 검사 시작
    ↓
AccessDecisionManager
현재 사용자의 권한과 요청된 리소스의 필요 권한을 비교
    ↓
권한 충분 → 접근 허용
권한 부족 → AccessDeniedException 발생, 접근 거부

3. 주요 객체 정리

객체역할
AuthenticationManager인증 과정을 관리하는 중심 컴포넌트. 구현체는 ProviderManager
AuthenticationProvider실제 인증 로직 처리. 인증 방식별로 여러 개 존재 가능
UserDetailsServiceDB에서 사용자 정보를 불러와 UserDetails로 반환
SecurityContext인증 완료된 사용자 정보를 저장하는 컨텍스트
SecurityContextHolderThreadLocal 기반으로 요청마다 인증 정보를 개별적으로 유지

SecurityContextHolder는 기본적으로 ThreadLocal을 사용하기 때문에 동일한 스레드 내에서는 각 사용자의 인증 정보를 개별적으로 유지한다. 어디서든 아래처럼 꺼낼 수 있다.

Authentication authentication
        = SecurityContextHolder.getContext().getAuthentication();

String username = authentication.getName();          // 사용자 이름
Collection<? extends GrantedAuthority> authorities
        = authentication.getAuthorities();           // 권한 목록

4. Filter Chain

Spring Security는 HTTP 요청을 일련의 필터들로 처리한다. 요청이 들어오면 Filter Chain의 첫 번째 필터부터 순차적으로 통과하고, 모든 필터를 통과한 요청만 실제 애플리케이션 로직에 도달한다.

주요 필터 목록:

필터역할
SecurityContextPersistenceFilter요청 간 SecurityContext 유지, 이전 인증 정보 복원
UsernamePasswordAuthenticationFilter폼 기반 로그인 처리
AnonymousAuthenticationFilter인증되지 않은 요청에 익명 사용자 인증 제공
ExceptionTranslationFilterSecurity 예외를 HTTP 응답으로 변환
FilterSecurityInterceptor권한 검사를 내리는 마지막 필터

5. 폼 로그인 구현

의존성 추가

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

SecurityConfig 작성

@EnableWebSecurity
@Configuration
public class SecurityConfig {

    private final String[] allowUris = {
            "/swagger-ui/**",
            "/swagger-resources/**",
            "/v3/api-docs/**",
            "/auth/**"
    };

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(requests -> requests
                        .requestMatchers(allowUris).permitAll()  // Public API
                        .anyRequest().authenticated()            // 나머지는 인증 필요
                )
                .formLogin(form -> form
                        .defaultSuccessUrl("/swagger-ui/index.html", true)
                        .permitAll()
                )
                .logout(logout -> logout
                        .logoutUrl("/logout")
                        .logoutSuccessUrl("/login?logout")
                        .permitAll()
                );

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  • @EnableWebSecurity: Spring Security 설정 활성화. 직접 작성한 보안 설정이 기본 설정보다 우선 적용됨
  • permitAll(): 인증 없이 접근 가능한 Public API 지정
  • anyRequest().authenticated(): 그 외 모든 요청은 로그인 필요
  • BCryptPasswordEncoder: 비밀번호 솔트 처리 (단방향 해시, 복호화 불가)

UserDetails / UserDetailsService 구현

UserDetails를 상속한 AuthMember와 UserDetailsService를 상속한 CustomUserDetailsService를 만든다.

// CustomUserDetailsService
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Member member = memberRepository.findByEmail(username)
                .orElseThrow(() -> new UsernameNotFoundException(username));
        return new AuthMember(member);
    }
}

loadUserByUsername에서 DB를 조회해 AuthMember를 반환하면, Spring Security가 내부적으로 비밀번호 검증까지 자동으로 처리해준다.


6. 인증 실패 응답 통일

기본적으로 인증 실패 시 HTML 응답이 내려와서 API 응답 형식이 깨진다. ExceptionTranslationFilter가 담당하는 두 예외를 커스터마이징해서 JSON으로 통일할 수 있다.

  • AuthenticationEntryPoint → 미인증(401) 처리
  • AccessDeniedHandler → 권한 부족(403) 처리
// CustomEntryPoint 구현 예시
public class CustomEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(
            HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException authException
    ) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED;

        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(code.getStatus().value());

        ApiResponse<Void> errorResponse = ApiResponse.onFailure(code, null);
        objectMapper.writeValue(response.getOutputStream(), errorResponse);
    }
}

AccessDeniedHandler도 동일한 구조로 구현하고, SecurityConfig의 .exceptionHandling()에 등록하면 된다.

http.exceptionHandling(ex -> ex
        .authenticationEntryPoint(customEntryPoint())
        .accessDeniedHandler(customAccessDenied())
);

핵심 키워드

  • Spring Security가 무엇인가?
  • 인증(Authentication) vs 인가(Authorization)
  • Stateful vs Stateless
  • SecurityContextHolder의 ThreadLocal 전략
  • BCrypt 솔트 처리란?

관련 포스트

댓글

방명록을 남겨주세요