본문 바로가기
IT/Spring boot

[Spring boot] Spring Security 구조 이해

by kyu-nahc 2024. 9. 4.
반응형

 

Spring Security

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

보안 옵션을 많이 제공해 주고 복잡한 로직 없이도 어노테이션으로도 설정이 가능하며,

여러 보안 위협 방어 및 요청 헤더도 보안 처리를 해준다.

기본적으로 스프링 시큐리티는 세션 기반 인증을 제공한다.

세션, 토큰 기반의 인증과 인가에 대한 내용은 아래 포스팅을 참고하면 된다.

 

[Server] 토큰 기반 인증 & 세션 기반 인증

Authentication (인증) /  Authorization (인가) 세션 기반 인가와 토큰 기반 인가에 대해 알아보기 이전에 먼저, 인증과 인가가 무엇인지부터 알아야 할 필요가 있다. 인증과 인가를 같거나 비슷한 개념

kyu-nahc.tistory.com

 

기본적인 인증 방식은 크게 3개 정도로 나눌 수 있다.

 

인증 방식

  1. credential 방식
    username, password를 이용하는 방식
  2. 이중 인증
    사용자가 입력한 개인 정보를 인증 후,
    다른 인증 체계(예: 물리적인 카드)를 이용하여 두 가지의 조합으로 인증하는 방식
  3. 하드웨어 인증
    자동차 키와 같은 방식

3개의 인증방식 중 Spring Security는 credential 기반의 인증을 취한다.

  • principal - 아이디 (username)
  • credential - 비밀번호 (password)

특정 자원에 대한 접근을 제어하기 위해서는 권한을 가지게 된다.

특정 권한을 얻기 위해서는 유저는 인증정보(Authentication)가 필요하고

관리자는 해당 정보를 참고해 권한을 인가(Authorization)한다.

보편적으로 username - password 패턴의 인증방식을 거치기 때문에 

Spring Security는 principal - credential 패턴을 가지게 된다.

 

Spring Security는 Filter를 기반으로 동작하며,  MVC와 분리되어 관리하고 동작할 수 있다.

또한 Spring Security 3.2부터는 XML 설정 없이 Bean으로 설정하여 사용할 수 있다.

Spring Security의 수행 절차를 살펴보면 다음과 같다.

 

Spring Security의 흐름을 따라가 보면 다음과 같다.

  1. 사용자가 Form에 아이디, 패스워드를 입력하면
     HTTPServletRequest에 아이디, 비밀번호 정보가 전달된다.
    이때 AuthenticationFilter가 요청을 가로채고
    파라미터로 넘어온 아이디와 비밀번호의 유효성 검사를 실시한다.
  2. 유효성 검사 후 실제 구현체인
    UsernamePasswordAuthenticationToken을 만들어 넘겨준다.
    이는 인증용 객체이다.
  3. 인증용 객체인 UsernamePasswordAuthenticationToken을 
    AuthenticationManager에게 전달한다.
  4. UsernamePasswordAuthenticationToken을 
    AuthenticationProvider에게 전달한다.
  5. 사용자 아이디를 UserDetailsService로 보낸다.
    UserDetailService는 사용자 아이디로 찾은 사용자의 정보를 
    UserDetails 객체로 만들어 AuthenticationProvider에게 다시 전달하는 역할을 한다.
  6. 데이터베이스에 있는 사용자 정보 ( Password, Roles...)를 가져오고,
    데이터베이스에서 찾아낸 사용자 정보인 UserDetails 객체를 만든다.
    이 역할이 UserDetailsService이고 5번에서 설명한 역할을 진행하는 것이다.
  7. AuthenticaitonProvider에서 UserDetails를 넘겨받고 사용자 정보를 비교한다.
    입력 정보와 UserDetails의 정보를 비교실제 인증 처리를 진행하는 것이다.
  8. ~ 10까지 인증이 완료되면 SecurityContextHolderAuthentication을 저장한다.
    인증 성공 여부에 따라 성공 시 AuthenticationSuccessHandler,
    실패 시 AuthenticationFailureHandler 핸들러를 실행한다.

 

SpringContextHolder / SecurityContext / Authentication

세 가지 클래스는 Spring Security의 주요 컴포넌트로,

각 컴포넌트의 관계를 간단히 표현하자면 다음과 같다.

유저의 아이디와 패스워드 사용자 정보를 넣고 실제 가입된 사용자인지 체크한 후

