Spring Boot(Security login#4)
Spring Security login의 마지막 파트이다.
loginForm 에서 /auth/loginProc 의 해당 주소로 이동하게 설정해놨다.
여기서 재밌는점은 UserApiController 에는 /auth/loginProc의 주소를 받는 컨트롤러는 존재하지 않고 /auth/joinProc의 주소만 존재한다. 이유는 뭘까? 그 이유는 바로 Security가 로그인 요청을 가로채게 만들 것이기 때문이다.
해당 설정을 위해 SecurityConfig에서 작업을 실시한다.
.loginProcessingUrl() 이 로그인 요청을 가로채고 해당주소로 오는 로그인을 대신해주고 정상적으로 요청이 완료되면 .defaultSuccessUrl()의 해당 주소로 이동하게 된다.
여기서 중요한점은 우리는 loginForm.jsp에서 Username과 Password를 들고 해당주소로 이동했는데 스프링 시큐리티가 이것을 가로채서 대신 로그인을 했다는 점인데 우리는 이 상황에서 반드시 Userdetails를 가지고 있는 User 객체를 하나 생성해야한다.
- 스프링 시큐리티가 로그인 요청을 하고 세션에 등록을 해주는데 그때 User 객체를 등록해줄 수가 없다.(타입이 맞지 않아서)
- Userdetails 타입을 가지고 있는 User 객체만이 세션에 등록될 수 있기 때문에 우리는 반드시 타입에 맞게 객체를 생성해야 한다.
Config 패키지 내부에 auth 패키지 생성후에 PrincipalDetail.java 클래스를 생성해준다.
여기서 principalDetail은 User 객체를 품고 있고, principalDetail은 UserDetails를 상속받아 UserDetails의 타입을 갖게한다.
UserDetails를 상속받으려면 UserDetails가 가지고 있는 추상 메소드들을 필수적으로 오버라이딩 해야 한다.
추상 메소드 오버라이딩 까지 구현했으면 생각해야될 것이 하나 더 있다. 저번 블로그 포스팅에서 password를 해쉬로 암호화 했었는데, 시큐리티가 대신 로그인해주면서 password를 가로챈 값이 어떤 값으로 해쉬가 되어 회원가입이 되었는지 알아야 같은 해쉬로 암호화해서 DB에 있는 해쉬랑 비교를 할 수 있기 때문에 이것도 알맞게 구현해야줘야 한다.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(null).passwordEncoder(encodePWD());
}
현재 null 값으로 표현되어 있는 곳에 실제로 로그인이 진행되는 곳을 넣어줘야된다.
실제로 로그인이 진행되는 곳을 구현해보자.
auth > PrincipalDetailService.java를 생성하고 UserDetailsService를 상속해주고 loadUserByUsername() 메소드를 오버라이딩 해준다. 이 메소드의 역할은 스프링이 로그인 요청을 가로칠 때, username, password 변수 2개를 가로채는데 password 부분 처리는 알아서 진행하고, username만 db의 있는지만 확인해주는 역할을 담당한다.
@Service // Bean 등록
public class PrincipalDetailService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
// 스프링이 로그인 요청을 가로챌 때 username, password 변수 2개를 가로채는데
// password 부분 처리는 알아서 함
// username이 db에 있는지만 확인해주면됨.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User principal = userRepository.findByUsername(username)
.orElseThrow(()->{
return new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다.:" + username);
});
return new PrincipalDetail(principal); // 시큐리티의 세션에 유저 정보가 저장이 됨.
}
}
username이 db에 있는지 확인해주기 위해 UserRepository에서 findByUsername() 메소드를 구현해준다.
public interface UserRepository extends JpaRepository<User, Integer>{
// SELECT * FROM user WHERE username = ?;
Optional<User> findByUsername(String username);
}
이제 null의 자리에다가 작업했던 principalDetailService를 넣어준다.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(principalDetailService).passwordEncoder(encodePWD());
}
굉장히 복잡한 구조이지만, 천천히 정리를 해보면
- 로그인을 요청하면 /auth/loginProc 보내지는 데이터를 스프링 시큐리티가 가로챈다.
- 시큐리티가 가로챈 데이터(username, password)를 principalDetailService에 loadUserByUsername에 던진다.
- username을 비교해한 후, 시큐리티의 세션에 유저 정보가 저장이 된 것을 return 해주고 이와 동시 auth.userDetailsService가 사용자가 적은 패스워드를 암호화해서 DB의 암호와 비교를 해준다.
- 데이터가 정상인것을 확인되면 시큐리티 세션에 유저 정보가 저장이된다.
굉장히 복잡하지만 Spring Security의 인증절차를 커스터마이징해서 새로운 로그인 서비스로 구현하는 방법을 공부했다.
사실 굉장히 어렵다.. 몇 번 더 반복해 보면서 구조 자체를 이해해야 될 것 같다..