어제 오늘 내일

[Spring Security] 8편 - SecurityConfig 대수술 (FormLogin 삭제 & JWT 필터 적용) 본문

IT/SpringBoot

[Spring Security] 8편 - SecurityConfig 대수술 (FormLogin 삭제 & JWT 필터 적용)

hi.anna 2026. 2. 20. 00:19

지난 시간까지 JWT 토큰 발급기(JwtTokenProvider)를 만들었습니다.
이제 스프링 시큐리티 설정을 뜯어고쳐서 세션 기반의 Form Login을 걷어내고, JWT 필터를 장착할 차례입니다.

이 과정이 끝나면 기존의 로그인 화면은 동작하지 않게 되며, 완전히 새로운 방식(API)으로 로그인을 처리하게 됩니다.

우리는 이제 세션 기반의 "페이지 이동식 로그인"을 버리고, REST API 방식(데이터만 주고받는 방식)으로 전환합니다.

더 이상 서버가 "로그인 페이지를 띄워주고, 로그인 성공하면 메인으로 튕겨주는" 역할을 하지 않습니다.
서버는 오직 "ID/PW를 주면 토큰을 던져주는" 역할만 합니다.


Step 1. JwtAuthenticationFilter 만들기 (보안 검색대)

먼저, 요청이 들어올 때마다 헤더에 있는 토큰을 검사할 필터를 만듭니다.
이 필터는 클라이언트가 보낸 "출입증(Token)"이 유효한지 확인하고, 유효하다면 "통과!" 도장을 찍어주는 역할을 합니다.

  • 위치: src/main/java/com/example/board/jwt/JwtAuthenticationFilter.java
package com.example.board.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;

import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        // 1. Request Header 에서 JWT 토큰 추출
        String token = resolveToken((HttpServletRequest) request);

        // 2. validateToken 으로 토큰 유효성 검사
        if (token != null && jwtTokenProvider.validateToken(token)) {
            // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        // 3. 다음 필터로 이동
        chain.doFilter(request, response);
    }

    // Request Header 에서 토큰 정보 추출
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

Step 2. SecurityConfig 수정 (★가장 중요)

여기가 오늘 가장 중요한 부분입니다. 기존 코드와 비교해보세요.

[삭제되는 것]

  • .formLogin(...): 이제 HTML 폼 로그인을 쓰지 않습니다.
  • .logout(...): 세션을 안 쓰니 로그아웃(세션 삭제) 개념도 서버엔 없습니다.

[추가되는 것]

  • .sessionManagement(...STATELESS): 세션을 아예 생성하지 않겠다고 선언합니다.
  • .addFilterBefore(...): 방금 만든 JWT 필터를 끼워 넣습니다.
  • 위치: src/main/java/com/example/board/config/SecurityConfig.java
package com.example.board.config;

import com.example.board.jwt.JwtAuthenticationFilter;
import com.example.board.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // [PART 1] 기본 설정 비활성화
            .httpBasic((basic) -> basic.disable()) // UI를 사용하는 기본 인증 비활성화 (API만 사용하므로)
            .csrf((csrf) -> csrf.disable()) // CSRF 보안 비활성화 (API 서버라 필요 없음)

            // [PART 2] 세션 미사용 설정 (가장 중요!)
            .sessionManagement((session) -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )

            // [PART 3] 기존의 화면 깨짐 방지 설정 유지
            .headers((headers) -> headers
                .frameOptions((frame) -> frame.sameOrigin())
            )

            // [PART 4] URL 별 권한 관리
            .authorizeHttpRequests((auth) -> auth
                // 기존 경로 유지 (로그인, 가입 등은 누구나 접근 가능)
                .requestMatchers("/", "/login", "/join", "/test").permitAll()
                // H2 Console 접속 허용
                .requestMatchers(PathRequest.toH2Console()).permitAll()
                // 그 외 모든 요청은 인증 필요
                .anyRequest().authenticated()
            )

            // [PART 5] JWT 필터 등록
            // 기존의 UsernamePasswordAuthenticationFilter 앞에 우리가 만든 필터를 끼워 넣음
            .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);

        // ▼ formLogin(), logout() 설정은 이제 필요 없어서 삭제했습니다! ▼

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

💡 왜 .formLogin()을 지웠나요?

  • 세션 vs 토큰: formLogin은 로그인 성공 시 서버가 세션(Session)을 만들고 브라우저를 리다이렉트 시켜주는 방식입니다.
  • 우리의 목표: 우리는 이제 "아이디/비번을 주면 -> 토큰(JSON)을 주는" API 서버를 만들고 있습니다. 따라서 HTML 폼을 처리하는 기능은 불필요합니다.
  • 그럼 로그인은 어떻게 해요? 다음 9편에서 만들 MemberController의 API가 그 역할을 대신하게 됩니다.

Step 3. 다음 단계 예고

이제 서버는 "세션을 만들지 않는 무상태(Stateless)" 모드로 전환되었습니다.
그리고 요청이 들어올 때마다 JwtAuthenticationFilter가 토큰을 검사합니다.

하지만 아직 "토큰을 발급해 주는 API"가 없습니다.
(기존의 /login 페이지는 SecurityConfig에서 기능이 삭제되었기 때문에 접속해도 아무 기능도 하지 않거나 에러가 날 수 있습니다.)

다음 9편(완결)에서는:

  1. 사용자에게 ID/PW를 받아서 검증하고,
  2. JwtTokenProvider를 사용해 진짜 토큰을 생성해서 반환하는 API Controller를 구현하겠습니다.
  3. 그리고 Postman을 이용해 실제 로그인을 테스트해 보겠습니다.

드디어 끝이 보입니다! 🚀

반응형
Comments