Spring Security??

위승현·2025년 1월 3일
1

Spring

목록 보기
11/12

Spring Security 란 무엇일까??
항상 새로 접하게되는 것들이 있다면 나 혼자만 이해를 못하는 것이 아닐까 생각이 든댜..

저는 벌레입니다 죄송합니다..

Spring Security에 대한 특강을 듣다가 특강만 보고
바로 실제로 적용시키는데에는 아직 무리라고 판단!!!!!!

따라서 현재 내 글은 스프링 시큐리티 (편의상 한글로 쓰겠어) 를 이해하는 것이 주 목적이며
거기에서 나아가 실제로 적용시킬 때 어떤 흐름으로 진행될지를 정리하고자한다.

우선 스프링 시큐리티의 흐름을 정확히 이해하고 가는게 우선이지 않을까?


스프링 시큐리티의 흐름~

Spring Security는 결국 애플리케이션에서 사용자가 누구인지 확인(인증)하고,
그 사용자가 무엇을 할 수 있는지 결정(인가) 하는 역할을 담당하는 것이라고 보면 된다.

1. 인증(Authentication): "너 누구야?"

인증은 사용자가 본인이 누구인지 증명하는 과정이다.
예를 들어, 사용자가 이메일과 비밀번호로 로그인할 때 진행된다.

인증의 주요 단계

  1. 사용자 요청:

    • 사용자가 웹사이트에서 로그인 폼에 이메일과 비밀번호를 입력
  2. AuthenticationFilter (인증 필터) :

    • 로그인 요청이 들어오면 AuthenticationFilter가 이를 가로챈다.
    • 로그인 정보를 AuthenticationManager로 넘긴다.
  3. AuthenticationManager (인증 관리자) :

    • 인증 관리자 역할을 한다.
    • 인증 과정을 직접 처리하지 않고, 이를 AuthenticationProvider에 위임한다.
  4. AuthenticationProvider (인증 공급자) :

    • 실제 인증 로직을 구현한다.
    • 다음 두 가지를 사용하여 인증을 진행 :
      1. UserDetailsService (사용자 정보 조회 서비스):
        사용자가 입력한 이메일을 데이터베이스에서 조회.
      2. PasswordEncoder (비밀번호 암호화기) :
        사용자가 입력한 비밀번호를 암호화된 비밀번호와 비교.
  5. SecurityContext (보안 컨텍스트) :

    • 인증에 성공하면, Spring Security는 사용자의 정보를 SecurityContext에 저장
    • 이 정보는 이후에 사용자가 접근하는 모든 요청에 재사용된다.

결과

  • 인증 성공: 사용자는 "인증된 사용자"로 간주되고, 로그인 성공.
  • 인증 실패: 로그인 실패 메시지를 사용자에게 반환.

2. 인가(Authorization): "너 여기 들어올 수 있어?"

인가는 인증된 사용자가 특정 리소스에 접근할 권한이 있는지 확인하는 과정이다.
예를 들어, 관리 페이지(/admin)는 관리자만 접근할 수 있도록 설정한다.

인가의 주요 단계

  1. AuthorizationFilter:
    • 로그인한 사용자가 특정 보호된 리소스 (/admin 등) 에 접근 요청을 보낸다.
    • 요청이 들어오면 AuthorizationFilter가 이를 가로챈다.
    • 사용자의 역할이 요청한 리소스에 필요한 권한을 충족하는지 확인한다.
  1. SecurityContext (보안 컨텍스트) :
    • Spring Security는 사용자의 정보를 SecurityContext에서 확인한다.
    • 사용자의 역할(Role)과 권한(GrantedAuthority)을 확인한다.
  1. 결정:
    • 권한이 있다면: 요청을 허용하고 컨트롤러로 전달한다.
    • 권한이 없다면: 403 Forbidden 응답을 반환한다.

한눈에 보는 역할 요약

구성 요소역할
AuthenticationFilter인증 요청을 가로채고 인증 매니저에 전달.
AuthenticationManager인증 요청을 인증 공급자에게 위임.
AuthenticationProvider실제 인증 로직 구현.
UserDetailsService사용자 정보를 데이터베이스에서 조회.
PasswordEncoder비밀번호 암호화 및 검증.
SecurityContext인증된 사용자의 정보를 저장.
AuthorizationFilter사용자의 권한을 확인하고 요청 허용 여부 결정.

결론

Spring Security는 인증(Authentication)과 인가(Authorization)를 자동화하고
간소화하는 강력한 보안 프레임워크이다.
위에서 설명한 구성 요소들이 유기적으로 동작하여
사용자의 신원을 확인하고, 적절한 권한을 가진 사용자만 리소스에 접근하도록 보장해준다.


Spring Security의 큰 그림

좋았어 이제 대충 흐름은 알았어. 그러면 이제는 흐름에서 각 역할을 상세하게 파보자.
파트의 대제목도 스프링 시큐리티의 큰 그림이니까 진짜로 큰 그림으로 살펴보자 ㅋㅋㅋ

인증, 인가 그림

그림을 살펴보면 색깔이 노랗게 칠해진 부분, 초록색으로 칠해진 부분이 있다.
노란색은 클래스, 초록색은 인터페이스를 의미한다!!

스프링 시큐리티의 흐름인데 대부분 인터페이스로 되어있다? 이것은 무슨 의미일까?
스프링 시큐리티는 보안을 도와주는 프레임워크 이다.

즉 시큐리티의 기본 기능들은 내부적으로 인터페이스를 구현한 구현체 클래스들에 이미
구현되어 있다는 사실을 의미한다!!

이미 구현되어있으니까 그냥 갖다 쓰면 되는거 아니야??
맞는 말이다. 하지만 모든 경우의 보안을 보장하는 것이 아니기에 우리가 구현할 프로젝트에
필요한 보안 수준으로 필요한 구성요소들만 재정의하여 구현해서 우리 프로젝트만의
맞춤 보안을 만들어내는 것이 우리의 역할이다.

  • UserDetailsService, UserDetails

스프링 시큐리티를 프로젝트에 적용할 때 다른 인터페이스들은 기본 구현체를 사용하는 경우가
많지만, UserDetailsService, UserDetails는 대부분의 프로젝트에서 사용자 인증을 위해
커스터 마이징이 필요한 인터페이스이다.

  • 왜 구현해야하는가?

    • UserDetailsService는 스프링 시큐리티가 사용자를 인증하기 위해
      사용자 정보를 조회하는 인터페이스이다.

    • 기본적으로 사용자의 데이터(예: 이메일, 비밀번호, 권한)를 커스텀 데이터베이스나
      API에서 조회해야 하기 때문에, 이를 구현하는 커스텀 클래스가 필요한 것이다.

    • Spring Security 의 인증 과정을 위해 사용자 정보를 구성하고 인증 시스템과 연동하기 위함이다.

    • 쉽게 설명하자면 Spring Security는 사용자 인증 과정에서
      UserDetailsServiceUserDetails를 모두 사용하는데 우리는 각 프로젝트마다 DB도 다르고 사용자의 데이터도 다를 것이기에 구현해야한다는 의미이다.

