spring

스프링시큐리티 [SpringBoot 3.x (Spring Security 6.x)]

devJK93 2025. 4. 28.

 

🔥 SpringBoot2.x

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable() // (테스트 용, 실제는 다르게)
            .authorizeRequests()
                .antMatchers("/employees/**").authenticated() // 조회는 인증만 있으면 OK
                .antMatchers(HttpMethod.PUT, "/employees/**").hasRole("ADMIN") // 수정은 ADMIN만
                .and()
            .formLogin()
                .and()
            .httpBasic(); // (간단 로그인 방식 예시)
    }
}
@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .roles(user.getRole()) // 여기에 ROLE_USER, ROLE_ADMIN 자동 prefix 됨
            .build();
    }
}

🔥 SpringBoot2.x → SpringBoot3.x

  • WebSecurityConfigurerAdapter 자체가 deprecated(사용 중단).
  • 이유는 → 람다 기반 설정(HttpSecurity) 이 더 직관적이고, 확장성 좋은 방법이라 채택.
  • 요즘은 Bean 등록 방식 + 람다 스타일로 설정.

@Configuration + SecurityFilterChain Bean 등록하는 방식

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/employees/**").authenticated()  // 인증만 필요
                .requestMatchers(HttpMethod.PUT, "/employees/**").hasRole("ADMIN") // 수정은 ADMIN만
            )
            .formLogin(withDefaults())  // Form Login 사용
            .httpBasic(withDefaults())  // HTTP Basic 인증 사용
            .build();
    }
}

 

바뀐 포인트

  • configure(HttpSecurity) 메소드 오버라이드 안 함.
  • 대신, SecurityFilterChain Bean을 등록해서 HttpSecurity를 설정한다.
  • 설정 구문도 람다식으로 바뀜 (csrf(csrf -> csrf.disable()) 이런 식으로).

 

✅ UserDetailsService는?

거의 변함없음.

@Service
public class CustomUserDetailsService implements UserDetailsService {
    // 네가 작성한 거랑 똑같이 그대로 사용 가능
}
  • 유저를 DB에서 조회해서 UserDetails 만들어주는 역할은 그대로.
  • 다만, PasswordEncoder Bean 등록을 명시적으로 해야하는 경우가 많아짐.

 

✅ PasswordEncoder도 꼭 등록해야 함

Spring Boot 3.x부터는 비밀번호 비교할 때 암호화 필수.
그래서 이런 것도 추가해야 한다.

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}
  • 로그인할 때 비밀번호 매칭용
  • 안 하면 에러 나거나 "비밀번호를 인코딩 해야 한다"는 메시지 뜬다.

 

✨ Spring Boot 3.x 기준 전체 구성 예시

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/employees/**").authenticated()
                .requestMatchers(HttpMethod.PUT, "/employees/**").hasRole("ADMIN")
            )
            .formLogin(withDefaults())
            .httpBasic(withDefaults())
            .build();
    }

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

 

CustomUserDetailsService는 그대로 쓴다!

 

🎯 요약

구분 Spring Boot 2.x 스타일 Spring Boot 3.x 스타일
설정 방법 extends WebSecurityConfigurerAdapter @Bean SecurityFilterChain 등록
설정 방식 메소드 오버라이드 람다식 설정
PasswordEncoder 암호화 강제 아님 (기본 허용) 암호화 강제 (BCrypt 등 등록 필수)

📑 Q: .authenticated() 이걸로 로그인 되었는지 안되었는지 판단하잖아 로그인이 된건 뭘 기준으로 판단하는거야?

 

 

✨ .authenticated()가 의미하는 것

.authorizeHttpRequests(auth -> auth
    .requestMatchers("/employees/**").authenticated()
)
  • 이건 "이 API는 로그인한 사용자만 접근 가능하다"는 뜻.
  • Spring Security 내부에서는 이렇게 해석:

"현재 요청한 사람이 SecurityContext에 인증(Authentication) 정보가 있고, 인증이 완료된 상태여야 한다."

 

🔥 그럼, "로그인된 상태"를 뭘 기준으로 보냐?

정리하면, Spring Security는
SecurityContextHolderAuthentication 객체가 존재하고, 그 안에 authenticated=true이면 로그인 된 것.

 

흐름을 조금 자세히 보면:

  1. 사용자가 /login, /authenticate 같은 엔드포인트로 아이디/비밀번호를 보낸다.
  2. Spring Security가 이걸 받아서 AuthenticationManager한테 검증을 맡긴다.
    • 예를 들어, UsernamePasswordAuthenticationToken을 만들어서 인증 시도.
  3. AuthenticationManagerUserDetailsService를 호출해서
    • username으로 유저 조회
    • 비밀번호 매칭 (PasswordEncoder.matches)
    • 성공하면 Authentication 객체를 만든다.
  4. 만들어진 Authentication 객체를
    • SecurityContextHolder.getContext().setAuthentication(authentication)
      이렇게 저장한다.
  5. 이후부터는 매 요청마다
    • SecurityContextHolder에 Authentication이 있는지 확인하고,
    • .isAuthenticated() == true 면 "로그인됨"이라고 판단한다.

 

📌 Authentication 객체 안에 뭐가 들어있냐?

Authentication이라는 건 일종의 로그인 정보 꾸러미.

예를 들면:

필드 내용
Principal 로그인한 사용자 객체 (UserDetails)
Credentials 보통 비밀번호 (인증 후에는 없어질 수도 있음)
Authorities 권한 목록 (ROLE_USER, ROLE_ADMIN 등)
authenticated 인증 여부 (true / false)

 

🔥 실제 SecurityContextHolder 구조 예시

로그인 성공하면 메모리에 이렇게 저장된다.

SecurityContextHolder
  ↳ SecurityContext
      ↳ Authentication (authenticated = true)
          ↳ Principal (UserDetails)
          ↳ Authorities ([ROLE_USER])
  • 이게 존재하면 .authenticated() 조건을 만족해서 API 접근 허용.
  • 없으면 (null이거나 authenticated=false면) 접근 거부 (401 Unauthorized).

 

📚 요약

구분 내용
.authenticated() 기준 SecurityContextHolder에 Authentication이 있고 authenticated=true면 인증된 사용자
인증 절차 로그인 요청 → AuthenticationManager가 인증 → SecurityContextHolder에 저장
내부 저장소 SecurityContextHolder (스레드별 SecurityContext 저장)

 

💡 추가 꿀팁

Spring Security에서 이걸 더 세세하게 쪼개서 제어하고 싶으면 @AuthenticationPrincipal을 쓰기도 한다.

예를 들면,

@GetMapping("/me")
public UserDetails me(@AuthenticationPrincipal UserDetails user) {
    return user;
}

 

 

🎯 한 줄 요약

.authenticated()는 "SecurityContextHolder에 인증된 Authentication 객체가 존재하면" 로그인 완료로 본다.


📑 Q: AuthenticationManager, SecurityContextHolder 이런 개념이 생소하다.

설명좀 해줘 전체적인 스프링시큐리티 흐름에 대해서 어찌됐든

AuthenticationManager가 검증을 통해 Authentication 객체를 만든다는거고

만들어진 Authentication 객체는 SecurityContextHolder에 저장되는건가

그 홀더는 서버 내에 있는 세션? 클라이언트에서 보내는 뭐를 기준으로

SecurityContextHolder에 Authentication이 있는지 확인하는거지?

 

 

🛠 스프링 시큐리티 전체 흐름 (큰 틀)

API 요청이 들어왔을 때 스프링 시큐리티가 인증하는 과정은 이렇다:

요청 → 필터체인 통과 → AuthenticationManager로 인증 시도 → 성공 시 Authentication 생성 → SecurityContextHolder에 저장 → 이후 요청마다 인증 정보 체크

 

요소 간 역할은:

컴포넌트 역할
AuthenticationManager 로그인 인증(인증 성공/실패 판단 담당)
Authentication 로그인한 사용자 정보 (아이디, 권한 등)
SecurityContextHolder 인증 정보를 저장하는 곳 (서버 메모리 영역, 스레드마다 따로 관리)

 

🧠 너가 말한 대로 풀어보자

어찌됐든 AuthenticationManager가 검증을 통해 Authentication 객체를 만든다는거고 만들어진 Authentication 객체는 SecurityContextHolder에 저장되는건가

 

정답.

AuthenticationManager 가 인증을 "검증" 해.
성공하면 Authentication 객체를 만들어서,
SecurityContextHolder에 저장해.
✔ 이 Authentication 안에 유저 정보(UserDetails)랑 권한(Authorities)이 들어있어.

 

📦 그럼 SecurityContextHolder는 정확히 뭔데?

SecurityContextHolder는

"현재 요청한 유저의 Authentication(인증정보)을 들고 있는 서버쪽 저장소"야.

  • HTTP 요청이 들어오면
  • Spring Security가 SecurityContextHolder에
    • "이 요청한 유저 인증정보(Authentication)가 있나?" 확인해.
  • 있다면 → "이 사람은 로그인 완료된 사람" 인정.
  • 없다면 → 401 Unauthorized 에러.

 

🧩 그럼 "SecurityContextHolder에 인증 정보가 있는지"를 뭘 보고 확인하냐?

 

세션이나 토큰 기반이다.

1. 세션 기반 (Spring Security 기본)

  • 로그인할 때 서버 세션이 생성된다. (JSESSIONID)
  • 이 세션 ID가 클라이언트 쿠키에 저장된다.
  • 클라이언트가 이후 요청마다 JSESSIONID를 같이 보낸다.
  • 서버(Spring)는 이 세션ID로 SecurityContext를 찾아서 꺼낸다.
  • SecurityContext에 Authentication 있으면 통과.

=> 정리: 클라이언트가 보내는 세션ID 쿠키를 보고 서버에서 SecurityContext를 찾는다.

 

 

2. JWT 기반 (커스텀 구현)

  • 서버가 세션을 만들지 않고 JWT 토큰을 만들어서 클라이언트에 준다.
  • 클라이언트는 Authorization 헤더에 JWT를 실어서 보낸다.
  • 서버(Spring Security Filter)가 JWT를 검증해서,
    • 직접 SecurityContextHolder에 Authentication을 "수동으로" 넣는다.
  • 이후 요청 처리.

=> 정리: 클라이언트가 보내는 토큰을 보고 서버에서 SecurityContext를 세팅한다.

 

🗺 전체 그림 요약 (세션 기반 기준)

(1) 로그인 요청 (POST /login)
    - AuthenticationManager가 인증 (DB 조회 + 비번 검증)
    - 인증 성공 → Authentication 객체 생성
    - SecurityContextHolder에 Authentication 저장
    - 세션에 SecurityContext 저장
    - 세션ID(JSESSIONID) 쿠키 발급

(2) 이후 요청
    - 클라이언트가 JSESSIONID 쿠키를 같이 보냄
    - 서버가 세션ID로 SecurityContext 찾음
    - SecurityContextHolder에 Authentication이 있나 확인
    - 있으면 로그인 성공한 사람 취급
    - 없으면 401 Unauthorized

 

📚 용어 다시 한 번 정리

용어 설명
Authentication 인증된 사용자 정보(Principal, 권한 등) 담은 객체
AuthenticationManager 로그인 요청을 처리해서 인증 성공/실패 판별하는 놈
SecurityContextHolder 현재 요청의 인증(Authentication) 정보를 담는 저장소 (스레드 로컬)
SecurityContext SecurityContextHolder 안에 들어있는 객체 (Authentication을 가짐)

 

🧤 추가로, 진짜 작은 디테일 하나

  • SecurityContextHolder는 기본적으로 ThreadLocal을 쓴다.
  • 그래서 "요청 단위(쓰레드 단위)"로 인증 정보가 분리된다.
  • (비동기 처리 같은 걸 하면 이거 주의해야 해.)

 

🔥 정리 한 줄

"AuthenticationManager가 로그인 검증 후 Authentication을 만들고, 그걸 SecurityContextHolder에 저장해두었다가, 요청이 올 때마다 인증 여부를 확인한다."

 

 

이제 이거 이해했으면,
👉 다음 레벨은 "JWT를 쓴다면 Authentication은 어떻게 만들어서 SecurityContextHolder에 넣냐" 이거야.

혹시 이어서 JWT 방식 흐름도 이어서 보고 싶어?


📝 Authentication vs Authorization 

1. Authentication : 로그인 (인증)
2. Authorization : 권한

보통 인증 후에 권한 체크

 

비슷해보이지만 다른 두 친구를 소개합니다. Authentication vs Authorization | 아웃풋 트레이닝

내외부 관련된 서비스를 런칭하거나 기능을 추가할때 같이 논의되는 Auth에 대해 들어보셨을텐데요, 보통 Auth라고 하면 Authentication을 말하거나 Authentication + Authorization를 통칭합니다. 이렇게 구분

baek.dev


 

 

댓글