인증에 성공하면 우리는 사용자의 principal과 credential 정보를 Authentication안에 담는다.

그리고 principal과 credential 정보를 담는 것이 UserDetails이다.

Spring Security에서는 방금 담은 Authentication을 SecurityContext에 보관한다.

이 SecurityContext를 SecurityContextHolder에 담아 보관하게 되는 것이다.

즉 구조를 정리하면 SecurityContextHolder(SecurityContext(Authentication(UserDetails))))

이렇게 내부적으로 구성되어 있고, 아래 그림과 같다.

 

Authentication는 현재 접근하는 주체의 정보와 권한을 담는 인터페이스고

SecurityContext 저장되며 SecurityContextHolder를 통해 SecurityContext에 접근하고,

SecurityContext를 통해 Authentication에 접근할 수 있다.

그리고 Authentication 객체를 통해 UserDetails 객체에 접근이 가능하다.

UserDetails 객체가 사용자 정보들을 담고 있는 최종 Class라고 생각하면 된다.

public interface Authentication extends Principal, Serializable {

	Collection<? extends GrantedAuthority> getAuthorities();
    
	Object getCredentials();
    
	Object getDetails();
 
	Object getPrincipal();
 
	boolean isAuthenticated();
    
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
 
}

 

우리가 로그인한 사용자의 정보를 얻기 위해서는

SecurityContextHolder.getContext().getAuthentication().getPrincipal();

이 구문을 사용하여 가져오는데 내부적으로 연결된 구조 때문에 다음과 같은 구문을 사용하는 것이다.

해당 구문을 통해 최종적으로 SecurityContextHolder에 저장되어 있는

로그인한 사용자의 정보에 대해 접근가능하고,

해당 구문으로 인해 반환받는 객체는 UserDetails 객체인 것이다.

 

UsernamePasswordAuthenticationToken

이 클래스는 Autentication을 구현한 AbstractAuthenticationToken의 하위의 하위클래스로,

유저의 ID가 Principal의 역할을 하고 유저의 Password가 Credential의 역할을 한다.

UserPasswordAuthenticationToken의 첫 번째 생성자는

인증 전에 객체를 생성하고, 두 번째는 인증이 완료된 객체를 생성한다.


public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = 620L;
    private final Object principal;
    private Object credentials;

    // 인증 전의 객체를 생성하는 생성자
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }

    // 인증 후의 객체를 생성하는 생성자
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    ...
}

 

AuthenticationManager

 

인증에 대한 부분은 이 클래스를 통해서 처리가 된다.

실질적으로는 AuthenticationManager에 등록된 AuthenticationProvider에 의해서 처리가 된다.

AuthenticationManager는 인증 관리자 역할을 하게 된다.

public interface AuthenticationManager {

    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

 

AuthenticationProvider

 

이 클래스는 실제 인증에 대한 부분을 처리하는 작업을 치른다.

인증 논리를 구현하는 인증 공급자 역할을 한다.

인증 전에 Authentication 객체를 받아 인증이 완료된 객체를 반환하는 역할을 하고,

아래와 같은 인터페이스를 구현해

Custom 한 AuthenticationProvider를 작성하고 AuthenticationManger에 등록할 수 있다.

Spring Security에서는 해당 인터페이스를 구현하는 여러 클래스가 존재하고,

Spring Security의 구현체 Class를 이용해도 되고, Custom 하여 사용도 가능하다.

public interface AuthenticationProvider {

    Authentication authenticate(Authentication authentication) throws AuthenticationException;

    boolean supports(Class<?> authentication);
}

 

ProviderManager

 

AuthenticationManager를 구현한

ProviderManager은 AuthenticationProvider를 구성하는 목록을 갖는다.

위의 4)번에서 설명한 AuthenticationProvider의 구현체들을 목록으로 가지는 것이다.

갖고 있는 Provider들이 while() 문을 돌면서 해당 인증을 처리할 수 있는지 찾고,

인증이 가능한 Provider가 있다면 인증을 위임하고, 없으면 예외를 발생시킨다.

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
	
	public List<AuthenticationProvider> getProviders() {
        return this.providers;
    }
    
   	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
       
        Iterator var9 = this.getProviders().iterator();
        ...
        
        // 등록된 모든 Provider를 순회
        while(var9.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var9.next();
            ...
            throw lastException;
        }
    }
}

 

UserDetailsService

 

AuthenticationProvider가 인증 공급자 역할을 하기 위해서 