(1) UserDetails

  • 역할: 사용자의 세부 정보를 담는 객체.
  • Spring Security는 내부적으로 UserDetails 객체를 통해 사용자 정보를 관리.
  • 여기에는 다음과 같은 정보가 포함 :
    • 사용자 이름(getUsername)
    • 비밀번호(getPassword)
    • 사용자 권한(getAuthorities)
    • 계정 상태(isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired, isEnabled)

(2) UserDetailsService

  • 역할: Spring Security가 사용자 정보를 조회하기 위한 서비스.
  • 로그인 과정에서 Spring Security는
    사용자가 입력한 username(혹은 이메일)을 기반으로 사용자 정보를 조회한다.
  • 조회된 정보를 UserDetails 객체로 반환해야 한다.

인증 흐름에서의 UserDetailsService 사용되는 과정

  1. 사용자가 로그인 요청:

    • 사용자가 로그인 화면에 이메일과 비밀번호를 입력한다.
  2. Spring Security의 인증 절차:

    • AuthenticationManagerAuthenticationProviderUserDetailsService 호출.
    • UserDetailsService는 입력받은 username(예: 이메일)을 이용해 사용자 정보를 조회.
  3. 조회된 사용자 정보 반환:

    • UserDetailsService는 조회한 사용자 정보를 UserDetails로 반환.
    • Spring Security는 반환된 UserDetails 객체를 사용해 비밀번호 검증 및 인증 처리.

(2) UserDetailsServiceImplUserDetailsImpl의 역할

  • UserDetailsServiceImpl:

    • 데이터베이스에서 사용자 정보를 조회한다.
    • 조회된 데이터를 UserDetailsImpl 객체로 변환하여 반환한다.
  • UserDetailsImpl:

    • 사용자 정보를 담는 객체로,
      Spring Security가 사용자 인증 및 권한 관리를 수행하는 데 사용된다.

    • getAuthorities()를 통해 사용자 권한을 제공하고, 계정 상태를 관리하는 메서드(isAccountNonExpired 등)를 구현

실제 구현체의 모습은 이 다음 파트인 특강을 기준으로 코드의 역할 분석하기! 에서
살펴볼 예정이니 조급해하지말고 이런 것이구나~ 를 알고 진행하면 된다!!!


  • PasswordEncoder
  • NoOpPasswordEncoder: 인코딩하지 않는다. ㅋㅋㅋ 네?
    따라서 실제 시나리오에는 절대 쓰지 말아야 한다.(NoOp는 No Operation을 의미한다)
  • StandardPasswordEncoder: SHA-256을 이용해 암호를 해시한다.
    강도가 약한 해싱 알고리즘을 사용하기 때문에 사용하지 않는 것이 좋다.
  • Pbkdf2PasswordEncoder: PBKDF2를 이용한다.
  • BCryptPasswordEncoder: bcrypt 해싱 함수로 암호를 인코딩한다.
  • SCryptPasswordEncoder: scrypt 해싱 함수로 암호를 인코딩한다.

패스워드 인코더는 구현체를 만들지 않고 이미 만들어져있는 것들을 사용한다.
대부분은 BCrypt, SCrypt 를 사용하고 함께 사용할 수 있다.
그냥 있는거 사용하는구나 하면 편리하다.



특강을 기준으로 코드의 역할 분석하기!

그래 일단 흐름은 대충 알게된 것 같아. 근데 예시 코드를 봐서는 뭐가 뭔지 구분이 안되던데???

이 세계를 파괴하고 싶지만 그럴 수는 없으니
특강에서 주어진 각 예시 코드의 모습, 역할과 서로 어떻게 연관되어 동작하는지 분석해보자


로그인 -> 토큰 반환 인증부터 차근차근!

1. 로그인 요청

@PostMapping("/login")
  public ResponseEntity<CommonResponseBody<JwtAuthResponse>> login(
      @Valid @RequestBody AccountRequest accountRequest) {
    JwtAuthResponse authResponse = this.accountService.login(accountRequest);

    return ResponseEntity.ok(new CommonResponseBody<>("로그인 성공", authResponse));
  }
  • 사용자가 이메일과 비밀번호를 입력하여
    AccountController/login API에 요청을 보낸다.
  • AccountController는 입력받은 로그인 정보를 AccountServicelogin 메서드에 전달한다.

2. AccountService 에서의 login 로직

public JwtAuthResponse login(AccountRequest accountRequest) {    // 사용자 확인.
    // 사용자 확인.
    Member member = this.memberRepository.findByEmail(accountRequest.getEmail())
        .orElseThrow(() -> new UsernameNotFoundException("이메일에 해당하는 사용자를 찾을 수 없습니다."));
    this.validatePassword(accountRequest.getPassword(), member.getPassword());

    // 사용자 인증 후 인증 객체를 저장
    Authentication authentication = this.authenticationManager.authenticate(
        new UsernamePasswordAuthenticationToken(
            accountRequest.getEmail(),
            accountRequest.getPassword())
    );
    log.info("SecurityContext에 Authentication 저장.");
    SecurityContextHolder.getContext().setAuthentication(authentication);

    // 토큰 생성
    String accessToken = this.jwtProvider.generateToken(authentication);
    log.info("토큰 생성: {}", accessToken);

    return new JwtAuthResponse(AuthenticationScheme.BEARER.getName(), accessToken);
  }
  • 사용자 검증 및 비밀번호 확인
  Member member = this.memberRepository.findByEmail(accountRequest.getEmail())
        .orElseThrow(() -> new UsernameNotFoundException("이메일에 해당하는 사용자를 찾을 수 없습니다."));
    this.validatePassword(accountRequest.getPassword(), member.getPassword());
    • 이메일로 사용자를 조회하고 입력된 비밀번호를 암호화된 비밀번호와 비교한다.
    • 이 때 비밀번호를 검증하는 것은 BCrypt 방식이다. 왜냐고? 답은 바로 뒤에서 공개한다!

  • AuthenticationManager를 통한 인증:
 // 사용자 인증 후 인증 객체를 저장
    Authentication authentication = this.authenticationManager.authenticate(
        new UsernamePasswordAuthenticationToken(
            accountRequest.getEmail(),
            accountRequest.getPassword())
    );
 - Spring Security의 `AuthenticationManager`를 사용해 인증을 진행한다.
 - 내부적으로 `AuthenticationProvider`로 인증 요청이 전달된다.
 

여기서 질문있는 사람 손? + Securityconfig의 비밀..!!

하이잇~
내부적으로 AuthenticationManager -> AuthenticationProvider 로 위임하는 과정은
어떻게 이루어지는 걸까? 답은 SecurityConfig 클래스에 존재한다
같이 비밀을 파헤쳐 보도록 하자!!

@Configuration
@RequiredArgsConstructor
@Slf4j(topic = "Security::SecurityConfig")
public class SecurityConfig {
![](https://velog.velcdn.com/images/weskii/post/a116a486-b657-4736-a8c2-1530ab4c7b05/image.png)

  private final UserDetailsService userDetailsService;

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

  @Bean
  public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
      throws Exception {
    log.info("AuthenticationManager에 위임.");
    return config.getAuthenticationManager();
  }

  @Bean
  AuthenticationProvider authenticationProvider() {
    DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
    log.info("AuthenticationProvider 설정. 구현체: {}", authProvider.getClass().getSimpleName());

    log.info("UserDetailsService에 사용자 관리 위임. 구현체: {}",
        this.userDetailsService.getClass().getSimpleName());
    authProvider.setUserDetailsService(this.userDetailsService);

    log.info("PasswordEncoder에 암호 검증 위임. 구현체: {}",
        this.passwordEncoder().getClass().getSimpleName());
    authProvider.setPasswordEncoder(passwordEncoder());

    return authProvider;
  }
}

SecuriyConfig 클래스는 Spring Security에 필요한
AuthenticationManager, AuthenticationProvider, PasswordEncoder를
빈으로 등록하는 중요한 Config 클래스이다.

이 Config 클래스 파일을 보고 알 수 있는 사실은 다음과 같다.

