Backend/Spring Security

Spring Security의 인증(Authentication)

olsohee 2024. 1. 21. 14:16

Spring Security의 인증

Spring Security는 기본적으로 인증 절차를 거친 후에 인가 절차를 진행한다. 인증은 사용자의 신원을 증명하는 과정으로, 인증된 Authentication을 만드는 과정이다. Authentication은 pricipal, credentials, authorities로 구성되어 있으며, 인증된 Authentication은 SecurityContextHolder에 저장된다.

인증 흐름

  1. 사용자가 로그인 정보와 함께 인증 요청을 한다(Http Request).
  2. AuthenticationFilter가 요청을 가로채고, 가로챈 정보를 통해 인증용 객체인 UsernamePasswordAuthenticationToken을 생성한다.
  3. 생성된 UsernamePasswordAuthenticationToken을 인증을 담당하는 AuthenticationManager에게 전달한다. 
  4. AuthenticationManager는 등록된 AuthenticationProvider들을 조회하여 인증을 요구한다.
  5. AuthenticationProvider는 DB에 담긴 사용자 정보와 비교하기 위해 UserDetailService에게 사용자 정보를 넘겨준다.
  6. UserDetailServices는 DB에서 사용자 정보를 찾으면 UserDetails 객체를 만들어 반환한다.
  7. AuthenticationProvider는 넘겨받은 UserDetails 객체와 사용자 정보를 비교한다.
  8. 인증이 완료되면 사용자 정보와 권한 등을 담은 Authentication 객체가 반환된다.
  9. AuthenticationFilter까지 Authentication 객체가 반환된다.
  10. AuthenticationFilter는 Authentication 객체를 SecurityContext에 저장한다.

주요 모듈

Authentication

현재 접근하는 주체의 정보와 권한을 담는 인터페이스이다. Authentication 객체는 SecurityContext에 저장된다. SecurityContextHolder를 통해 SecurityContext에 접근할 수 있고, SecurityContext를 통해 Authentication에 접근할 수 있다.

public interface Authentication extends Principal, Serializable {
	
	Collection<? extends GrantedAuthority> getAuthorities(); // 현재 사용자의 권한 목록
    
	Object getCredentials();
    
	Object getDetails();
 
	Object getPrincipal();
 
	boolean isAuthenticated(); // 인증 여부 
    
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; // 인증 여부 설정
 
}

 

우리는 다음과 같이 인증된 사용자에 대해, 인증이 완료되었다는 의미로 Authentication 객체를 생성하고 SecurityContext에 저장할 수 있다.

UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword(), user.getAuthorities());

 

Authentication 구현체 중 하나인 UsernamePasswordAuthenticationToken은 다음 두개의 생성자를 갖는다. 이때 매개변수로 principal과 credentials만 받는 생성자는 인가 권한인 authenticated 필드를 false로 초기화한다. 반면, 매개변수로 authorities까지 받는 생성자는 authenticated 필드를 true로 초기화 한다.

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
	
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true); // must use super, as we override
	}
    ...
}

 

Spring Security의 인가를 담당하는 AuthorizationFilter는 SecurityContextHolder에서 Authentication 객체를 얻어서 Authentication 객체의  authenticated 필드의 값을 기반으로 인가 권한을 줄지, AccessDeniedException 예외를 발생시킬지 결정한다. (참고: https://olsohee.tistory.com/92)

 

따라서 Authentication을 생성할 때 매개변수로 authorities를 전달해주어야 authenticated 필드 값이 true가 되고, 향후 인가 검증시 AccessDeniedException 예외가 발생하지 않고, 인가 권한을 받아서 권한이 있는 리소스에 접근할 수 있다.

UsernamePasswordAuthenticationToken

Authentication의 구현체인 AbstractAuthenticationToken를 상속받는다. 주로 사용자 id가 Pricipal 역할을 하고, password가 Credential 역할을 한다. 첫 번째 생성자는 인증 전의 객체를 생성하고, 두번째는 인증이 완료된 객체를 생성한다.

// AbstractAuthenticationToken: Authentication의 구현체
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
}
 
