[Spring] 스프링 시큐리티(Spring Security)

2025. 10. 17. 17:48·Spring

※ 개발 관점에서 작성한 포스팅입니다.

 

인증과 인가에 대해 먼저 알아보자.

 

인증(Authentication)

인증은 서비스에 접근하려는 사용자의 신원을 확인하는 과정으로, 주로 로그인을 통해 이루어진다.

 

인가(Authorization)

인가는 인증된 사용자가 특정 자원이나 기능에 접근할 수 있는 권한이 있는지를 결정하는 과정이다.

 


스프링 시큐리티(Spring Security)

스프링 시큐리티는 모든 요청에 대해서 인증과 인가를 거치게 해주는 보안 프레임워크이다.

 

Dispatcher Servlet은 http 요청이 들어오면 해당 요청을 처리한다.

하지만 요청한 클라이언트의 인증·인가를 검사하려면, Spring Security가 먼저 http 요청을 가로채어 수행되어야 한다.

이때, Spring Security는 인증·인가를 필터(Filter) 체인 형태로 순차적으로 실행한다.

 

 

 

필터 중 하나인 UsernamePasswordAuthenticationFilter의 사용자 인증 흐름을 살펴보자.

 

1. 아이디/비밀번호가 담긴 클라이언트의 요청(로그인)이 왔을 때, AuthenticationFilter(UsernamePasswordAuthenticationFilter)가 수행된다.

2. 아이디와 비밀번호로 UsernamePasswordAuthenticationToken을 생성한다.

3. AuthenticationManager에게 생성한 토큰을 넘겨준다.

4. AuthenticationManager(ProviderManager)는 AuthenticationProvider 목록 중 DaoAuthenticationProvider을 선택한다.

6. DaoAuthenticationProvider는 토큰의 아이디를 UserDetailsService에게 넘겨준다.

7. UserDetailsServices의 loadUserByUsername()은 아이디로 비밀번호를 얻어 UserDetails 객체를 반환한다.

9. PasswordEncoder로 UserDetails의 비밀번호(암호문)와 토큰의 비밀번호(평문)을 인코딩해 대조한다.
    (같다면 인증 성공, 다르다면 인증 실패)

10. AuthenticationFilter는 대조(인증) 결과에 따라 후처리를 실행한다.

11. 인증에 성공했다면 SecurityContext에 Authentication(사용자 정보)를 저장한다.

     (SecurityContext는 SecurityContextHolder에 의해 관리된다.)

 

 

인증을 마치면, 인가 설정도 해야겠지??? 🧐

 

인가 설정은 간단하다.

HttpSecurity 설정을 통해 URL 패턴에 대한 접근 권한을 설정하면 된다.

 

특정 URL들에 대해 인증된 사용자만 권한을 부여하고 싶다면, authenticated()로 접근 권한을 설정하자.

또한 회원가입을 진행할 때는 요청에 대한 인증·인가가 필요하지 않다.

때문에 해당 요청이 들어오면 authenticated()가 아닌 permitAll()로 모두 접근이 가능하도록 설정하자!

 


 

DelegatingFilterProxy → FilterChainProxy

UsernamePasswordAuthenticationFilter 뿐만 아니라, SessionManagementFilter, ExceptionTranslationFilter 등

다양한 필터가 체인(Chain) 형태로 순차적으로 실행된다.

 

그렇다면 필터는 어디서 어떻게 실행될까?

 

DelegatingFilterProxy

 

맞다. 제목에서처럼 DelegatingFilterProxy로부터 필터가 실행된다!

DelegatingFilterProxy는 서블릿 컨테이너(톰캣 등)에 위치해있다.

서블릿 컨테이너 참고 |   2025.10.02 - [Spring] - [Spring] 서블릿(Servlet)/서블릿 컨테이너(Servlet Container)

 

하지만 DelegatingFilterProxy는 서블릿 컨테이너의 http 요청을 받기만 하고,

스프링 빈에 있는 FilterChainProxy에게 요청을 대신해줘라~하고 DelegatingFilterProxy의 doFilter()를 통해 위임한다!

 

FilterChainProxy

 

스프링 빈에 있는 FilterChainProxy는 요청을 위임받아 차례대로 필터들을 수행한다.

따라서 스프링 시큐리티 로직을 수행하는 주체는 사실상 FilterChainProxy라 할 수 있다.

모든 필터들이 수행되고 나면, 요청은 다시 → DelegatingFilterProxy → DispatcherServlet으로 돌아와 요청을 처리한다.

 

참고로 필터는 이렇게나 많다..

 


 

예제 코드

 

SecurityFilterChain 설정

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
            .csrf(AbstractHttpConfigurer::disable) // csrf 비활성화
            .httpBasic(AbstractHttpConfigurer::disable) // http basic 인증 방식 비활성화
            .formLogin(AbstractHttpConfigurer::disable) // 폼 로그인 비활성화
            .sessionManagement( // 세션 관리 설정
                    session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            );

    http
            .authorizeHttpRequests(
            auth -> {
                auth
                        .requestMatchers(HttpMethod.POST, "/api/users/join").permitAll()
                        .requestMatchers(HttpMethod.POST, "/api/users/login").permitAll()
                        .anyRequest().authenticated();
            });

    http
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);

    return http.build();
}

 

 