  1. BcryptPasswordEncoder 를 bean 으로 등록.

    • 위에서 설명했던 많은 패스워드 인코더 중 Bcrypt 를 사용하기 위해 빈으로 등록하는 모습
    • 이 패스워드 인코더는 이 프로젝트 내에서의 모든 암호화 과정에 사용된다.
  2. AuthenticationManager 를 bean 으로 등록

  3. AuthenticationProvider 를 bean 으로 등록

    • AuthenticationProvider 중 DaoAuthenticationProvider 를 사용
    • provider 가 유저 정보를 조회할 때 사용해야하는 UserDetailsService를 등록
    • 해당 Provider가 사용할 PasswordEncoder를 위에서 빈으로 등록한 BCrypt로 등록

AuthenticationManagerAuthenticationProvider 를 Bean 으로 등록함으로써
Spring Security가 의존성 주입(DI)으로 관리할 수 있게 되고

따라서 내부적으로 AuthenticationManager 가 우리가 등록한
DaoAuthenticationProvider, BCryptPasswordEncoder, UserDetailsServiceImpl
가진 AuthenticationProvider 에게 자동적으로 인증 요청을 위임하게 되는 것이고
우리는 자연스레 AuthenticationManager 의 autheticate 를 사용하기만 하면 되는 것이다.

또한 AccountService 에서 사용하던 PasswordEncoder 도 여기서 SecurityConfig 에서
bean 으로 등록한 BCrypt 가 사용되기에 아까 BCrypt 방식으로 동작한다고 한 것이다.

SecurityConfig 가 왜 있는지 의아했는데 드디어 파악을 성공했다. 휴~!

인줄 알았지? Spring Security 는 나를 그렇게 쉽게 놓아주지 않아..

UsernamePasswordAuthenticationToken???

Authentication authentication = this.authenticationManager.authenticate(
    new UsernamePasswordAuthenticationToken(
        accountRequest.getEmail(),
        accountRequest.getPassword())
);

여기서 생성되는 UsernamePasswordAuthenticationToken 은 대체 뭘까?

UsernamePasswordAuthenticationToken
Spring Security에서 사용자의 인증 정보(Authentication)를 나타내는 클래스 중 하나다.
사용자의 이메일(또는 username)비밀번호(password)를 포함하여 인증 요청을 전달하거나,
인증이 완료된 후 사용자의 인증 상태를 표현하는 데 사용된다.

즉 Authentication 을 나타내는 것 중에서 하나라는 소리 같은데....

이왕 알아볼 거 좀 더 파보도록 하자.


  1. UsernamePasswordAuthenticationToken의 정체
  • Spring Security의 Authentication 인터페이스를 구현한 클래스.
  • 사용자의 인증 정보(이메일, 비밀번호, 권한 등)를 담는 컨테이너 역할
  • 두 가지 주요 상태에서 사용된다.
    1. 인증 요청 시:
      • 이메일과 비밀번호를 담아 AuthenticationManager 에 전달
        이는AuthenticationProvider에서 사용됨.
    2. 인증 완료 후:
      • 인증이 성공하면 사용자 정보와 권한 정보를 포함한 완전한 인증 객체로 변환된다.

  1. UsernamePasswordAuthenticationToken 생성자의 구조

(1) 인증 전 상태

public UsernamePasswordAuthenticationToken(Object principal, Object credentials)
  • principal: 사용자의 고유 식별자(예: 이메일, username).
  • credentials: 사용자의 비밀번호(평문으로 전달).

(2) 인증 후 상태

public UsernamePasswordAuthenticationToken(
    Object principal,
    Object credentials,
    Collection<? extends GrantedAuthority> authorities)
  • principal: 인증된 사용자 정보(UserDetails 또는 username)
  • credentials: 인증 후에는 보통 null로 설정(비밀번호를 더 이상 보관하지 않음)
  • authorities: 사용자의 권한 정보(ROLE_USER, ROLE_ADMIN 등)

  1. UsernamePasswordAuthenticationToken 실제 인증 과정에서 생성되는 모습

인증 요청 시

인증 로직에서 사용자의 입력값(이메일과 비밀번호)을 담아 AuthenticationManager 로 전달한다.

Authentication authentication = authenticationManager.authenticate(
    new UsernamePasswordAuthenticationToken(email, password)
);
  • email: 사용자가 입력한 이메일.
  • password: 사용자가 입력한 비밀번호.
  • 이 객체는 AuthenticationManager에 전달되어 인증 요청을 나타낸다.

인증 완료 후

Spring Security는 인증이 성공하면
UsernamePasswordAuthenticationToken인증된 상태로 변환하고,
사용자 정보와 권한 정보를 담아 SecurityContext에 저장한다.

UsernamePasswordAuthenticationToken authenticatedToken =
    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticatedToken);

즉 이러한 과정이 어디인가 추가되어있겠쥬?


그 다음엔?

자!!! 이제 우리는 AuthenticationManager 에게 넘겨주는 매개변수에 대해서 알게 되었고
그 다음에 일어나는 과정들을 알아야겠지?

맨 처음 스프링 시큐리티의 흐름을 공부할 때의 순서를 기억해보자.

인증 과정이 일어나면
요청 -> AuthenticationFilter -> AuthenticationManager -> AuthenticationProvider
이러한 순서로 진행되었는데 지금 우리는 Manager 의 authenticate() 를 호출했기 때문에
당연히 Provider로 넘어가게 될 것이고 그러면 UserDetailsService, UserDetails
사용해야할 것이니 이것들이 구현되어 있겠죠??

당연히 특강 코드 예시에도 두 녀석들이 들어간다. 어떻게 구현되었는지 드디어 살펴보도록 하자!!

UserDetailsServiceImpl

@Service
@RequiredArgsConstructor
@Slf4j(topic = "Security::UserDetailsServiceImpl")
public class UserDetailsServiceImpl implements UserDetailsService {

  /**
   * Member entity의 repository.
   */
  private final MemberRepository memberRepository;

  /**
   * 입력받은 이메일에 해당하는 사용자 정보를 찾아 리턴.
   *
   * @param username username
   * @return 해당하는 사용자의 {@link UserDetailsImpl} 객체
   * @throws UsernameNotFoundException 이메일에 해당하는 사용자를 찾지 못한 경우
   * @apiNote 이 애플리케이션에서는 사용자의 이메일을 {@code username}으로 사용합니다
   */
  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Member member = this.memberRepository.findByEmail(username)
        .orElseThrow(() -> new UsernameNotFoundException("User not found"));

    log.info("찾은 사용자: {}", username);
    return new UserDetailsImpl(member);
  }
}

