Spring Security 정리

SANG HUN SHON·2023년 1월 30일
0

해당 포스팅은 참고 사이트에서 읽은 내용을 이해하고자 예제를 따라해본것이니, 참고 사이트 내용을 먼저 읽어보시는 것을 권유드립니다.


Spring Security란?

인증(authentication)과 인가(authorization)을 제공해주고 보안 관련 공격을 보호해주는 Framework이다.

  • 인증 : 해당 사용자가 본인이 맞는지 확인하는 과정
  • 인가 : 해당 사용자가 요청하는 자원을 실행할 수 있는 권한이 있는지 확인하는 과정

Spring Security는 인증과 인가에 대한 부분을 Filter 흐름에 따라 처리하고 있다.
Filter는 Dispatcher Servlet으로 가기전에 가장 먼저 URL 요청을 받지만, Intercepter는 Dispatcher와 Controller 사이에 위치한다는점 적용 시기의 차이가 있다.
(Client -> Filter -> Dispatcher Servlet -> Intercepter -> Controller)

또한, 보안과 관련해서 많은 옵션을 제공해주기 때문에 개발자 입장에서는 일일이 보안관련 로직을 작성해야하는 수고로움을 덜어준다.


Spring Security Modules

주요 모듈은 아래와 같으며, 하나씩 살펴보자

[SecurityContextHolder]
SecurityContextHolder는 보안 주체의 세부 정보를 포함하여 응용 프로그램의 SecurityContext에 대한 세부 정보가 저장 된다.

4개 모드를 지원하고 별도로 설정하지 않으면 SecurityContextHolder.MODE_THREADLOCAL 모드를 사용한다.

  • SecurityContextHolder.MODE_THREADLOCAL
  • SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
  • SecurityContextHolder.MODE_GLOBAL
  • SecurityContextHolder.MODE_PRE_INITIALIZED

[SecurityContext]
Authentication을 보관하는 역할을 하며, SecurityContext를 통해 Authentication 객체를 꺼내올 수 있다.
SecurityContext는 SecurityContextHolder에서 꺼내 올 수 있다.

[Authentication]
Authentication는 현재 접근하는 주체의 정보와 권한을 담은 Interface이다.
Authentication 객체는 SecurityContext에서 꺼내 올 수 있다.

public interface Authentication extends Principal, Serializable {

	// 현재 사용자의 권한 목록을 가져온다.
	Collection<? extends GrantedAuthority> getAuthorities();
    
    // 현재 사용자의 credentials(주로 비밀번호)를 가져온다.
    Object getCredentials();
    
    // 현재 사용자의 부가 정보를 가져온다. 만약 사용하지 않으면 Null을 반환한다.
    Object getDetails();
    
	// Principal 객체를 가져온다.
    Object getPrincipal();
    
    // 인증 여부를 가져온다.
    boolean isAuthenticated();
    
    // 인증 여부를 설정한다.
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

[UsernamePasswordAuthenticationToken]
Authentication 인터페이스를 구현 한 AbstractAuthenticationToken의 하위 클래스로, 유저의 ID가 principal 역할을 하고 Password가 credentials 역할을 한다.

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
	
    private final Object principal;
	private Object credentials;
    
    // 인증 완료 전의 객체 생성
    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
	}
    
    // 인증 완료 전의 객체를 생성하는 Factory method
    public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
		return new UsernamePasswordAuthenticationToken(principal, credentials);
	}
    
    // 인증 완료 후의 객체를 생성하는 Factory method
    public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
	}
}

[AuthenticationProvider]
현재 사용자의 인증을 실제로 처리하는 Interface이다. 인증 전의 Authentication 객체를 받아서 인증이 완료 된 Authentication 객체를 반환한다.

AuthenticationProvider 인터페이스를 직접 구현해서 AuthenticationManager에 등록 하여 Custom 기능을 추가 할 수 있다.

public interface AuthenticationProvider {

	// // 인증 전의 Authenticaion 객체를 받아서 인증된 Authentication 객체를 반환
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
    
    boolean supports(Class<?> authentication);
}

[AuthenticationManager]
인증에 대한 부분은 AuthenticationManager를 통해서 처리하게 되는데, 실질적으로는 AuthenticationManager에 등록 된 AuthenticationProvider에 의해 처리된다.

// 인증 처리하는 Filter
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		String username = obtainUsername(request);
		username = (username != null) ? username.trim() : "";
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
		UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
        
        // AuthenticationManager를 가져와서 AuthenticationManager에 등록 된 AuthenticationProvider에 의해 인증 처리가 진행된다.
		return this.getAuthenticationManager().authenticate(authRequest);
	}
}

인증이 성공하면 인증이 성공한 객체를 생성하여 Security Context에 저장한다. 그리고 인증 상태를 유지하기 위해 세션에 보관하며, 인증이 실패한 경우에는 AuthenticationException를 발생시킨다.