사용자 세부 정보 서비스인 UserDetailsService를 이용하는 것이다.

해당 서비스를 이용하여 사용자를 찾고 암호 인코더로 암호를 검증하는 절차를 거친다.

UserDetailsService는 DaoAuthenticationProvider와 협력하는 인터페이스이다.

이는 ProviderManager의 목록 중 하나인 Provider이다.

 

DaoAuthenticationProvider는 요청받은 유저의 ID, Password와

데이터베이스에 저장된 ID, Password의 검증하는 책임을 가지고 있다.

따라서 저장된 ID, Password를 갖고 오기 위하여 UserDetailsService와 협력하는 구조를 가진다.

DaoAuthenticationProvider에서는 돌려받은 UserDetails 객체를 가지고,

최종적으로 UsernamePasswordAuthentication 객체를 만들어

ProviderManager에게 돌려주는 역할을 가진다.

DaoAuthenticationProvider의 retrieveUser 함수에 의해

인보크 되어 실제 저장된 사용자 정보를 가지고 검증을 하게 된다.

 

따라서 UserDetailsService에서 파라미터로 전달된 아이디를 이용하여

해당 사용자를 데이터베이스에서 찾고 UserDetails 객체를 생성하여 반환만 해주면 

DaoAuthenticationProvider에서 비밀번호에 대한 검증이 추가로 이루어지는 것이다.

따라서 Spring Security를 이용한다면 비밀번호에 대한 추가적인 검증 로직을 작성할 필요는 없다.

이 클래스는 UserDetails 객체를 반환하는 하나의 메서드만을 가지고 있는데,

일반적으로 이를 구현한 클래스에서 UserRepository를 주입받아 DB와 연결하여 처리한다.

DaoAuthenticationProvider의 코드 일부분을 보면 다음과 같다.

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider{

    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
        this.passwordEncoder = passwordEncoder;
        this.userNotFoundEncodedPassword = null;
    }

    protected PasswordEncoder getPasswordEncoder() {
        return this.passwordEncoder;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    protected UserDetailsService getUserDetailsService() {
        return this.userDetailsService;
    }

    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            UsernameNotFoundException ex = var4;
            this.mitigateAgainstTimingAttack(authentication);
            throw ex;
        } catch (InternalAuthenticationServiceException var5) {
            InternalAuthenticationServiceException ex = var5;
            throw ex;
        } catch (Exception var6) {
            Exception ex = var6;
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }
    
}

 

아래 코드가 UserDetailsService Interface이고

이를 구현한 구현체를 통해 MemberRepository를 주입받아서

데이터베이스와 연결하여 처리하는 예제는 다음과 같다.

public interface UserDetailsService {
 
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
 
}
@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        Optional<Member> memberEntity = memberRepository.findByUsername(username);
        return memberEntity.map(PrincipalDetails::new).orElse(null);
    }
}

 

 

UserDetails

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

인증에 성공하여 생성된 UserDetails 클래스는 Authentication 객체를 구현한

UsernamePasswordAuthenticationToken을 생성하기 위해 사용된다.

public interface UserDetails extends Serializable {
 
	Collection<? extends GrantedAuthority> getAuthorities();
 
	String getPassword();
 
	String getUsername();
 
	boolean isAccountNonExpired();
 
	boolean isAccountNonLocked();
 
	boolean isCredentialsNonExpired();
 
	boolean isEnabled();
 
}

 

SecurityContextHolder

SecurityContextHolder는 보안 주체의 세부 정보를 포함하여

응용 프로그램의 현재 보안 컨텍스트에 대한 세부 정보가 저장된다.

 

SecurityContext

Authentication을 보관하는 역할을 하며,

SecurityContext를 통해 Authentication을 저장하거나 꺼내온다.

 

GrantedAuthority

GrantedAuthority는 현재 사용자(Pricipal)가 가지고 있는 권한을 의미하며

ROLE_MANAGER, ROLE_ADMIN, ROLE_USER와 같이 ROLE_* 형태로 사용한다.

GrantedAuthority객체는 UserDetailsService에 의해 불러올 수 있고,

특정 자원에 대한 권한이 있는지 없는지를 검사해 접근 허용 여부를 결정한다.

 

 

참고자료

https://velog.io/@hope0206/Spring-Security-%EA%B5%AC%EC%A1%B0-%ED%9D%90%EB%A6%84-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%97%AD%ED%95%A0-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0

 

 

 

반응형

loading