로그인 요청이 들어왔으므로 AuthenticationProvider
UserDetailsService 의 loadUserByUsername() 를 사용하여
해당 멤버가 DB 에 존재하는지 확인하고 해당 멤버가 존재한다면
해당 멤버의 객체로 UserDetails 를 생성하여 리턴해준다.
UserDetails 정보가 Authentication 객체에 포함되는 정보이다.

loadUserByUsername() 메서드를 구현해준 것이 UserDetailsServiceImpl 인 것이다.
그렇다면 UserDetails는 무엇일까??

UserDetailsImpl

@Getter
@RequiredArgsConstructor
@Slf4j(topic = "Security::UserDetailsImpl")
public class UserDetailsImpl implements UserDetails {

  /**
   * Member entity.
   */
  private final Member member;

  /**
   * 계정의 권한 리스트를 리턴.
   *
   * @return {@code Collection<? extends GrantedAuthority>}
   */
  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    Role role = this.member.getRole();
    log.info("사용자 권한: {}", role.getAuthorities());

    return new ArrayList<>(role.getAuthorities());
  }

  /**
   * 사용자의 자격 증명 반환.
   *
   * @return 암호
   */
  @Override
  public String getPassword() {
    return this.member.getPassword();
  }

  /**
   * 사용자의 자격 증명 반환.
   *
   * @return 사용자 이름
   */
  @Override
  public String getUsername() {
    return this.member.getEmail();
  }

  /**
   * 계정 만료.
   *
   * @return 사용 여부
   * @apiNote 사용하지 않을 경우 true를 리턴하도록 재정의.
   */
  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  /**
   * 계정 잠금.
   *
   * @return 사용 여부
   * @apiNote 사용하지 않을 경우 true를 리턴하도록 재정의.
   */
  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  /**
   * 자격 증명 만료.
   *
   * @return 사용 여부
   * @apiNote 사용하지 않을 경우 true를 리턴하도록 재정의.
   */
  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  /**
   * 계정 활성화.
   *
   * @return 사용 여부
   * @apiNote 사용할 경우 true를 리턴하도록 재정의.
   */
  @Override
  public boolean isEnabled() {
    return true;
  }
}

Spring Security의 UserDetails 인터페이스를 구현하여 사용자 정보를 정의할 수 있다.

이 클래스는 사용자의 권한(Role)과 기본 정보를 제공하며
Spring Security가 인증 처리 시 사용자 정보를 다룰 수 있도록 해준다.

loadUserByUsername() 메서드에서 매개변수로 넘겨주던 Member 엔티티가 존재하는 모습을
볼 수 있으며 해당 UserDetailsImpl 클래스를 통해 Spring Security 는 인증/인가에 필요한
다양한 작업들을 할 수 있다.

Spring Security가 Member 엔티티를 바로 사용하지 않고 UserDetails 를 사용하는 이유는
사용자 정보를 표준화된 방식으로 처리하기 위함이다.

사용자와 관련된 모든 정보를 포함하는 엔티티를 Spring Security 에서 사용할 수 있게
변환해주는 과정이라고 생각하면 쉽다.


AuthenticationManager.authenticate 메서드 하나를 파보는데 이렇게 많은 것들이
필요하다니... 방대한 흐름을 이해하는게 참 쉽지가 않다.

그래도!!! 많은 궁금증들을 해결했으니 이제 남은 로그인 로직들을 살펴보자!!

  • SecurityContext에 저장:
log.info("SecurityContext에 Authentication 저장.");
    SecurityContextHolder.getContext().setAuthentication(authentication);
  • 인증이 성공하면 Authentication 객체가 생성되고,
    Spring Security는 인증 정보를 SecurityContextHolder 를 통해
    SecurityContext에 저장한다.

  • JWT 생성 및 반환:
// 토큰 생성
    String accessToken = this.jwtProvider.generateToken(authentication);
    log.info("토큰 생성: {}", accessToken);

    return new JwtAuthResponse(AuthenticationScheme.BEARER.getName(), accessToken);
  • 인증 정보(Authentication)를 기반으로 JwtProvider에서 JWT 토큰을 생성.
  • JwtAuthResponse 객체에 JWT와 인증 스키마(Bearer)를 포함하여 반환.

또 질문이야??

로그인 과정 마지막에 반환해주는 JwtAuthResponse 와 AuthenticationScheme.BEARER
이 무엇인지 다시 궁금해졌다.

이번에는 이 녀석들에 대해서 알아보자

JwtAuthResponse

@Getter
@NoArgsConstructor(access = AccessLevel.PACKAGE)
public class JwtAuthResponse {

  /**
   * access token 인증 방식.
   */
  private String tokenAuthScheme;

  /**
   * access token.
   */
  private String accessToken;

  /**
   * 생성자.
   */
  public JwtAuthResponse(String tokenAuthScheme, String accessToken) {
    this.tokenAuthScheme = tokenAuthScheme;
    this.accessToken = accessToken;
  }
}

JwtAuthResponse는 사용자에게 인증 토큰(JWT)인증 방식을 제공하기 위해 사용되는 데이터 전송 객체(DTO) 이다.

아 이 친구는 별 거 아닌 친구네~
그냥 생성한 토큰을 반환해주는 DTO 객체구나??
근데 저건 뭐지?? Sche..me..?? 저게 인증 방식인건가..??

AuthenticationScheme.BEAR..ER? 곰..? 이게 뭐시여..

내가 아는 Bear 는 이런 것 밖에 없는데 뭐지??

@Getter
@RequiredArgsConstructor
public enum AuthenticationScheme {
  BEARER("Bearer");

  private final String name;

  /**
   * Authorization 헤더의 값으로 사용될 prefix를 생성.
   *
   * @param authenticationScheme {@link AuthenticationScheme}
   * @return 생성된 prefix
   */
  public static String generateType(AuthenticationScheme authenticationScheme) {
    return authenticationScheme.getName() + " ";
  }
}

AuthenticationScheme는 서버가 클라이언트와 통신할 때
사용하는 인증 방식을 명시적으로 정의하는 열거형(Enum) 이라고 한다.

왜 이러한 열거형 BEARER 이 필요할까?

  • HTTP 헤더 표준:

    • HTTP의 Authorization 헤더는 다양한 인증 방식을 지원한다
    • "Bearer"는 JWT를 사용하는 인증 방식 중 하나로,
      토큰 기반 인증 시스템에서 일반적으로 사용된다.
    • 예: Authorization: Bearer <JWT>
      여기서:
      • Bearer: 인증 방식.
      • <JWT>: 실제 인증에 사용되는 토큰.

    반환해줄 때 토큰 기반 인증이라는 것을 명시해주는 Bearer 을 토큰 앞에 넣음으로써
    다음에 사용자가 해당 토큰 값을 사용하면 JWT 를 쓰는 것임을 서버가 인식할 수 있게

    즉 Spring Security 가 Bearer 방식을 기반으로 토큰 인증을 처리할 수 있게
    도와주는 역할을 하는 것이다.

  • 클라이언트-서버 상호작용 흐름:
  1. 클라이언트가 /login 요청을 통해 JWT를 수신.

  2. 서버는 JwtAuthResponse를 통해 인증 방식(Bearer)과 토큰을 반환.

