복잡한뇌구조마냥

[Spring] Spring Security + JWT 인증 구조 본문

BE/Spring

[Spring] Spring Security + JWT 인증 구조

지금해냥 2025. 8. 28. 09:13

1. 스프링 시큐리티란?

  • 스프링 기반 애플리케이션의 인증(Authentication) & 인가(Authorization) 프레임워크
  • 필터 기반 아키텍처로 동작
  • 내부적으로 SecurityFilterChain을 기반으로 요청을 처리함
  • 특징:
    • 필터 기반 구조 (서블릿 컨테이너 레벨에서 동작)
    • 설정 기반 보안 (configure → bean 등록)
    • 확장성이 뛰어남 (커스텀 필터 추가 가능)
    • OAuth2, JWT, Form Login, Session 기반 등 다양하게 지원

2. 인증 vs 인가 개념 정리

✔ 인증(Authentication)

"누구인지 확인하는 단계"

  • 로그인 시도 → 토큰/JWT/세션 → 본인임을 증명

✔ 인가(Authorization)

"권한이 있는지 확인하는 단계"

  • ex) 어떤 게시글의 수정 버튼을 누를 때 → ROLE_USER가 가능한지 체크

👉 요약 그림

[사용자 정보 확인] = 인증
[해당 자원이 가능한 권한인지 체크] = 인가

3. 스프링 시큐리티 요청 흐름 (JWT 기준)

요청 흐름 전체:

  1. 사용자가 로그인 요청을 보냄
  2. 커스텀 필터(CustomAuthenticationFilter)가 동작
    → MemberService로 DB 조회
    → 비밀번호 비교
    → 성공 시 JWT 발급
  3. 이후 클라이언트는 Authorization 헤더에 JWT 포함
    → Bearer xxx.yyy.zzz
  4. JwtAuthenticationFilter가 요청마다 헤더의 JWT 검증 수행
    → 사용자 정보(SecurityContextHolder)에 저장
  5. 컨트롤러 @PreAuthorize / 권한 체크

4. SecurityConfig — 프로젝트의 보안 정책을 모두 정의하는 핵심 클래스

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    
    private final CustomUserDetailService userDetailService;
    private final CustomAuthenticationFilter customAuthenticationFilter;
    private final CustomOAuth2UserService customOAuth2UserService;
    private final OAuth2SuccessHandler oAuth2SuccessHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                .csrf(AbstractHttpConfigurer::disable)
                .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
                .cors(cors -> cors.configurationSource(corsConfigurationSource()))
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(HttpMethod.POST, "/api/v1/auth/login").permitAll()
                        .requestMatchers("/api/v1/auth/**").permitAll()
                        .anyRequest().authenticated()
                )
                .oauth2Login(oauth2 -> oauth2
                        .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
                        .successHandler(oAuth2SuccessHandler)
                )
                .addFilterBefore(customAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

🔍 핵심 포인트 정리

1) 세션 비활성화

.sessionCreationPolicy(SessionCreationPolicy.STATELESS)

JWT 사용 시 세션 방식이 아니므로 반드시 STATELESS로 지정해야 한다.

2) CORS 설정 별도 적용

리액트/웹앱에서 요청 시 반드시 필요.

3) 허용 URL 정의

로그인 엔드포인트(/api/v1/auth/login)는 permitAll
나머지는 인증 필요.

4) 커스텀 로그인 필터 등록

.addFilterBefore(customAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)

스프링 기본 UsernamePasswordAuthenticationFilter 이전 단계에서
CustomAuthenticationFilter를 실행하도록 설정한다.


5. CustomAuthenticationFilter — 로그인 요청을 가로채는 핵심 역할

이 필터는 사용자의 로그인 요청을 처리하고, 인증 성공 시 JWT 발급을 담당한다.

public class CustomAuthenticationFilter extends OncePerRequestFilter {

    private final MemberService memberService;
    private final JwtProvider jwtProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        // 로그인 요청만 처리
        if (!isLoginRequest(request)) {
            chain.doFilter(request, response);
            return;
        }

        LoginRequest loginRequest = objectMapper.readValue(request.getInputStream(), LoginRequest.class);

