본문 바로가기
Java , Spring/Spring

[Spring] 스프링 시큐리티 정리

by 방배킹 2024. 2. 14.

스프링 시큐리티란?

Spring Security는 Spring 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크이다.

Spring Security는 '인증'과 '인가'에 대한 부분을 Filter 에서 처리한다.

 

(HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러)

 

Spring Security는 Spring 생태계에서 보안에 필요한 기능들을 쉽게 사용하도로 도와준다.

 

Spring Security  아키텍처

 

앞서 말했듯이 Spring Security는 필터를 이용한다.

 

 

간단한 동작과정은 아래와 같다.

 

1. 사용자의 요청이 서버로 들어온다.
2. Authotication Filter가 요청을 가로채고 Authotication Manger로 요청을 위임한다.
3. Authotication Manager는 등록된 Authotication Provider를 조회하며 인증을 요구한다. 
4. Authotication Provider가 실제 데이터를 조회하여 UserDetails 결과를 돌려준다.
5. 결과는 SecurityContextHolder에 저장이 되어 저장된 유저정보를 Spring Controller에서 사용할 수 있게 된다,

Spring Security가 작동하는 내부 구조

 

1. 사용자가 자격 증명 정보를 제출하면, AbstractAuthenticationProcessingFilter가 Authentication 객체를 생성한다.

2. Authentication 객체가 AuthenticationManager에게 전달된다.

 

3-1. 인증에 실패하면, 로그인 된 유저정보가 저장된 SecurityContextHolder의 값이 지워지고

RememberMeService.joinFail()이 실행된다. 그리고 AuthenticationFailureHandler가 실행된다.


3-2. 인증에 성공하면, SessionAuthenticationStrategy가 새로운 로그인이 되었음을 알리고,

Authentication 이 SecurityContextHolder에 저장된다.

이후에 SecurityContextPersistenceFilter가 SecurityContext를 HttpSession에 저장하면서 로그인 세션 정보가 저장된다.

이후 RememberMeServices.loginSuccess()가 실행되고, ApplicationEventPublisher가 InteractiveAuthenticationSuccessEvent를 발생시키고 AuthenticationSuccessHandler 가 실행된다.

loginProcessingUrl를 통한 로그인 구현

Spring Security에서 loginProcessingUrl를 통해 로그인을 구현할 수 있다.

.loginProcessingUrl("/loginSuccess")

해당 url로 로그인 요청을 보내면 스프링 시큐리티에서 자동으로 로그인을 처리해준다.

 

스프링은 어떻게 데이터베이스에 있는 로그인 정보를 확인해서 로그인을 처리할까?

여기에 사용되는것이 UserDetails와 UserDetailsService 인터페이스 이다.

 

loginProcessingUrl의 세부 과정은 아래와 같다.

 

1. loginProcessingUrl에 넣어준 url로 로그인 요청을 보낸다.

<form action="/loginSuccess" method="post" name="loginForm">
    <input id="username" type="text" name="username" placeholder="id"/>
    <input id="password" type="password" name="password" placeholder="password"/>
    <input type="hidden" name="_csrf" th:value="${_csrf.token}">
    <input type="submit" value="login"/>
</form>


2. UserDetailsService 타입으로 IoC 되어있는 객체를 찾는다.
3. 해당 객체 안에 구현된 loadUserByUsername 함수를 실행하고 UserDetails 객체 리턴한다.

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    UserEntity userData = userRepository.findByUsername(username);
    // 아이디가 존재
    if(userData != null){
        return new CustomUserDetails(userData);
    }
    return null;
}


4. Spring Security가 받은 UserDetails 객체를 이용해 Authentication 객체를 만든다.
5. Authentication 객체를 이용해 Security Session을 만든다.

6. Security ContextHolder에 세션을 저장한다.

 

  • 여기서  중요한 것은 저장하려는 세션이 Security Session이라는 것이다.
  • Security Session은 Authentication 객체를 필요로 하고, Authentication 객체는 UserDetails가 필요하다.
  • 그래서 이런 로그인을 구현하기 위해 UserDetails를 구현해서 사용해야한다.

 

UserDetails와 UserDetailsService

그러면 UserDetails와 UserDetailsService에 대해 알아보자

 

UserDetails란 Spring Security에서 사용자의 정보를 담는 인터페이스이고

UserDetailsService는 Spring Security에서 유저의 정보를 가져오는 인터페이스이다.

 

UserDetails와 UserDetailsService는 인터페이스로 사용하기 위해서는 우리가 직접 구현을 해야한다.

 

구현해야하는 UserDetails 메소드

 

구현해야하는 UserDetailsService 메소드

 

UserDetails를 구현한 CustomUserDetails

@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {

    private final UserEntity userEntity;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        Collection<GrantedAuthority> collection = new ArrayList<>();

        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return userEntity.getRole();
            }
        });

        return collection;
    }

    @Override
    public String getPassword() {
        return userEntity.getPassword();
    }

    @Override
    public String getUsername() {
        return userEntity.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

 

UserDetailsService를 구현한 CustomUserDetailsService

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userData = userRepository.findByUsername(username);
        // 아이디가 존재
        if(userData != null){
            return new CustomUserDetails(userData);
        }
        return null;
    }
}

 

@EnableWebSecurity

Spring Security 실습을 할때 SecurityConfig 클래스에 @EnableWebSecurity를 추가해줬는데 해당 어노테이션이 어떤역할을 하는지, 그리고 왜 사용하는지 궁금해서 찾아보게되었다.

 

@EnableWebSecurity는 web security 구성정보를 활성화하기 위해 사용된다

@EnableWebSecurity 어노테이션을 등록하지 않으면 Spring Security가 기본적으로 설정해둔 디폴트 인증/인가 및 기타 작업들이 동작하게된다.

해당 어노테이션을 사용하면 우리가 커스텀한 bean들을 찾아서 등록해준다.

 

따라서 커스텀한 내용을 적용시키려면 @EnableWebSecurity 어노테이션을 추가해줘야한다.

 

CSRF

CSRF(Cross-Site Request Forgery)는 웹 애플리케이션 취약점 중 하나로, 인증된 사용자가 악의적인 웹사이트를 방문하여 그 사이트가 이 사용자의 권한으로 인증되어 있는 웹 애플리케이션에 악의적인 요청을 보내는 공격을 말합니다. 이것은 사용자의 동의 없이 악의적인 행동을 수행하게 하는데 사용될 수 있습니다.

 

즉, 웹사이트에 로그인한 상태로 악의적인 웹사이트에 방문했을때, 악의적인 웹사이트에서 사용자의 세션 정보를 바탕으로 악의적인 요청을 보내는것이다.

 

이를 방지하기 위해 서버는 CSRF 토큰을 만들 클라이언트에게 전달하고 클라이언트는 서버에 요청을 보낼때 해당 토큰을 포함해야한다.

댓글