로그인이란.. 대충 이런 느낌일 것 같습니다
1. 로그인하기 전에는 로그인 페이지로 리다이렉트
2. 사용자에게 아이디와 비밀번호를 받는다
3. 입력받은 값과 DB의 값을 비교해서 로그인 처리를 한다
요거를 모두 Spring Boot와 Spring Security의 WebSecurityConfigurerAdapter
를 통해서 간단히 구현해볼 수 있는데요..
하나씩 한 번 해보겠습니다~
extends
, implements
, @Override
, @Controller
, @Service
, @Mapper
(혹은 DataSource
등) , @Autowired
, @Configuration
, @SpringBootApplication
, 그리고 ApplicationContext
가 어떤 것들인지 알고 있어야 합니다~
사실 일단 이것부터 만들어야겠죠;
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
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을 정의해줍니다.
@GetMapping("/login")
을 통해 적당~히 로그인 페이지를 만듭니다.
login.jsp
<form method="post"> <input type="text" name="username" /> <input type="password" name="password" /> </form>
여기서 포인트는.. <form />
에 action
attiribute가 없다는 점입니다. 그러니까 Postback으로 구현되어 있습니다.
POST
로 들어온 아이디와 비밀번호는 누가 처리할까요?
UserDetailsService
와 PasswordEncoder
가 처리하도록 정해져 있습니다!
그런데 이게 얘들만의 공식이 있어서.. 먼저 간단히 알아볼게요
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
를 쓰시거나 상속받으시면 될거에요.
음 중요한건.. 음..
보시면 그 생성자에 정의되어 있는데요, 여기에서 username
과 password
를 잘 넣어줘야 합니다.
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()
가 호출되고, 이어DaoAuthenticationProvider
의 retrieveUser()
와 additionalAuthenticationChecks()
를 통해 UserDetailsService.loadUserByUsername()
과 PasswordEncoder.matches()
가 호출되어 로그인 정보를 확인합니다.
확인하여 이상이 없다면, 필터에서는 UsernamePasswordAuthenticationToken
을 리턴받아 세션에 저장하고, 다시 successfulAuthentication()
을 통해 SecurityContext
에 저장하고, 사용자를 로그인 완료 페이지로 리다이렉트 시켜줍니다. 와!💀
스프링이 어떤 요청이 로그인 요청인줄 알고 UserDetailService를 호출해주는건가요? config파일에 .formLogin().loginPage("/login"). 설정 덕분인가요?? 그런데 저 설정이 없어도 문제는 없더라고요... 신기하네요