        SecurityUser securityUser = memberService.authenticateUser(loginRequest);

        // 인증 성공 → JWT 발급
        String accessToken = jwtProvider.generateAccessToken(securityUser);

        writeResponse(response, accessToken);
    }
}

🧩 CustomAuthenticationFilter 핵심 역할

기능 설명
로그인 요청 감지 /api/v1/auth/login 같은 특정 경로만 필터링
요청 바디 파싱 JSON → LoginRequest
사용자 인증 처리 MemberService를 통해 email/pw 검증
JWT 발급 JwtProvider.generateAccessToken() 호출
응답 생성 토큰 반환

특히 이 필터는 스프링 시큐리티의 기본 인증 흐름을 아예 커스텀으로 대체하는 방식이므로, 실무에서 많이 사용되는 패턴이다.


6. CustomUserDetailService — DB에서 회원 정보를 조회하는 인증 로직

@Service
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String email) {
        Member member = memberRepository.findByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException("회원 정보를 찾을 수 없습니다."));
        return new SecurityUser(member);
    }
}

⭐ 역할 요약

  • 스프링 시큐리티가 인증 시 호출
  • DB에서 email 기반 사용자 조회(아이디를 사용하면 username 사용)
  • 조회된 엔티티를 SecurityUser 형태로 래핑하여 반환

이 메서드가 반환하는 UserDetails 구현체가 AuthenticationManager 내부에서 사용된다.


7. SecurityUser — 인증된 사용자를 시큐리티 컨텍스트에 올리는 객체

@Getter
public class SecurityUser implements UserDetails {
    
    private final Long id;
    private final String email;
    private final String nickname;
    private final Collection<? extends GrantedAuthority> authorities;

    public SecurityUser(Member member) {
        this.id = member.getId();
        this.email = member.getEmail();
        this.nickname = member.getNickname();
        this.authorities = List.of(new SimpleGrantedAuthority(member.getRole().name()));
    }

    @Override public String getPassword() { return null; }
    @Override public String getUsername() { return email; }
    @Override public boolean isAccountNonExpired() { return true; }
    @Override public boolean isAccountNonLocked() { return true; }
    @Override public boolean isCredentialsNonExpired() { return true; }
    @Override public boolean isEnabled() { return true; }
}

📝 정리

SecurityUser는 인증이 완료된 사용자 정보를 시큐리티 컨텍스트(SecurityContextHolder)에 저장하는 역할을 한다.

여기서 비밀번호는 JWT 방식이므로 인증 이후에는 필요 없기 때문에 null 처리했다.


8. 전체 JWT 인증 흐름 정리

  • 전체 플로우 요약
    1. 클라이언트 → /api/v1/auth/login POST 요청
    2. CustomAuthenticationFilter가 요청 바디 파싱
    3. UserDetailService에서 사용자 조회
    4. 비밀번호 검증
    5. JWT 발급
    6. 응답으로 토큰 반환
    7. 이후 Authorization 헤더 기반 인증
    8. JwtAuthenticationFilter에서 매 요청마다 토큰 검증
    9. SecurityContextHolder에 SecurityUser 저장
    10. 컨트롤러 진입 → @AuthenticationPrincipal로 사용자 정보 접근 가능

🏁 마무리

정리한 내용은 실제 서비스에서 가장 많이 사용하는 방식인 Spring Security + JWT 기반 인증 흐름 구조입니다.
SecurityFilterChain부터 CustomAuthenticationFilter까지 전체적인 흐름이 이해되면,

확장(Refresh Token / 권한 구분 / Role 기반 접근 제어)도 훨씬 쉬워집니다.

LIST

'BE > Spring' 카테고리의 다른 글

[Spring] Spring Security + OAuth2 로그인  (0) 2025.10.15
[Spring] JPA + QueryDSL 정리  (0) 2025.10.02
[Spring] MyBatis 구조 정리  (0) 2025.08.29
[Spring] 스프링 부트 생성  (1) 2025.06.18
[Spring + JPA] 인프런 김영한님 커리큘럼  (2) 2025.06.18