스프링 시큐리티에서 json 형식으로 로그인 하는 것을 구현해보려고 합니다.

 

그러기 위해서 먼저 스프링 시큐리티에서 제공하는 기본 형식인 폼 로그인(formLogin) 방식에 대해 먼저 분석해보고, 

해당 구조를 커스텀해보겠습니다.

 

스프링 시큐리티가 구성된 프로젝트는 기본적으로 폼 로그인이 적용됩니다.

 

또한 개발자가 서블릿 기반 시큐리티 설정을 추가하게 되면 아래와 같이 폼 로그인을 별로도 설정할 수 있습니다.

 

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // 서블릿 기반 설정을 추가
        http
            .authorizeRequests(authorizeRequests -> 
                authorizeRequests
                    .anyRequest().authenticated()
            )
            // 폼 기반 로그인을 명시적으로 설정
            .formLogin(formLogin -> 
                formLogin
                    .loginPage("/login")
                    .permitAll()
            );

        return http.build();
    }
}

 

 

그럼 해당 경로로 로그인이 요청되고 실제 로그인이 처리될 때까지 내부 작동에 대해 분석해보겠습니다.

 

 

 

스프링 시큐리티 필터 체인

 

스프링 시큐리티는 로그인 요청을 받으면 가장 먼저 FilterChain을 통해 순서대로 준비된 필터를 거치게 됩니다.

 

위의 그림에서 세 번째 순서에 해당하는 UsernamePasswordAuthenticationFilter가 오늘의 주인공입니다.

 


UsernamePasswordAuthenticationFilter를 중심으로 클래스 다이어그램을 그려보면 위처럼 표현해볼 수 있습니다.

위의 다이어그램을 참고하면서 차근차근 코드를 분석해보시죠.

 


AbstractAuthenticationProcessingFilter 추상 클래스

- UsernamePasswordAuthenticationFilter 구현 클래스

 

UsernamePasswordAuthenticationFilter는 AbstractAuthenticationProcessingFilter라는 추상 클래스를 부모로 갖고 있는데요,

 

가장 먼저 서블릿 컨테이너에 의해 AbstractAuthenticationProcessingFilterdoFilter() 가 호출되면서 본격적으로 로그인 로직이 시작됩니다.

 

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
	//..
    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            //..
            try {
            	 // 요청으로 인증 객체 얻기
                 Authentication authenticationResult = attemptAuthentication(request, response); 
                 //..
                }
            //..
    }
//..
}



위의 doFilter() 내부에서 호출된 attempAuthentication(HttpServletRequest request, HttpServletResponse response)은 추상메소드로 자식 클래스인 UsernamePasswordAuthenticationFilter에서 구현되어 호출됩니다.

 

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	//..
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        //..
        // 아직 인증 완료되지 않은 인증 객체 생성
        UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
                password);
        //..
        // 인증 로직 이어서 호출
        return this.getAuthenticationManager().authenticate(authRequest);
    }
    //..
}

 

UsernamePasswordAuthencticationFilter#attempAuthentication()에서 중요한 두 가지 부분은 

 

1. UsernamePasswordAuthenticationToken의 정적 팩토리 메서드 unauthenticated()를 이용하여 미인증 객체 생성하는 부분과

 

2. 필드 참조 중인 AuthenticationManagerauthenticate()를 호출하는 부분인 this.getAuthenticationManger().authenticate() 부분입니다.

 

UsernamePasswordAuthenticationTokenAuthentication 인터페이스를 구현한 인증 객체 클래스입니다.

(UsernamePasswordAuthenticationToken는 AbstractAuthenticationToken 추상클래스도 상속받음)

 

 

UsernamePasswordAuthenticationToken#unauthenticated()를 통해서 인증이 완료되지 않은 인증 객체를 생성한 후 AuthenticationManager의 authencicate() 에 미인증 객체를 인자로 전달하고 있는 것을 볼 수가 있습니다.

 

다이어그램에서 찾아보면 이부분에 해당합니다.

 


 

AuthenticationManager 인터페이스

- ProviderManager 구현 클래스

 

그 다음 알아볼 곳은 AuthenticationManager입니다.

현재 로직에선 AuthenticationManager 인터페이스를 구현한 ProviderManager를 구현 클래스로 사용하고 있습니다.

 

