로그인이란.. 뭘까요? 🧐

로그인이란.. 대충 이런 느낌일 것 같습니다

  1. 로그인하기 전에는 로그인 페이지로 리다이렉트
  2. 사용자에게 아이디와 비밀번호를 받는다
  3. 입력받은 값과 DB의 값을 비교해서 로그인 처리를 한다

요거를 모두 Spring Boot와 Spring Security의 WebSecurityConfigurerAdapter를 통해서 간단히 구현해볼 수 있는데요..
하나씩 한 번 해보겠습니다~

-1. 시작하기 전에

extends, implements, @Override, @Controller, @Service, @Mapper (혹은 DataSource 등) , @Autowired, @Configuration, @SpringBootApplication, 그리고 ApplicationContext가 어떤 것들인지 알고 있어야 합니다~

0. WebSecurityConfigurerAdapter 만들기

사실 일단 이것부터 만들어야겠죠;

build.gradle

dependencies {
  compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.1.8.RELEASE'
}

SecurityConfig.java

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter

1. 로그인하기 전에는 로그인 페이지로 리다이렉트

WebSecurityConfigurerAdapter 안에는 쓸만한 메소드들이 많은데요, 그중에서 먼저
void configure(HttpSecurity http)를 적당~히 @Override 해보겠습니다.

SecurityConfig.java

@Override
public void configure(HttpSecurity security) throws Exception {
  security
    .antMatchers("/admin/*").authenticated()
    .and().formLogin().loginPage("/login").defaultSuccessUrl("/admin/main", true);
}

HttpSecurity를 쓰려고 얘를 @Override했는데요..
.antMatchers()를 통해 로그인이 필요한 url을 정의해주고,
.formLogin()을 통해 로그인 페이지와 로그인 성공시 보내줄 url을 정의해줍니다.

2. 사용자에게 아이디와 비밀번호를 받는다

@GetMapping("/login")을 통해 적당~히 로그인 페이지를 만듭니다.

login.jsp

<form method="post">
 <input type="text" name="username" />
 <input type="password" name="password" />
</form>

여기서 포인트는.. <form />action attiribute가 없다는 점입니다. 그러니까 Postback으로 구현되어 있습니다.

3. 입력받은 값과 DB의 값을 비교해서 로그인 처리를 한다

POST로 들어온 아이디와 비밀번호는 누가 처리할까요?
UserDetailsServicePasswordEncoder가 처리하도록 정해져 있습니다!

그런데 이게 얘들만의 공식이 있어서.. 먼저 간단히 알아볼게요

  1. DB에서 아이디로 사용자 정보를 조회한다 (=비밀번호 포함)
  2. 입력받은 비밀번호를 인코딩한다
  3. 인코딩한 비밀번호와 사용자 정보의 비밀번호를 비교한다

OperatorService.java

@Service
public class OperatorService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
}

여기서 3-1번을 해주게 됩니다. loadUserByUsername()안에서 적당~히 DB에서 조회하면 되겠네요.
리턴해줄 사용자 정보 객체는 UserDetails의 구현체인 User를 쓰시거나 상속받으시면 될거에요.
음 중요한건.. 음..
보시면 그 생성자에 정의되어 있는데요, 여기에서 usernamepassword를 잘 넣어줘야 합니다.

SecurityConfig.java

private class MyEncoder implements PasswordEncoder {
  @Override
  public String encode(CharSequence rawPassword)

  @Override
  public boolean matches(CharSequence rawPassword, String encodedPassword)
}

그리고 여기서 각각 3-2번과 3-3번을 해주게 됩니다.
음.. 아니면 이렇게 PasswordEncoder를 직접 구현하지 말고 그냥
PasswordEncoderFactories에서 하나 꺼내 쓰거나
StandardPasswordEncoder등의 구현체를 사용하시는 것도 좋습니다.

이제 두 클래스를 객체로 만들어 잘 사용 해야겠죠?WebSecurityConfigurerAdapter로 다시 가봅시다.

SecurityConfig.java

@Autowired
OperatorService operatorService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  auth    
    .userDetailsService(operatorService)
    .passwordEncoder(new MyEncoder());
}

이제 잘 될거에요!

부록 📔

위의 내용을 약간 자세히 설명하면요..
사실 실제로 동작하는 순서는 조금 다릅니다.

제일 제일 먼저, 스프링의 ApplicationContext가 만들어지는 과정에서 DefaultPasswordEncoderAuthenticationManagerBuilder, ProviderManager, DaoAuthenticationProvider, 그리고 서블렛 필터 UsernamePasswordAuthenticationFilter가 만들어집니다. WebSecurityConfigurerAdapter 덕분이죠

이후 사용자가 POST로 로그인을 시도하면.. 위의 서블릿 필터를 통해 ProviderManager.authenticate()가 호출되고, 이어DaoAuthenticationProviderretrieveUser()additionalAuthenticationChecks()를 통해 UserDetailsService.loadUserByUsername()PasswordEncoder.matches()가 호출되어 로그인 정보를 확인합니다.

확인하여 이상이 없다면, 필터에서는 UsernamePasswordAuthenticationToken을 리턴받아 세션에 저장하고, 다시 successfulAuthentication()을 통해 SecurityContext에 저장하고, 사용자를 로그인 완료 페이지로 리다이렉트 시켜줍니다. 와!💀