AuthenticationManager를 구현 한 ProviderManager는 실제 인증 과정에 대한 로직을 가지고 있는 AuthenticationProvider를 리스트로 가지고 있다.
또한, for문을 통해 모든 AuthenticationProvider를 순회하면서 authenticate 처리를 한다.

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
	
    @Override
	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()) {
        	if (!provider.supports(toTest)) {
				continue;
			}
			...
			try {
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
				prepareException(ex, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw ex;
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
    	}
        ....
    }
}

[UserDetails]
인증에 성공하여 생성된 UserDetails 객체는 Authentication 객체를 구현한 UsernamePasswordAuthenticationToken를 생성하기 위해 사용된다.

UserDetails 인터페이스를 직접 구현 한 User Model을 사용하거나, Spring Security에서 제공해주는 User 구현체를 사용해도 된다.

[UserDetailsService]
UserDetailsService는 UserDetails 객체를 반환하는 단 하나의 메소드를 가진 인터페이스이다.

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

일반적으로 구현체는 UserRepository를 주입받아 DB와 연결하여 처리한다.

[PasswordEncoding]
AuthenticationManagerBuilder.userDetailsService().passwordEncoder() 를 통해 패스워드 암호화에 사용될 PasswordEncoder 구현체를 지정할 수 있다.

	@Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
            .userDetailsService(userDetailService)
            .passwordEncoder(passwordEncoder())
            .and()
            .build();
    }

[GrantedAuthority]
GrantAuthority는 현재 사용자가 가지고 있는 권한을 의미한다. ROLEADMIN이나 ROLE_USER처럼 ROLE...의 형태로 사용하며, 보통 roles이라고 부른다.

GrantAuthority 객체는 UserDetailsService에 의해 불러올 수 있고, 특정 자원에 대한 권한이 있는지를 검사하여 접근 허용 여부를 결정한다.


Spring Security Architecture

아래 과정은 Session 기반으로 Authentication 처리되는 과정을 다루는 내용이다.

1. 로그인 요청
사용자는 로그인 하기 위해 아이디와 비밀번호를 입력하여 로그인 요청을 한다.

2. UserPasswordAuthenticationToken 발급
전송이 오면 AuthenticationFilter로 요청이 가장 먼저 오게되고, 아이디와 비밀번호를 기반으로 UserPasswordAuthenticationToken을 발급한다.

3. UsernamePasswordToken을 AuthenticationManager에게 전달
AuthenticationFilter는 발급 받은 UserPasswordAuthenticationToken을 AuthenticationManager에게 전달한다.

AuthenticationManager은 인증을 처리 할 AuthenticationProvider를 여러개 가지고 있다.

4. UsernamePasswordToken을 AuthenticationProvider에게 전달
AuthenticationManager는 전달 받은 UserPasswordAuthenticationToken을 순차적으로 AuthenticationProvider에게 전달 하여 실제 인증의 과정을 수행해야 하며, 실제 인증에 대한 부분은 authenticate 함수에 작성 해줘야 한다.

SpringSecurity에서는 username으로 DB에서 사용자 조회 후 비밀번호가 일치하는지 검증한다.

5. UserDetailsService로 조회할 아이디를 전달
UserDetailsService은 전달받은 아이디로 사용자를 조회한다.
loadUserByUsername 메소드에 대한 부분은 7번에서 다룬다.

6. 아이디를 기반으로 DB에서 데이터 조회
전달받은 아이디를 기반으로 DB에서 조회하는 구현체는 직접 개발한 Model일 것이고, UserDetailsService의 반환값은 UserDetails 인터페이스이기 때문에 이를 implements하여 구현한 UserDetails Model를 반환한다.

7. 아이디를 기반으로 조회한 결과를 반환
loadUserByUsername 메소드 수행시 6번에서 조회 한 결과를 반환한다.
만약 DB에서 사용자 조회에 실패하는 경우 예외처리를 해준다.

8. 인증 처리 후 인증된 토큰을 AuthenticationManager에게 반환
UserPasswordAuthenticationToken 토큰 정보와 7번에서 반환 된 실제 유저의 패스워드와 일치하는지 확인한다.
DB에 저장 된 유저 패스워드는 암호화되어 있기때문에, 입력 받은 패스워드는 PasswordEncoder를 통해 암호화하여 DB에 저장 된 유저 패스워드와 일치하는지 확인한다.

비밀번호가 일치한다면, 인증 된 토큰을 생성하여 반환하고
일치하지 않다면, BadCredentialsException 예외처리된다.

9. 인증된 토큰을 AuthenticationFilter에게 전달
AuthenticaitonProvider에서 인증이 완료된 UsernamePasswordAuthenticationToken을 AuthenticationFilter로 반환하고, AuthenticationFilter에서는 LoginSuccessHandler로 전달한다.

10. 인증된 토큰을 SecurityContextHolder에 저장
LoginSuccessHandler로 넘어온 Authentication 객체를 SecurityContextHolder에 저장하면 인증 과정이 끝나게 된다.


[참고 사이트]
https://mangkyu.tistory.com/76
https://mangkyu.tistory.com/77
https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/basic.html

profile
개발이 너무 좋아요

0개의 댓글