Search

Spring Security 인증 절차

Tags
Spring Security
Date
2023/11/17

개요

스프링 시큐리티는 웹 애플리케이션 보안의 핵심 요소로, 프로젝트에서 가장 중요한 부분 중 하나입니다. 하지만 그 깊이와 복잡성으로 인해 스프링을 배우고자 하는 사람들에게 큰 도전이 되기도 합니다
오늘 포스팅에서는 스프링 시큐리티의 인증 절차에 대해 정리해보도록 하겠습니다!

1. 스프링 시큐리티 아키텍쳐

2. HTTP Request

사용자가 HTML 폼을 통해 로그인 정보를 입력하면, 일반적으로 아이디와 비밀번호가 포함된 HTTP 요청 메시지가 생성되어 서버로 전송됩니다. 이 요청은 보통 POST 메소드를 사용하여 전송됩니다. 아래는 이러한 HTTP POST 요청 메시지의 예시입니다.
POST /login HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded Content-Length: 27 username=user&password=pass
Java
복사

3. AuthenticationFilter

AuthenticationFilter 는 HTTP 요청을 인터셉트하고, 해당 요청을 바탕으로 UsernamePasswordAuthenticationToken 객체를 생성합니다.
내부 코드는 다음과 같습니다.
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); return this.getAuthenticationManager().authenticate(authRequest); }
Java
복사
POST로 요청되었을 때만 인증 절차를 수행합니다. 만약 요청한 HTTP 메소드가 POST가 아니라면 AuthenticationServiceException을 발생합니다.
usrname과 password를 추출합니다.
UsernamePasswordAuthenticationToken을 username과 password를 통해 생성합니다.
해당 객체는 AuthenticationManager에 담겨 반환됩니다.

 그렇다면 HTTP 요청은 어떻게 인터셉트 되는걸까요?

UsernamePasswordAuthenticationFilter은 기본적으로 /login URL에서 POST 요청을 처리하도록 설정되어 있습니다. 이는 Spring Security의 HttpSecurity 설정 중 formLogin() 메서드에 의해 설정됩니다.
물론 해당 /login URL은 변경될 수 있습니다. 다음은 SecurityConfig 설정 클래스에서 configure()@Override 한 예제입니다.
@Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .defaultSuccessUrl("/home", true) .and() .logout() .permitAll(); }
Java
복사
위 코드에서 .formLogin() 부분이 UsernamePasswordAuthenticationFilter를 활성화하고, 기본적으로 /login URL에서 작동하도록 설정합니다. 추가적으로 .loginPage("/login") 설정을 통해 사용자 정의 로그인 페이지 경로를 설정할 수 있습니다. 이렇게 UsernamePasswordAuthenticationFilter를 사용하여 해당 경로로 들어오는 POST 요청에서 사용자 이름과 비밀번호를 추출하고 인증을 시도합니다.

4. AuthenticationManager

AuthenticationManager는 실제 인증 역할을 하는 AuthenticationProvider를 관리하는 역할을 합니다.
앞서 AuthenticationFilter에서 생성한 UsernamePasswordAuthenticationTokenAuthenticationManager에 의해 처리되었습니다. AuthenticationManager는 적절한 AuthenticationProvider를 찾아 UsernamePasswordAuthenticationToken을 검증하도록 인증 처리를 위임합니다. 인증이 성공하면, AuthenticationProvider는 사용자의 권한과 기타 정보를 담은 완전히 채워진 Authentication 객체를 반환합니다.
이 또한 코드로 확인해보겠습니다.
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
Java
복사
AuthenticaionManager는 인터페이스로 실제로는 이를 구현한 ProviderManager를 확인해야 합니다.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { private List<AuthenticationProvider> providers = Collections.emptyList(); private AuthenticationManager parent; public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) { Assert.notNull(providers, "providers list cannot be null"); this.providers = providers; this.parent = parent; checkState(); } @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 (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (logger.isTraceEnabled()) { logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)", provider.getClass().getSimpleName(), ++currentPosition, size)); } try { result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException | InternalAuthenticationServiceException ex) { prepareException(ex, authentication); throw ex; } catch (AuthenticationException ex) { lastException = ex; } } if (result == null && this.parent != null) { try { parentResult = this.parent.authenticate(authentication); result = parentResult; } catch (ProviderNotFoundException ex) { } catch (AuthenticationException ex) { parentException = ex; lastException = ex; } } if (result != null) { if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { ((CredentialsContainer) result).eraseCredentials(); } if (parentResult == null) { this.eventPublisher.publishAuthenticationSuccess(result); } return result; } if (lastException == null) { lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } if (parentException == null) { prepareException(lastException, authentication); } throw lastException; } }
Java
복사
for-each 문을 통해 해당 Authentication 객체를 처리할 수 있는 AuthenticationProvider를 찾는 것을 확인할 수 있습니다.

5. AuthenticationProvider

AuthenticationProvider는 다음의 역할등을 수행합니다.
AuthenticationManager로부터 Authentication 객체를 수신합니다.
UserDetailsServiceloadUserByUsername()을 호출하여 UserDetails 객체를 생성합니다. 이 때, UserDetails 객체는 사용자의 정보를 포함합니다.
해당 정보를 Authentication 객체에 포함된 정보와 비교하여 사용자의 신원을 검증합니다.
완전하게 인증된 Authentication 객체는 Spring Security의 여러 부분에서 사용됩니다.
기본적으로 Authentication 객체는 SecurityContext에 저장됩니다. SecurityContext는 현재 인증된 사용자의 세부 정보를 포함하고 있으며, 애플리케이션의 다른 부분에서 현재 사용자의 정보에 접근할 때 사용됩니다.
요청된 리소스에 대한 접근 제어를 수행할 때 사용됩니다. 예를 들어, 특정 URL이나 메소드에 대한 권한을 확인할 때, Authentication 객체에 포함된 권한 정보(GrantedAuthority)를 사용하여 사용자가 해당 작업을 수행할 수 있는지 여부를 결정합니다.
현재 인증된 사용자의 정보를 필요로 할 때 SecurityContextHolder를 통해 Authentication 객체에 접근할 수 있습니다. 예를 들어, 사용자의 아이디, 이름, 역할 등의 정보를 조회할 수 있습니다.
스프링 시큐리티 설정에서 인증 성공 핸들러를 구성했다면, 이 핸들러는 인증된 Authentication 객체를 사용하여 인증 성공 시의 특정 동작을 정의할 수 있습니다. 예를 들어, 성공적인 로그인 후 특정 페이지로 리디렉션하는 로직을 구현할 수 있습니다.

6. 코드로 알아보는 추가 정보

UserDetailsService는 DB로 부터 사용자가 로그인 요청 한 정보가 있는지 확인합니다.
@Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { User user = userRepository.findByEmail(email) .orElseThrow(() -> new IllegalArgumentException("해당 이메일로 가입된 계정이 존재하지 않습니다.")); return new CustomUserDetails(user); }
Java
복사
존재한다면 해당 객체를 UserDetails로 감싸 반환합니다.
AuthenticationProvider에서 UserDetails 객체의 암호화된 비밀번호와 임시 Authentication 객체 안에 포함된 Password가 일치하는지 검증합니다.
모든 과정을 성공하면 최종적으로 인증객체를 생성합니다.
해당 인증 객체는 AuthenticationManager 에게 반환될 것이고, UsernamePasswordAuthenticaitonFilter 에게 연달아서 반환됩니다.
최종적으로 인증객체는 SecurityContext에 저장되며 세션에 저장되는 등의 작업이 진행됩니다.