{
       "tokenAuthScheme": "Bearer",
       "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
  1. 클라이언트는 이후 요청에서 아래 형식으로 서버에 인증 요청
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

이렇게 요청이 와야 Spring Security 가 이해하고 인증/인가 요청을 할 수 있는 것이다~

즉 클라이언트-서버 간 JWT 기반 인증을 원활히 수행하기 위한 핵심 요소이다

로그인 전체 과정 요약 (인증)

  1. 사용자가 /login 요청 → AccountControllerAccountService.
  2. 이메일로 사용자 조회 → 비밀번호 검증.
  3. AuthenticationManagerAuthenticationProvider로 인증 요청 위임.
  4. 인증 성공 시, Authentication 객체를 SecurityContext에 저장.
  5. JwtProvider가 JWT 생성.
  6. JwtAuthResponse로 토큰 반환.

드디어 로그인 과정이 끝났다!!!!! 로그인이 성공하면 인증이

하지만 이제 시작인걸 ^^


로그아웃은 어떻게??

본격적으로 요청에 대한 인증/인가 과정을 알아보기 전에 로그아웃은 어떻게 처리하는지 살펴보자!

@PostMapping("/logout")
  public ResponseEntity<CommonResponseBody<String>> logout(HttpServletRequest request,
      HttpServletResponse response, Authentication authentication)
      throws UsernameNotFoundException {
    // 인증 정보가 있다면 로그아웃 처리.
    if (authentication != null && authentication.isAuthenticated()) {
      new SecurityContextLogoutHandler().logout(request, response, authentication);

      log.info("인증 객체의 삭제 확인: {}", SecurityContextHolder.getContext().getAuthentication() == null);
      return ResponseEntity.ok(new CommonResponseBody<>("로그아웃 성공."));
    }

    // 인증 정보가 없다면 인증되지 않았기 때문에 로그인 필요.
    throw new UsernameNotFoundException("로그인이 먼저 필요합니다.");
  }

로그아웃 메서드Spring Security를 사용하여
인증된 사용자의 세션과 관련된 모든 인증 정보를 삭제하여 로그아웃을 처리하는 메서드이다.


코드 상세 설명

  • 요청받은 인증 정보 확인
if (authentication != null && authentication.isAuthenticated()) {
    // ...
} else {
    throw new UsernameNotFoundException("로그인이 먼저 필요합니다.");
}
  • authentication:
    • Spring Security가 제공하는 Authentication 객체로, 현재 요청에 대한 인증 정보를 포함한다.
    • 컨트롤러의 메서드에서 매개변수로 주입됩니다.
  • authentication.isAuthenticated():
    • 사용자가 인증된 상태인지 확인한다.
    • 인증되지 않은 사용자는 로그아웃을 수행할 수 없으므로 예외를 던진다.

  • 로그아웃 처리
new SecurityContextLogoutHandler().logout(request, response, authentication);
  • SecurityContextLogoutHandler:
    • Spring Security에서 제공하는 로그아웃 처리 클래스.
    • 다음 작업을 수행:
      1. 세션 무효화:
        • HTTP 세션과 연결된 모든 인증 정보를 제거.
      2. SecurityContext 초기화:
        • 현재 사용자와 관련된 SecurityContext를 초기화하여 인증 정보를 삭제.
  • 매개변수:
    • HttpServletRequest: 클라이언트의 요청.
    • HttpServletResponse: 클라이언트로 반환될 응답.
    • Authentication: 현재 인증된 사용자 정보.

  • 로그아웃 후 상태 확인
log.info("인증 객체의 삭제 확인: {}", SecurityContextHolder.getContext().getAuthentication() == null);
  • SecurityContextHolder:
    • Spring Security에서 SecurityContext를 관리하는 전역 클래스.
    • 로그아웃 후 SecurityContext가 초기화되었는지 확인.
    • 초기화가 성공하면 로그아웃이 완료된 것으로 간주.

  • 클라이언트 응답 반환
return ResponseEntity.ok(new CommonResponseBody<>("로그아웃 성공."));
  • 로그아웃 성공 시:
    • HTTP 200 상태 코드와 함께 "로그아웃 성공" 메시지를 반환.
  • 로그아웃 실패 시:
    • 인증 정보가 없는 경우 예외를 던지고 클라이언트는 401 Unauthorized 응답을 받을 수 있음.

로그아웃은 그냥 있는 것들을 잘 사용해서 SecurityContext 를 초기화하는 과정이라고
이해하면 될 것 같다!!


그래서 결국 인증, 인가는 어디서 거르는데?

지금까지는 인증 성공 후 토큰을 반환하는 것을 알아본 것이고
이제 사용자가 보호된 리소스에 요청을 수행할 때 어떻게 걸러지는지 알아봐야 할 차례이다.

거른다는 말은 좀 그러니까 있어보이게 인증, 인가 검증 이라고 할까 ?? ^-^

거의 다 왔으니 힘내보자!!!!!

JwtAuthFilter 의 등장

JwtAuthFilter 클라이언트 요청에 포함된 JWT 토큰을 검증하고,
요청이 인증된 사용자로부터 온 것인지 확인하는 필터이다.

전체적인 코드 모습을 보고 세세하게 알아보도록 하자!

@Component
@RequiredArgsConstructor
@Slf4j(topic = "Security::JwtAuthFilter")
public class JwtAuthFilter extends OncePerRequestFilter {

  /**
   * JWT 토큰 제공자.
   */
  private final JwtProvider jwtProvider;

  /**
   * UserDetailsService.
   */
  private final UserDetailsService userDetailsService;

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
    log.info("URI: {}", request.getRequestURI());
    this.authenticate(request);
    filterChain.doFilter(request, response);
  }

  private void authenticate(HttpServletRequest request) {
    log.info("인증 처리.");

    // 토큰 검증.
    String token = this.getTokenFromRequest(request);
    if (!jwtProvider.validToken(token)) {
      return;
    }

    // 토큰으로부텨 username을 추출.
    String username = this.jwtProvider.getUsername(token);

    // username에 해당되는 사용자를 찾는다.
    UserDetails userDetails = userDetailsService.loadUserByUsername(username);

    // SecurityContext에 인증 객체 저장.
    this.setAuthentication(request, userDetails);
  }

  private String getTokenFromRequest(HttpServletRequest request) {
    final String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION);
    final String headerPrefix = AuthenticationScheme.generateType(AuthenticationScheme.BEARER);

    boolean tokenFound =
        StringUtils.hasText(bearerToken) && bearerToken.startsWith(headerPrefix);
    if (tokenFound) {
      return bearerToken.substring(headerPrefix.length());
    }

    return null;
  }

  private void setAuthentication(HttpServletRequest request, UserDetails userDetails) {
    log.info("SecurityContext에 Authentication 저장.");

    // 찾아온 사용자 정보로 인증 객체를 생성.
    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
        userDetails, userDetails.getPassword(), userDetails.getAuthorities());
    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

    // SecurityContext에 인증 객체 저장.
    SecurityContextHolder.getContext().setAuthentication(authentication);
  }
}

OncePerRequestFilter??

public class JwtAuthFilter extends OncePerRequestFilter

JwtAuthFilter 는 OncePerRequestFilter 라는 필터를 상속받는다.
나는 궁금한건 못참으니 이것 먼저 알아보도록하자!!

OncePerRequestFilter 는 Spring Framework에서 제공하는 추상 클래스이다.

