동작과정편에서 설명했던 것처럼 이번 글은 로그인 과정에 대해서 알아보고 활용하는 것까지 예시를 통해 알아보자. 이 글을 완벽하게 이해하기 위해서는 앞선 동작 과정편을 보고 오는 것을 추천한다.
동작과정편에서 설명한 것처럼 인증은 filter에 의해서 진행되고 DelegatingFilterProxy로 등록된 FilterChainProxy 빈을 이용한 SecurityFilterChain으로 진행되게 된다. 아래 그림과 같다.
SecurityFilterChain 안에는 spring security에서 제공하는 여러가지 filter들이 있다고 했었다. 이때 Authentication(로그인)을 담당하는 필터는 AbstractAuthenticationProcessingFilter이다. 이 필터는 추상 클래스로 로그인에 필요한 공통적인 로직을 가지고 있는 필터이다. 대충 보면 아래와 같은 로직들을 가지고 있다고 생각하면 된다.
AbstractAuthenticationProcessingFilter는 추상 클래스이기 때문에 SecurityFilterChain안에 직접 들어갈 수 없고 이를 상속 받은 filter들이 속해 있다. 이 글에서는 username과 password를 form태그로 전달해서 로그인을 하는 UsernamePasswordAuthenticationFilter를 가지고 로그인 과정을 살펴볼 것이다. UsernamePasswordAuthenticationFilter는 AbstractAuthenticationProcessingFilter를 상속 받은 클래스이고 SecurityFilterChain안에 들어있다.
AbstractAuthenticationProcessingFilter의 구체적인 설명은 UsernamePasswordAuthenticationFilter가 구동하는 것을 보며 하겠다.
위에서 언급했듯이 UsernamePasswordAuthenticationFilter는 AbstractAuthenticationProcessingFilter를 상속 받은 클래스이다. AbstractAuthenticationProcessingFilter에서 username과 password를 form 태그 기반으로 받는 로직과 UsernamePasswordAuthenticationToken을 발행하는 로직이 추가되어서 상속 받았다고 생각하면 된다.
이제 UsernamePasswordAuthenticationFilter가 구동하는 전체적인 과정에 대해 살펴보자.
위 그림은 spring 공식문서에서UsernamePasswordAuthenticationFilter가 구동하는 과정을 간단하게 표현한 그림이다. 그림에 나온 번호 순대로 진행이되고 나도 그 순서에 맞게 설명을 진행하겠다.
위에서는 전체적인 로그인 과정을 봤다. 이제 구체적으로 어떻게 진행되는지 살펴보자.
위 그림은 UsernamePasswordAuthenticationFilter가 구동하는 과정에 활용되는 class들을 보여주는 그림이다. 번호가 있으니 번호 순서대로 설명하겠다.
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final CustomUserDetailsService userDetailsService;
private final SamplePasswordEncoder passwordEncoder;
public CustomAuthenticationProvider(CustomUserDetailsService userDetailsService, SamplePasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
UserDetails user = userDetailsService.loadUserByUsername(username);
if (user == null) {
throw new BadCredentialsException("username is not found. username=" + username);
}
if (!this.passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("password is not matched");
}
return new CustomAuthenticationToken(username, password, user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return CustomAuthenticationToken.class.isAssignableFrom(authentication);
}
}
이 코드에서 중요한 포인트는 userDetailsService라는 서비스 객체를 주입 받음.
private final CustomUserDetailsService userDetailsService;
이는 실제 인증이 실행되는 authenticate 메서드에서 UserDetail 객체를 뽑아오는데에 활용 됨.
UserDetails user = userDetailsService.loadUserByUsername(username);
그 후 UserDetails객체를 활용해서 인증을 진행하는 것을 볼 수 있음.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberServiceImpl implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Optional<Member> memberOptional = memberRepository.findByEmail(email);
if (memberOptional.isPresent()) {
return new PrincipalDetails(memberOptional.get());
} else {
throw new UsernameNotFoundException(email);
}
}
}
인증이 성공되면 sessionAuthenticationStrategy.onAuthentication가 실행되며 새로운 session이 생기게 된다.
그리고 위 그림의 10번처럼 반환된 정보가 모두 채워져서 반환된 Authentication 객체를 SecurityContext에 감싸고 또 SecurityContextHolder로 감싼다.
그럼 Spring Security는 SecurityContextHolder에 값이 있으면 인증된 유저로 인식하고 Set-Cookie 헤더에 JSESSINID를 넣어서 응답을 한다.
최초 인증이 끝난 유저가 JSESSINID를 Cookie 헤더에 넣어서 요청을 보내면 서버에서는 JSESSINID가 세션에 있는지 확인하여 인증을 진행한다.
여기까지가 전체적인 인증 흐름이다.
여기서 조금 더 나아가서 설명이 부족했던 인증된 객체를 감싸는SecurityContextHolder, SecurityContext와 인증 정보를 가지고 있는 Authentication 객체에 대해서 알아보자.
인증된 객체는 위와 같은 구조로 감싸져 있다. 가장 바깥에 있는 SecurityContextHolder부터 알아보자.
At the heart of Spring Security’s authentication model is the SecurityContextHolder. It contains the SecurityContext.
-Spring 공식문서-
공식문서에서 SecurityContextHolder를 설명한 글이다. 간단하게 얘기하면 SecurityContextHolder는 SecurityContext를 감싸고 있고 spring security의 인증에 핵심 모델이라는 뜻이다.
SecurityContextHolder는 인증된 유저에 대한 정보를 저장하고 있는 객체이다. 특이하게 SecurityContextHolder가 값을 가지고만 있어도 spring security에서는 인증된 유저라고 가정한다.
SecurityContext는 그냥 SecurityContextHolder와 Authentication 객체를 이어주는 역할이라고 보면 된다.
Authentication는 크게 두가지 목적을 가지고 있는 객체이다.
1. AuthenticationManager에 들어가기 위함이다.
2. 현재 입증된 유저를 대표하는 객체이다.
Authentication는 다음과 같은 구성요소를 가진다.
spring security는 WebSecurityConfigurerAdapter를 상속 받아 관련 설정을 관리할 수 있다.
package peermarket.peershop.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import peermarket.peershop.security.LoginSuccessHandler;
@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록됨
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) // secured 어노테이션 활성화, preAuthorize 어노테이션 활성화
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final LoginSuccessHandler loginSuccessHandler;
//해당 메서드의 리턴되는 오브젝트를 ioc로 등록해줌.
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// .antMatchers("/user/**").authenticated()
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/member/loginForm")
.usernameParameter("email")
.loginProcessingUrl(
"/member/login") // /login 주소가 호출이 되면 시큐리티가 낚아채서 대신 로그인을 진행해줌. -> Controller에 로그인을 안만들어됨.
.defaultSuccessUrl("/")
.successHandler(loginSuccessHandler)
.and()
.sessionManagement()
.maximumSessions(1)
.expiredUrl("/member/loginForm");
}
}
configure를 오버라이딩해서 여러가지 설정을 할 수 있다.
너무 많은 설정을 할 수 있으므로 검색해보시기를 추천한다.
지금까지 spring security에 대해서 공부해봤다. 기본적인 내용만 다룬다고 했지만 이것만해도 어려운 내용이 많았다. 그래도 기초는 다졌으니까 다른 내용도 비교적 쉽게 들어올 것 같다! 다음에 spring security를 이용한 jwt 구현과 oauth구현을 할 때도 포스팅을 해야겠다.
긴 글 읽어주셔서 감사합니다 :)
잘 읽었습니다~ 덕분에 spring security에 대해 감을 잡은 것 같아요