ProviderManager#authenticate() 코드를 보겠습니다.

 

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
	//..
	private List<AuthenticationProvider> providers = Collections.emptyList();
	//..
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //..
        Authentication result = null;
        //..
        for (AuthenticationProvider provider : getProviders()) {
           //..
            try {
            	// AuthenticationProvider 인터페이스를 구현한 DaoAuthenticationProvider 클래스가 주입됨
            	// authenticate()를 통해 미인증된 객체를 전달하고 인증된 객체를 받는다.
                result = provider.authenticate(authentication); 
                //..
            }
            //..

            return result;
    }
    //..
}

 

 

ProviderManager클래스의 authenticate()는 필드의 참조 중인 AuthenticationProvider의 타입으로 주입받은 DaoAuthenticationProvider 구현 객체를 통하여 provider.authenticate()를 호출하고 있습니다.

 


여기서도 인증이 완료되지 않은 UsernamePasswordAuthenticationToken 인증 객체를 파라미터로 전달한 후,

인증이 된 객체를 전달받고 있습니다.

 


 

AuthenticationProvider 인터페이스

- AbstractUserDetailsAuthenticationProvider 추상 클래스

- DaoAuthenticationProvider 구현 클래스

 

다음으로, AuthenticationProvider 인터페이스와 그의 추상클래스인 AbstractUserDetailsAuthenticationProvider, 구현 클래스인 DaoAuthenticationProvider를 분석해보겠습니다.

 

 

AuthenticationProvider#authenticate(Authentication authentication)는
AuthenticationProvider를 필드로 가지는 ProviderManager의 getProviders()에 의하여 호출되고 있습니다.

 

AuthenticationProvider의 authenticate()는 인터페이스의 추상메소드이기 때문에 구현이 필요합니다.

이 메소드는 추상클래스인 AbstractUserDetailsAuthenticationProvider에서 구현되어 있습니다. 

public abstract class AbstractUserDetailsAuthenticationProvider
		implements AuthenticationProvider, InitializingBean, MessageSourceAware {
	//..
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		// ..
                // 캐시에 존재한다면 캐시에서 UserDetails를 가져옴
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			//..
			try {
            			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			//..
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
	//..
    
	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
			UserDetails user) {
		UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal,
				authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
		//..
		return result;
	}
    //..
}

 

 

AbstractUserDetailsAuthenticationProvider의 authenticate()는 retrieveUser()에 미인증 인증객체를 전달하여 UserDetails 객체를 가져오고 있습니다. 

 

UserDetails 객체는 시큐리티에서 핵심이 되는 객체로 유저정보를 갖고 있습니다. 

 

최종적으로 받아온 UserDetails 객체를 createSuccessAuthentication()에 전달하고
UsernamePasswordAuthenticationToken.authenticated()라는 정적 팩토리 메소드를 통하여 인증된 Authentication 객체를 받아오고 있습니다.

 

 

retrieveUser()의 내부도 알아보겠습니다. 

 

이는 핵심적인 로직을 담고 있는  DaoAuthenticationProvider에 구현되어 있습니다.

Dao라는 명칭에서 알 수 있듯이 DB를 조회하여 유저정보를 가져옵니다. 

 

Json 로그인으로 커스텀할 때 우리는 이 클래스를 이용해 DB에 연동시키면 되겠죠?

 

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {	
	//..
	private UserDetailsService userDetailsService;
	//..

	@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		//..
		try {
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			//..
			return loadedUser;
		}
		//..
	}
}

 

DaoAuthenticationProvider 구현 클래스로, AbstractUserDetailsAuthenticationProvider 추상클래스를 상속 받았습니다.

 

DaoAuthenticationProvider에서 눈여겨 볼 곳은 retrieveUser()입니다.

 

retrieveUser()는 AbstractUserDetailsAuthenticationProvider의 추상메소드로 DaoAuthenticationProvider에서 구현하였습니다.

DaoAuthenticationProvider라는 클래스명에서 유추할 수 있듯, DataBase를 Access하여 user를 retrieve하는 메소드입니다.

DB에 접근할 수 있도록  DaoAuthenticationProvider 필드로 UserDetailsService를 의존받고 있습니다.

 

UserDetailsService 클래스 또한 스프링 시큐리티의 핵심이 되는 클래스로, 

 

 

 

 

 

참고링크)

 

https://velog.io/@choidongkuen/Spring-Security-Spring-Security-Filter-Chain-%EC%97%90-%EB%8C%80%ED%95%B4

https://cjw-awdsd.tistory.com/45

https://memodayoungee.tistory.com/135