이 클래스는 필터(Filter)로 동작하며,
이름 그대로 하나의 요청(Request)에 대해 한 번만 실행되도록 설계된 필터를 구현할 때 사용된다.


  • OncePerRequestFilter의 역할
    • HTTP 요청(Request)이 들어올 때 한 번만 실행되도록 보장
    • 중복 실행을 방지:
      • 필터 체인을 따라 동일한 요청이 여러 번 처리되지 않도록 제어.
      • 예를 들어, 요청이 포워딩(forward)되거나, 인클루드(include) 될 경우에도
        필터가 중복 실행되지 않음.

이 클래스는 javax.servlet.Filter 인터페이스를 간단히 구현하며,
doFilterInternal 메서드를 오버라이드하여 동작을 정의한다.

기본 구조를 간단히 알아보자

public abstract class OncePerRequestFilter implements Filter {

    @Override
    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;

        // 이미 처리된 요청인지 확인 (중복 실행 방지)
        if (skipDispatch(httpRequest) || shouldNotFilter(httpRequest)) {
            filterChain.doFilter(request, response);
        }

		//생략.....
        
        // 한 번만 실행
        doFilterInternal(httpRequest, (HttpServletResponse) response, filterChain);
    }

    protected boolean shouldNotFilter(HttpServletRequest request) {
        return false; // 기본적으로 모든 요청에 대해 필터를 실행
    }

    protected abstract void doFilterInternal(
        HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException;
}

메서드 설명

  1. doFilter:

    • 필터 체인에서 호출되며, 요청 처리의 진입점.
    • 내부적으로 doFilterInternal을 호출한다.
  2. doFilterInternal:

    • 개발자가 실제 필터 로직을 구현해야 하는 메서드.
    • 각 요청에 대해 한 번 실행되도록 보장된다.
  3. shouldNotFilter:

    • 특정 요청에 대해 필터를 건너뛸 수 있도록 조건을 정의하는 메서드.
    • 기본값은 false(모든 요청에 대해 필터 실행).

장점

  1. 중복 실행 방지:

    • 동일한 요청이 여러 번 필터링되지 않도록 보장.
    • 특히 요청이 포워딩(forward)되거나, 인클루드(include) 되는 경우 유용.
      포워딩(Forward): 요청을 다른 리소스로 완전히 넘겨주는 방식.
      인클루드(Include): 요청을 다른 리소스로 전달하여 일부 결과를 응답에 포함시키는 방식.
  2. 필터 로직 캡슐화:

    • doFilterInternal 메서드에 핵심 로직만 작성.
    • doFilter와 같은 복잡한 처리는 내부적으로 관리.

요약

OncePerRequestFilter는 Spring에서 제공하는 필터로,
한 요청당 한 번만 실행되는 필터를 구현할 때 사용된다.

주로 Spring Security에서 JWT 인증, 세션 검증 등의 작업에 사용된다.

개발자는 doFilterInternal 메서드를 오버라이드하여 필터 로직을 구현한다.


궁금증이 해결되었으니 다음으로 넘어가자!


  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
    log.info("URI: {}", request.getRequestURI());
    this.authenticate(request);
    filterChain.doFilter(request, response);
  }
  
  private void authenticate(HttpServletRequest request) {
    ...
  }

  
  private String getTokenFromRequest(HttpServletRequest request) {
    ...
  }

  
  private void setAuthentication(HttpServletRequest request, UserDetails userDetails) {
    ...
  }

doFilterInternal 내부에서 authenticate 를 호출한다.
이 때 매개변수는 HttpServletRequest 를 넣어준다.

 private void authenticate(HttpServletRequest request) {

    // 토큰 검증.
    String token = this.getTokenFromRequest(request);
    if (!jwtProvider.validToken(token)) {
      return;
    }
	//....
  }

authenticate 메서드는 request 를 사용하여 우선 토큰 값을 추출하는
getTokenFromRequest 메서드를 호출한다.

private String getTokenFromRequest(HttpServletRequest request) {
    final String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION);
    final String headerPrefix = AuthenticationScheme.generateType(AuthenticationScheme.BEARER);

    boolean tokenFound =
        StringUtils.hasText(bearerToken) && bearerToken.startsWith(headerPrefix);
    if (tokenFound) {
      return bearerToken.substring(headerPrefix.length());
    }

    return null;
  }

getTokenFromRequest 메서드의 작업을 살펴보자.

  1. HTTP 요청의 Authorization 헤더에서 값을 가져온다
  2. 헤더 값이 존재하고, "Bearer"로 시작하면, "Bearer" 부분을 제거하여 JWT 토큰만 반환한다.
  3. 토큰이 없거나 잘못된 경우 null 반환.

이제 이렇게 반환받은 토큰값으로 어떤 작업을 마저 진행하는지 살펴보자.

private void authenticate(HttpServletRequest request) {
    log.info("인증 처리.");

    // 토큰 검증.
    String token = this.getTokenFromRequest(request);
    if (!jwtProvider.validToken(token)) {
      return;
    }

    // 토큰으로부텨 username을 추출.
    String username = this.jwtProvider.getUsername(token);

    // username에 해당되는 사용자를 찾는다.
    UserDetails userDetails = userDetailsService.loadUserByUsername(username);

    // SecurityContext에 인증 객체 저장.
    this.setAuthentication(request, userDetails);
  }
  1. 반환받은 String token 값을 jwtProvider를 통해 검증

  2. 토큰으로부터 username 추출,
    UserDetailsService 의 loadUserByUsername 메서드를 통해 해당 사용자의
    UserDetails 를 생성하고 반환받는다.

  3. SecurityContext 에 해당 객체를 저장하는 setAuthentication 메서드를 호출한다.


private void setAuthentication(HttpServletRequest request, UserDetails userDetails) {
    log.info("SecurityContext에 Authentication 저장.");

    // 찾아온 사용자 정보로 인증 객체를 생성.
    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
        userDetails, userDetails.getPassword(), userDetails.getAuthorities());
    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

    // SecurityContext에 인증 객체 저장.
    SecurityContextHolder.getContext().setAuthentication(authentication);
  }

이 메서드가 하는 작업이 어디서 본 것처럼 익숙하지 않은가???
UsernamePasswordAuthenticationToken 에 대해서 알아볼 때
Spring Security 가 인증이 성공하면 해당 객체를 인증된 상태로 변환하고
사용자 정보, 권한 정보를 담아서 SecurityContet 에 저장한다고 했었다!

이걸 JwtAuthFilter 에서 구현하여 완전한 Authentication 객체로 변환해주는 것이었다.

필터에서 인증이 성공하면 완전한 Authentication 객체로 SecurityContext 에 저장되는구나

결국 위에서 배웠던 것들이 하나로 이어지고있는 느낌이야~


그럼 이제 이 필터를 등록해야겠지?? WebConfig 출격~

WebConfig 에서 이제 이 필터를 등록하면 사용되는 것이야~~ 출격이요 뿌뿌~~

그전에 잠깐!!!!

Spring Security에서 인증(Authentication) 및 인가(Authorization) 과정에서
발생하는 예외 상황을 처리하기 위한 구성 요소들을 알아보자.

DelegatedAuthenticationEntryPoint.java

@Component
public class DelegatedAuthenticationEntryPoint implements AuthenticationEntryPoint {

  /**
   * Spring Security 예외를 처리하기 위한 resolver.
   */
  private final HandlerExceptionResolver resolver;

