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 | 실제 인증 로직 처리. 인증 방식별로 여러 개 존재 가능 |
UserDetailsService | DB에서 사용자 정보를 불러와 UserDetails로 반환 |
SecurityContext | 인증 완료된 사용자 정보를 저장하는 컨텍스트 |
SecurityContextHolder | ThreadLocal 기반으로 요청마다 인증 정보를 개별적으로 유지 |
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 | 인증되지 않은 요청에 익명 사용자 인증 제공 |
ExceptionTranslationFilter | Security 예외를 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 솔트 처리란?
