[OAuth 2.0] Spring Security 기본 설정(FilterChain + formLogin)

보안 규칙(필터 체인) 설계도

@Configuration
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login", "/error").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults())
            .logout(Customizer.withDefaults());

        return http.build();
    }
}
  • Configuration → 이 클래스가 스프링 설정 클래스를 의미
    • 애플리케이션 시작 시 Spring이 읽어서 빈(Bean)을 등록함
  • @Bean SecurityFilterChain ...
    • Spring Security가 적용되는 핵심 객체를 직접 등록하는 방식
    • Spring Security는 내부적으로 Filter들의 묶음(필터 체인)을 만들어서 Tomcat의 Filter Chain에 끼워 넣는데, 그 구성이 바로 SecurityFilterChain.

 

즉, 이 메서드는 “요청이 들어올 때 어떤 순서/규칙으로 보안 필터가 동작할지”를 정의한다.

 

  • .csrf(csrf -> csrf.disable())
    • CSRF 보호를 끔.
    • 폼 로그인 + POST 요청이 있는 경우 보통 CSRF가 필요하지만, 지금 단계는 “동작 확인”이 목적이라 끄고 진행한 것.
    • 나중에 진짜 서비스 형태가 되면 다시 켤지 판단하면 됨.

 

  • authorizeHttpRequests(...)
    • 접근 제어 규칙(인가, Authorization)
    • /login, /error는 누구나 접근 가능
    • 나머지는 전부 로그인(인증)된 사용자만 접근 가능
      • 그래서 /oauth2/authorize는 여기에 걸려서: (/oauth2/authorize로 접속하면 /login으로 넘어감)
        • 로그인 안 했으면 → 로그인 페이지로 리다이렉트
        • 로그인 했으면 → 컨트롤러까지 도달

 

  • .formLogin(Customizer.withDefaults())
    • 기본 폼 로그인을 켠다
    • 따로 로그인 페이지를 만들지 않아도 /login에 기본 UI가 뜬다
    • 인증되지 않은 사용자가 보호된 URL로 접근하면 Spring Security가 자동으로
      • 로그인 페이지로 보내고
      • 로그인 성공 후 원래 가려던 URL로 돌아오게 해준다.

 

  • .logout(Customizer.withDefaults())
    • 기본 로그아웃을 켠다.
    • 보통 /logout 요청으로 세션을 무효화하고 SecurityContext를 지운다.

 

로그인 가능한 사용자(테스트 계정) - 공급자

@Configuration
public class TestUsersConfig {

    @Bean
    UserDetailsService userDetailsService() {
        UserDetails user = User.withUsername("user")
            .password("{noop}1234")
            .roles("USER")
            .build();

        return new InMemoryUserDetailsManager(user);
    }
}
  • Spring Security에서 폼 로그인이 동작하려면 입력한 username/password가 맞는지 확인할 사용자 저장소가 필요함 → 그 역할을 하는 표준 인터페이스가 UserDetailsService

 

  • UserDetailsService
    • Spring Security가 로그인 시도할 때 내부적으로 호출하는 메소드가 있음
      • loadUserByUsername(username)
    • 아직 DB를 붙이지 않았으니 가장 간단한 인 메모리 사용자 저장소를 제공한 것

 

  • {noop} → Spring Security는 기본적으로 비밀번호 인코딩(해시) 방식이 필요함 {noop}는 “인코딩 안 함(평문 비교)”이라는 뜻

 

  • InMemoryUserDetailsManager
    • 메모리에 사용자 정보를 저장하는 구현체.
    • user/1234로 로그인 가능해짐.

 

로그인 상태인지 확인하는 테스트 엔드포인트

@RestController
public class AuthDebugController {

    @GetMapping("/oauth2/authorize")
    public String authorizeDebug(@AuthenticationPrincipal UserDetails user) {
        return "OK - logged in as: " + user.getUsername();
    }
}
  • 왜 /oauth2/authorize를 이렇게 만들었나?
    • 지금은 아직 OAuth2 인가 로직(클라이언트 검증, code 발급)이 없으니
    • “로그인/세션이 제대로 동작하는지” 확인하기 위해 /oauth2/authorize를 디버그 용으로 임시 구현한 것

 

나중에 이 엔드포인트가

  • client_id, redirect_uri 검증
  • code 발급
  • redirect

를 하게 될 거임

 

  • @AuthenticationPrincipal이 뭐냐?
    • 현재 요청을 보낸 사용자의 인증 정보(Principal)를 Spring Security가 SecurityContext에 저장해두는데 그걸 컨트롤러에서 꺼내 쓰기 쉽게 주입해주는 어노테이션.
    • 즉, 이 메소드가 호출된다다는 것 자체가 이미 SecurityFilterChain을 통과했고 인증된 사용자라는 뜻

 

시나리오

A. 로그인 안 한 상태로 /oauth2/authorize 접근

  1. 브라우저가 GET /oauth2/authorize 요청
  2. Spring Security 필터 체인이 먼저 실행됨
  3. .anyRequest().authenticated() 규칙에 걸림
  4. 인증 정보가 없으니 로그인 페이지 /login으로 리다이렉트(302)

B. 로그인 성공 후 다시 /oauth2/authorize

  1. /login에서 user/1234 입력
  2. UserDetailsService가 user 조회 → 비밀번호 비교
  3. 성공하면 인증 정보가 세션 + SecurityContext에 저장됨
  4. Spring Security가 원래 목적지였던 /oauth2/authorize로 이동시킴
  5. 컨트롤러 호출
  6. @AuthenticationPrincipal로 user 주입
  7. "OK - logged in as: user" 반환

 

Spring Security 순서

  • Spring Security에서 클라이언트가 요청했을 때 어떻게 돌아가는지부터 보자