  /**
   * 생성자.
   */
  public DelegatedAuthenticationEntryPoint(
      @Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
    this.resolver = resolver;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException authException) {
    resolver.resolveException(request, response, null, authException);
  }
}

Spring Security의 AuthenticationEntryPoint 인터페이스를 구현하여
인증 실패 시 예외를 처리하는 구현체이다.

이는 인증되지 않은 사용자가 보호된 리소스에 접근하려고 할 때 실행되는 엔트리 포인트이다.

  • 동작 방식:
  1. commence 메서드 호출:

    인증되지 않은 사용자가 접근하려고 하면 Spring Security가 이 메서드를 호출한다.
    예외(AuthenticationException)를 처리하여 적절한 응답을 반환.

  2. HandlerExceptionResolver를 사용한 예외 처리:

    내부적으로 Spring MVC의 HandlerExceptionResolver를 호출하여 예외를 처리.
    클라이언트에게 401 Unauthorized 상태 코드를 반환하거나 커스텀 메시지를 제공한다.

DelegatedAccessDeniedHandler.java

@Component
public class DelegatedAuthenticationEntryPoint implements AuthenticationEntryPoint {

  /**
   * Spring Security 예외를 처리하기 위한 resolver.
   */
  private final HandlerExceptionResolver resolver;

  /**
   * 생성자.
   */
  public DelegatedAuthenticationEntryPoint(
      @Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
    this.resolver = resolver;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException authException) {
    resolver.resolveException(request, response, null, authException);
  }
}

Spring Security의 AccessDeniedHandler 인터페이스를 구현하여 인가 실패 시 예외를 처리

사용자가 권한이 없어서 리소스에 접근하지 못할 때 실행되는 핸들러이다.

  • 동작 방식:
  1. handle 메서드 호출:

    Spring Security가 사용자의 권한(Role)을 확인한 후, 권한 부족 시 이 메서드를 호출한다
    예외(AccessDeniedException)를 처리하여 적절한 응답을 반환.

  2. HandlerExceptionResolver를 사용한 예외 처리:

    내부적으로 Spring MVC의 HandlerExceptionResolver를 호출하여 예외를 처리.
    클라이언트에게 403 Forbidden 상태 코드를 반환하거나 커스텀 메시지를 제공한다.


왜 하나는 EntryPoint? 하나는 Handler?

핸들러(Handler)엔트리포인트(EntryPoint) 의 역할이 비슷해 보이는 데다가
HandlerExceptionResolver 을 사용하여 예외를 처리하는 것도 똑같은데
왜 명명을 굳이 다르게 한 것인지 의문이 들었다.

1. EntryPoint: 인증 실패 처리

  • 언제 동작하는가?
    인증(Authentication)이 필요한 상황인데,
    사용자가 인증되지 않은 상태에서 보호된 리소스에 접근하려고 할 때 동작한다.

  • 목적

    • 사용자가 아예 인증되지 않았기 때문에 인증 절차로 안내하는 것이 목적
    • 예를 들어, 로그인 페이지로 리다이렉트하거나,
      "로그인이 필요합니다(401 Unauthorized)"라는 메시지를 반환해야한다.
  • 예시

    • 로그인을 안 한 사용자가 /user/profile에 접근하면:
      • Spring Security가 인증 정보가 없다고 판단.
      • DelegatedAuthenticationEntryPoint가 호출되어 "401 Unauthorized" 응답을 반환.
  • 흐름

    1. 요청이 들어옴.
    2. Spring Security가 SecurityContext에서 인증 정보를 확인.
    3. 인증 정보가 없으면 EntryPoint 호출 → 인증 절차로 안내.

2. Handler: 인가 실패 처리

  • 언제 동작하는가?
    사용자가 인증은 되었지만, 요청한 리소스에 접근할 권한(Authorization)이 없을 때 동작

  • 목적

    • 사용자가 인증은 완료했지만, 권한이 부족하기 때문에 접근을 차단하는 것이 목적
    • 예를 들어, "403 Forbidden" 상태를 반환하거나, 권한 부족 메시지를 전달
  • 예시

    • 일반 사용자가 /admin에 접근하면:
      • Spring Security가 인증 정보는 유효하다고 판단.
      • 그러나, 해당 리소스에 필요한 권한이 부족.
      • DelegatedAccessDeniedHandler가 호출되어 "403 Forbidden" 응답을 반환.
  • 흐름

    1. 요청이 들어옴.
    2. Spring Security가 인증 정보 확인 → 인증 정보는 유효함.
    3. 리소스에 필요한 권한과 비교.
    4. 권한이 부족하면 Handler 호출 → "403 Forbidden" 응답 반환.

따라서?

  • EntryPoint:

    • "출입구"를 의미
    • 사용자가 인증되지 않은 상태에서 출입구에 도달했을 때,
      "여기 들어오려면 먼저 인증하세요!"라고 안내하는 역할
    • 즉, 인증(Authentication) 과정으로 사용자를 유도
  • Handler:

    • "처리자"를 의미
    • 사용자가 출입구는 통과했지만,
      내부 구역에 들어가려는 시도 중 권한이 부족한 경우 이를 처리하는 역할
    • 즉, 인가(Authorization) 실패를 처리

정리

특징EntryPointHandler
목적인증되지 않은 사용자 처리권한이 없는 사용자 처리
동작 시점인증(Authentication) 실패 시점인가(Authorization) 실패 시점
반환 HTTP 상태 코드401 Unauthorized403 Forbidden
비유"신분증 없으면 못 들어가요!""VIP만 들어갈 수 있어요!"

진짜 WebConfig 살펴보기

이제 다 알아보았으니 진짜 WebConfig 를 살펴보자.

@Configuration
@EnableWebSecurity // SecurityFilterChain 빈 설정을 위해 필요.
@RequiredArgsConstructor
public class WebConfig {
  
  private final JwtAuthFilter jwtAuthFilter;
  
  private final AuthenticationProvider authenticationProvider;
  
  private final AuthenticationEntryPoint authEntryPoint;
  
  private final AccessDeniedHandler accessDeniedHandler;
  
  private static final String[] WHITE_LIST = {"/accounts/login", "/accounts/join", "/favicon.ico",
      "/error"};

 
  @Bean
  SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    ...
  }

  @Bean
  public RoleHierarchy roleHierarchy() {
    ...
  }

방금 설명했던 EntryPoint 와 DeniedHandler 가 @Component 로 등록되어있기 때문에
그 둘을 주입받는 모습을 볼 수 있다.

그 외에도 Filter 로 적용할 JwtAuthFilter,
사용자 정보를 가져올 때 사용할 AuthenticationProvider 도 존재하는 모습이다.

또한 인증, 인가가 필요하지 않은 WHITE_LIST 도 따로 준비된 모습이다.

@EnableWebSecurity 어노테이션은 Spring Security를 활성화하고,
애플리케이션의 보안 설정을 정의하는 SecurityFilterChain Bean을 설정하기 위해 필요하다.

이 어노테이션은 Spring Boot 애플리케이션에서 Spring Security가 동작하도록 설정하는 중요한 역할을 하기 때문에 꼭 필요하다!

Spring Security는 모든 요청이 인증 필요로 설정된 기본 보안 설정을 제공한다.
@EnableWebSecurity를 사용하면, 기본 보안 설정을 사용자가 정의한 설정으로 대체할 수 있다.