SecurityFilterChain 설정으로, 특정 url 경로에 어떤 보안이 적용되어야 할 지를 선언하는 것이다.

  • authorizeHttpRequests를 두어 회원가입과 로그인을 제외한 모든 api 요청에 대해, 인증된 사용자에 대한 접근 권한을 부여하였다.
  • addFilterBefore(): UsernamePasswordAuthenticationFilter을 실행하기 이전에 jwtFilter(JWT 토큰 인증 필터)를 미리 수행하고자 함.
  • addFilterAt(): UsernamePasswordAuthenticationFilter 대신 loginFilter를 등록
        (loginFilter가 로그인 시 인증을 하려는 필터!)

 

AuthenticationFilter(UsernamePasswordAuthenticationFilter)

public class LoginFilter extends AbstractAuthenticationProcessingFilter {
	@Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException {
        LoginRequestDto loginRequestDto = objectMapper.readValue(request.getInputStream(), LoginRequestDto.class);
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(loginRequestDto.getLoginId(), loginRequestDto.getPassword());

        return getAuthenticationManager().authenticate(authToken);
    }

    protected void successfulAuthentication(HttpServletRequest request
                                            , HttpServletResponse response
                                            , FilterChain chain
                                            , Authentication authResult) throws IOException {
        String loginId = authResult.getName();
        User user = userRepository.findByLoginId(loginId)
                .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

        ReissueResponseDto tokens = jwtUtil.createTokens(loginId);

        LoginResponseDto loginResponseDto = new LoginResponseDto(user.getId());

        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        new ObjectMapper().writeValue(response.getWriter(), loginResponseDto);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request,
                                              HttpServletResponse response,
                                              AuthenticationException failed) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json");
        response.getWriter().write(objectMapper.writeValueAsString(
                ErrorResponse.of(ErrorCode.USER_NOT_FOUND)
        ));
    }
}

 

클라이언트로부터 받은 아이디/비밀번호의 Request로 인증을 시도한 후, 그 결과에 따라 수행해야 하는 작업을 오버라이딩한 것이다.

  • attemptAuthentication(): request로부터 UsernamePasswordAuthenticationToken의 토큰을 만들어 인증 시도
        (by AuthenticationManager)
  • successfulAuthentication() / unsuccessfulAuthentication(): 인증 성공 여부에 따라 작업 수행 

 

CustomUserDetailsService

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException {
        User user = userRepository.findByLoginId(loginId)
                .orElseThrow(() -> new UsernameNotFoundException(ErrorCode.USER_NOT_FOUND.getMessage()));

        return CustomUserDetails.builder()
                .loginId(user.getLoginId())
                .password(user.getPassword())
                .roles(List.of("ROLE_USER"))
                .build();
    }
}

 

AuthenticationManager는 내부적으로 인증을 시도하기 위해 UserDetails 객체가 필요하다!

따라서 UserDetailsService에서 사용자 요청을 토큰화한 id로 password(비밀번호)를 찾은 다음,

UserDetails 객체를 만들어 반환하는 코드이다.

 


 

마무리

스프링 시큐리티에 대해서 알아보았다.!

모든 서비스는 보안이 중요하므로, 매 요청마다 사용자의 접근 권한을 확인하면 좋다.

또한 예제 코드에만 나왔지만, jwt 토큰을 함께 전달받아 토큰의 유효성을 검증하는 로직을 추가하면

보안을 더욱 강화할 수 있을 것이다.😊

 

참고

https://hello-judy-world.tistory.com/216

https://prao.tistory.com/entry/Spring-Security-Spring-Security%EC%99%80-JWT-%EC%A0%81%EC%9A%A9-%EA%B3%BC%EC%A0%95#google_vignette

 

'Spring' 카테고리의 다른 글

[Spring] JDBC / Spring JDBC  (0) 2026.01.25
[Spring] 서블릿(Servlet)/서블릿 컨테이너(Servlet Container)  (0) 2025.10.02
'Spring' 카테고리의 다른 글
  • [Spring] JDBC / Spring JDBC
  • [Spring] 서블릿(Servlet)/서블릿 컨테이너(Servlet Container)
jjangsudiary
jjangsudiary
jjangsudiary 님의 블로그 입니다.
  • jjangsudiary
    jjangsudiary 님의 블로그
    jjangsudiary
  • 전체
    오늘
    어제
    • 분류 전체보기 (81) N
      • 이모저모 (0)
        • 회고 (0)
      • Development (17) N
        • 개발 공부 (14) N
        • 프로젝트 (2)
      • Android (10)
        • Compose (1)
      • AI (15)
      • Computer Science (25)
        • 네트워크 (8)
        • 데이터베이스 (10)
        • 운영체제 (6)
        • 자료구조 (0)
        • 컴퓨터구조 (1)
      • Java (9)
        • 디자인패턴 (2)
      • Spring (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    인공지능
    database
    CS
    Ai
    코딩 테스트
    백준
    프로그래머스
    운영체제
    딥러닝
    db
    Python
    TensorFlow
    안드로이드
    파이썬
    java
    android
    머신러닝
    baekjoon
    os
    자바
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
jjangsudiary
[Spring] 스프링 시큐리티(Spring Security)
상단으로

티스토리툴바