Spring Security (3) - AuthenticationManager, AuthenticationProvider 동작 흐름 분석

hyozkim·2020년 5월 17일
1

Spring Security

목록 보기
3/6
post-thumbnail

사용자 아이디/비밀번호를 인증처리하는 과정으로 Spring Security에서는 AuthenticationManager, AuthenticationProvider가 있다.

Manager은 쉽게 말해서 공장 안에서 작업 처리를 지시하는 매니저라 생각하자.

Provider은 Manager가 시켜서 일하는 작업자라 생각하자.

이렇게 컨셉을 잡고, Spring Security를 차근차근 파헤쳐 보자!

AuthenticationManager

Manager가 일을 시키기 위해선 Provider가 누군지 알아야 겠죠.

WebSecurityConfigure에서 AuthenticationProvider을 등록해주죠.

1. AuthenticationProvider 추가하기

WebSecurityConfigure.java

@Autowired
public void configureAuthentication(AuthenticationManagerBuilder builder, JwtAuthenticationProvider jwtAuthenticationProvider) {
    builder.authenticationProvider(jwtAuthenticationProvider);
}

WebSecurityConfigure에서 다음과 같이 커스텀한 JwtAuthenticationProvider을 등록했습니다.

👉 AuthenticationManagerBuilder

스프링의 ApplictaionContext가 만들어지는 과정에서 생성되고, 클래스 내부 메소드가 실행되면서 ProviderManager 클래스를 생성하여 add한 JwtAuthenticationProvider가 주입된다.

2. AuthenticationManager 실행 동작 흐름

1번에서 WebSecurityConfigure에서 AuthenticationManagerAuthenticationProvider를 추가했다.

이제 AuthenticationManager authenticate() 메소드를 실행시키면 어떻게 동작하는지 보자!

ProviderManager.java

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
	// ...
	private List<AuthenticationProvider> providers = Collections.emptyList();
	// ...
    	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;
		boolean debug = logger.isDebugEnabled();
        for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}
			try {
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			} catch (AuthenticationException e) {
				lastException = e;
			}
		}

AuthenticationManager은 interface이다.

ProviderManagerAuthenticationManager interface의 구현체 클래스이다.

따라서, ProviderManager 클래스 안에 authenticate()에서 WebSecurityConfigure에서 등록했던 AuthenticationProvider들을 getProviders()로 받아와 for문을 실행하며 모두 일 시킨다.

바로 이 부분이 Manager가 등록된 Provider에게 일을 시키는 부분이다.

Authentication
authenticate() 메소드에서 파라메터로 받는 Authentication은 스프링 시큐리티의 핵심이라고 할 수 있다. 그래서 다음 포스트에 내용을 깊게 정리할 것이다.

AuthenticationProvider

그럼 이제 AuthenticationProvider가 일해야 하는 부분을 커스텀하여 만들어 줘야 한다.

왜냐하면 AuthenticationProvider도 interface이다.

이 interface를 구현하는 구현체 클래스를 사용하고자하는 목적에 따라 커스텀하면 된다.

난 이번에 JWT 토큰을 사용해서 인증하기 때문에 JwtAuthenticationProvider 클래스를 작성했다.

AuthenticationProvider 인터페이스는 기본적으로 2개 메소드를 Override받는다.

public Authentication authenticate(Athentication authentication)
public boolean supports(Class<?> authentication) 

JwtAuthenticationProvider.java

public class JwtAuthenticationProvider implements AuthenticationProvider {
    // ...
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        try {
            // 로그인 인증 전
            JwtAuthenticationToken authenticationToken = (JwtAuthenticationToken) authentication;
            String principal = (String) authenticationToken.getPrincipal();
            String credential = authenticationToken.getCredentials();
            AuthResponseDto authResponseDto = userService.login(new AuthRequestDto(principal, credential));
            // 로그인 인증 후
            JwtAuthenticationToken authenticated
                    = new JwtAuthenticationToken(authResponseDto.getUser().getId(), null, AuthorityUtils.createAuthorityList("ROLE_USER"));
            authenticated.setDetails(authResponseDto);
            return authenticated;
        } catch(DoNotExistException e) {
            throw new UsernameNotFoundException(e.getMessage());
        } catch(IllegalArgumentException e) {
            throw new BadCredentialsException(e.getMessage());
        } catch(DataAccessException e) {
            throw new AuthenticationServiceException(e.getMessage());
        }
    }
    @Override
    public boolean supports(Class<?> authentication) {
        return ClassUtils.isAssignable(JwtAuthenticationToken.class, authentication);
    }
}

JwtAuthenticationTokenAuthentication의 구현체 클래스이므로 Authentication(인증 정보)이라고 생각해주쇼.

userService.login() 이후 Authentication은 인증이 되고, Details에 로그인 결과값(User 객체, jwtToken)을 담아 return하게 된다.

그리고 1번에서 Manager가 일 처리할 때 for문 안에 if문supports메소드 사용하는 것을 볼 수 있다. JwtAuthenticationTokenAuthentication 인터페이스를 사용하는 클래스인지 분별하는 함수로 커스텀에 맞게 작성해주면 된다.

마무리

Manager가 일을 시키면 Provider가 일한다.

스프링 시큐리티는 추상화된 부분이 너무 많아 document읽는 것도 중요하지만 직접 소스를 까보며 코드를 작성하는 것이 좋다.

물론 시간이 너무 많이 걸리지만 한번 이해하고 이해하는 것에 그치지 않고 정리해두면 다음에 볼 때 더 생산성있게 코드를 짤 수 있을 것 같다.

(프로그래머스) 단순 CRUD는 그만! 웹 백엔드 시스템 구현 온라인 스터디(Java반) 강의를 수강하고 제가 이해한대로 정리했습니다. 문제가 될시 삭제하겠습니다!

profile
차근차근 develog

0개의 댓글