이제 아래 두개의 메서드를 각각 알아보도록하자!!


SecurityFilterChain

  @Bean
  SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.cors(AbstractHttpConfigurer::disable)
        .csrf(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(auth ->
            auth.requestMatchers(WHITE_LIST).permitAll()
                // static 리소스 경로
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                // 일부 dispatch 타입
                .dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.INCLUDE,
                    DispatcherType.ERROR).permitAll()
                // path 별로 접근이 가능한 권한 설정
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/staff/**").hasRole("STAFF")
                .requestMatchers("/user/**").hasRole("USER")
                // 나머지는 인증이 필요
                .anyRequest().authenticated()
        )
        // Spring Security 예외에 대한 처리를 핸들러에 위임.
        .exceptionHandling(handler -> handler
            .authenticationEntryPoint(authEntryPoint)
            .accessDeniedHandler(accessDeniedHandler))
        // JWT 기반 테스트를 위해 SecurityContext를 가져올 때 HttpSession을 사용하지 않도록 설정.
        .sessionManagement(
            session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authenticationProvider(authenticationProvider)
        /*
         * Spring Security와 관련된 예외(AuthenticationException, AccessDeniedException)는
         * ExceptionTranslationFilter에서 처리가 된다.
         *
         * ExceptionTranslationFilter의 doFilter()는 이후의 필터 체인에서 예외가 발생하면 그 예외를 처리하도록 작성되어 있다.
         * request를 넘겨 JwtAuthFilter에서 발생한 예외를 처리시키기 위해 ExceptionTranslationFilter 다음에 수행하도록 순서를 설정.
         */
        .addFilterAfter(jwtAuthFilter, ExceptionTranslationFilter.class);

    return http.build();
  }

SecurityFilterChain은 Spring Security의 핵심 구성 요소로,
HTTP 요청을 처리하는 필터들의 체인을 정의하는 역할을 수행한다.

securityFilterChain 메서드는 Spring Security의 보안 설정을 정의한 메서드다.
이 메서드는 HttpSecurity 객체를 사용하여 애플리케이션의 보안 정책(인증, 인가, 필터 설정 등)을 구성하고, 이를 기반으로 SecurityFilterChain 객체를 생성하여 반환한다.


1. 주요 역할

  • 보안 정책 정의:

    • 요청별 접근 권한 설정.

    • 인증 및 인가 실패 시 처리 핸들러 설정.

    • JWT 기반 인증을 위한 상태 관리 정책 정의.

  • 필터 체인 구성:

    • 요청이 처리될 때 어떤 필터들이 동작할지 정의.

    • JwtAuthFilter를 필터 체인에 추가.


2. 코드 분석

  • (1) CORS 및 CSRF 비활성화
http.cors(AbstractHttpConfigurer::disable)
    .csrf(AbstractHttpConfigurer::disable);
  • CORS (Cross-Origin Resource Sharing):

    • 기본적으로 Spring Security는 CORS를 활성화하지 않는다.
    • 여기서는 CORS 정책을 명시적으로 비활성화 하고 있다.
  • CSRF (Cross-Site Request Forgery):

    • CSRF 보호는 주로 세션 기반 인증에 사용된다.
    • JWT 인증은 Stateless 환경에서 동작하므로, CSRF 보호를 비활성화.

(2) 요청별 접근 권한 설정

.authorizeHttpRequests(auth -> auth
    .requestMatchers(WHITE_LIST).permitAll()
    .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
    .dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ERROR).permitAll()
    .requestMatchers("/admin/**").hasRole("ADMIN")
    .requestMatchers("/staff/**").hasRole("STAFF")
    .requestMatchers("/user/**").hasRole("USER")
    .anyRequest().authenticated());
  • WHITE_LIST:

    • 로그인(/accounts/login), 회원가입(/accounts/join) 등 특정 경로를
      모든 사용자가 접근 가능하도록 설정.
  • 정적 리소스 경로:

    • CSS, JS, 이미지 같은 정적 파일들은 인증 없이 접근 가능.
  • Dispatcher Type 허용:

    • 포워드(Forward), 인클루드(Include), 에러(Error) 처리는 인증 없이 허용.
  • 경로별 권한 설정:

    • /admin/**: 관리자(ADMIN)만 접근 가능.
    • /staff/**: 직원(STAFF)만 접근 가능.
    • /user/**: 일반 사용자(USER)만 접근 가능.
  • 기본 정책:

    • 위 조건에 해당하지 않는 모든 요청은 인증 필요(authenticated())

(3) 예외 처리 핸들러 설정

.exceptionHandling(handler -> handler
    .authenticationEntryPoint(authEntryPoint)
    .accessDeniedHandler(accessDeniedHandler));
  • authenticationEntryPoint:

    • 인증되지 않은 사용자가 보호된 리소스에 접근하려고 할 때 호출된다.
    • 주로 401 Unauthorized 상태 코드 반환.
  • accessDeniedHandler:

    • 인증은 되었지만 권한이 부족한 사용자가 리소스에 접근하려고 할 때 호출된다.
    • 주로 403 Forbidden 상태 코드 반환.

(4) 세션 관리 정책 설정

.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
  • SessionCreationPolicy.STATELESS:
    • Spring Security가 세션을 사용하지 않도록 설정.
    • 모든 요청은 JWT를 통해 인증되며, Stateless 환경에서 동작.

(5) 인증 공급자 등록

.authenticationProvider(authenticationProvider);
  • authenticationProvider:
    • Spring Security가 인증 요청을 처리할 때 사용하는 인증 공급자(예: DaoAuthenticationProvider).
    • 사용자 정보 로드(UserDetailsService) 및 비밀번호 검증(PasswordEncoder)을 처리.

(6) JWT 필터 추가

.addFilterAfter(jwtAuthFilter, ExceptionTranslationFilter.class);
  • JwtAuthFilter:

    • 요청에서 JWT를 추출 및 검증하여 인증 상태를 설정하는 필터.
    • 필터 체인에서 ExceptionTranslationFilter 이후에 실행되도록 설정.
  • ExceptionTranslationFilter 이후에 추가하는가?

    • ExceptionTranslationFilter는 인증 및 인가 예외를 처리하는 필터.
    • JwtAuthFilter에서 발생한 예외를 처리하도록 흐름을 설정.

4. 요약

  • 이 메서드는 Spring Security의 보안 설정을 정의한다.

  • 요청별 권한 설정, JWT 인증 필터 추가, Stateless 환경 설정, 예외 처리 핸들러 등록 등을 통해 애플리케이션의 보안을 강화한다.

  • 핵심 설정:

    • JWT 기반 인증: JwtAuthFilter를 통해 인증 처리.
    • Stateless 환경: 세션을 사용하지 않고, 요청마다 JWT를 통해 인증.
    • 경로별 권한 정책: 특정 경로는 인증 없이 접근 가능, 나머지는 인증 필요.

SecurityFilterChain 을 다 알아봤따!!!!

RoleHieracy는 나중에 필요할 때 다시 공부해봐야겠다. 그리 어렵지도 않은 듯?

이제 이 정리 글을 여러번 읽어보면서 흐름을 더 명확하게 이해하고 구현을 시작해야겠따

진짜 끗

profile
개발일기

0개의 댓글

관련 채용 정보