// UsernamePasswordAuthenticationToken: AbstractAuthenticationToken의 자식 클래스
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
 
	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
 
	private final Object principal; // 주로 사용자의 id
 
	private Object credentials; // 주로 사용자의 password
 
	// 인증 전의 객체 생성
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		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를 통해서 처리하게 되는데, 실질적으로는 AuthenticationManager에 등록된 AuthenticationProvider에 의해서 처리된다.

public interface AuthenticationManager {
 
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
 
}

ProviderManager

AuthenticationManager의 구현체로, ProviderManager는 AuthenticationManager에 등록된 AuthenticationProvider를 관리한다. 

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
	
	public List<AuthenticationProvider> getProviders() {
		return this.providers;
	}
    
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		int currentPosition = 0;
		int size = this.providers.size();
        
		// for문으로 모든 provider를 순회하여 처리하고 result가 나올때까지 반복한다.
		for (AuthenticationProvider provider : getProviders()) { ... }
	}
}

AuthenticationProvider

AuthenticationProvider에서는 실제 인증에 대한 부분을 처리한다. 인증 전의 Authentication 객체를 받아서 인증이 완료된 Authentication 객체를 반환하는 역할을 한다.

public interface AuthenticationProvider {
 
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
 
	boolean supports(Class<?> authentication);
 
}

UserDetailsService

Spring Security에서 사용자의 정보를 불러오기 위해 구현해야 하는 인터페이스이다. UserDetailsService는 UserDetails 객체를 반환하는 하나의 메소드만 가지고 있다. 일반적으로 UserDetailsService 인터페이스를 구현한 클래스에 UserRepository를 주입받아 DB와 연결하여 사용자의 정보를 불러온 후, UserDetails 객체로 반환한다.

public interface UserDetailsService {
 
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
 
}

UserDetailsManager

UserDetailsService 인터페이스는 사용자 정보를 가져오는 read-only인 loadUserByUsername 메서드 하나만 가지고 있다. 따라서 사용자 정보를 저장하고 수정하는 인터페이스는 UserDetailsManager이다. UserDetailsManager는 UserDetailsService를 상속받기 때문에 사용자에 대해서 좀 더 다양한 역할을 수행할 수 있다.

public interface UserDetailsManager extends UserDetailsService {

	void createUser(UserDetails user);

	void updateUser(UserDetails user);

	void deleteUser(String username);

	void changePassword(String oldPassword, String newPassword);

	boolean userExists(String username);

}

UserDetails

Spring Security에서 사용자 정보를 담는 인터페이스로, UserDetailsService에서 DB 조회 후 조회된 사용자 정보를 반환하기 위해, UserDatails 인터페이스를 구현한 객체를 반환하면 된다. 그러나 UserDetails로는 실무에서 필요한 모든 정보를 담을 수 없을 수 있다. 따라서 이 경우에는 CustomUserDatils를 구현하여 사용하면 된다.

public interface UserDetails extends Serializable {
 
	Collection<? extends GrantedAuthority> getAuthorities();

	String getPassword();
 	
	String getUsername();
 
	boolean isAccountNonExpired();
 
	boolean isAccountNonLocked();
 
	boolean isCredentialsNonExpired();
 
	boolean isEnabled();
 
}

SecurityContextHolder

SecurityContextHolder에는 보안 주체의 세부 정보를 포함하여 응용프로그램의 현재 보안 컨텍스트에 대한 세부 정보가 저장된다.

SecurityContext

SecurityContext는 Authentication을 보관하는 역할을 하며, 이를 통해 Authentication을 저장하거나 꺼내올 수 있다.

GrantedAuthority

GrantedAuthority는 현재 사용자(Principal)가 가지고 있는 권한을 의미하며, ROLE_ADMIN이나 ROLE_USER와 같이 ROLE_* 형태로 사용한다. GrantedAuthority 객체는 UserDetailsService에 의해 불러올 수 있고, 특정 자원에 대한 권한이 있는지를 검사하여 접근 허용 여부를 결정한다.


Reference