유튜브 "개발자 유미"님 강의를 듣고 정리한 내용입니다.

DB 기반 로그인 검증 로직

세션 기반 Form 로그인 인증 절차에 대해서는 이 글을 확인하면 된다.
로그인 폼으로 제출된 로그인 데이터와 DB에 저장된 데이터를 대조하여 로그인 처리를 하기 위해서, 먼저 회원이 존재하는지에 대한 검증 로직을 짜야한다.

CustomUserDetailsService 구현

검증 로직을 작성하기 위해, UserDetailsService 인터페이스를 implement 하는 CustomUserDetailsService를 구현해야 한다.

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userData = userRepository.findByUsername(username);
        if(userData != null){
            return new CustomUserDetails(userData);
        }
        return null;
    }
}

서비스 빈으로 등록이 필요하기 때문에, @Service를 붙인다.
loadUserByUsername() 메서드는 UserDetails를 리턴 타입으로 가진다. UserDetails 객체는 로그인 시 로그인 폼과의 비교를 위해 사용된다.
로그인 시도를 하는 username이 존재하는 회원인지 확인하기 위해, repository에서 username으로 회원을 조회한다.
존재하는 회원이라면, CustomUserDetails 객체를 반환하도록 한다.

CustomUserDetails 구현

UserDetails를 implement하는 CustomUserDetails을 구현해야한다.

@Getter
@Setter
public class CustomUserDetails implements UserDetails {
    private UserEntity userEntity;

    public CustomUserDetails(UserEntity userEntity) {
        this.userEntity = userEntity;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(() -> userEntity.getRole());
        return collection;
    }

    @Override
    public String getPassword() {
        return userEntity.getPassword();
    }

    @Override
    public String getUsername() {
        return userEntity.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

객체의 내부 값을 사용하기 위해서 getter와 setter를 만들어주어야 한다. 또한, CustomUserDetailsService에서 객체를 생성하기 위해서 UserEntity를 파라미터로 받는 생성자를 만들어야한다.

getPassword(), getUsername()은 내부 필드인 userEntity로부터 값을 리턴하면 된다.

getAuthorities() 메서드는 반환 타입에 맞추어 GrantedAuthority의 Collection을 반환해야 한다. Collection 선언 후, userEntity의 Role을 추가해서 반환한다. GrantedAuthority 인터페이스의 구현해야하는 메서드가 1개만 존재하므로, 람다 함수로 작성했다.

로그인 동작 확인

로그인 검증과 관련된 설정이 끝났다.

/admin에 접근했을 때, 먼저 /login 페이지로 리다이렉트된다. ADMIN 역할을 가진 사용자로 로그인을 하게 되면, /admin?continue 페이지로 잘 리다이렉트 된 것을 확인할 수 있다.

Role 주의사항

DB에 저장하는 엔티티의 Role 설정 시 반드시 접두사로 ROLE_을 붙여주고, 뒤에 역할 이름을 써주어야 했다.
ex) ROLE_ADMIN, ROLE_USER

하지만 Security Configuration에서 hasRole()에 설정했던 역할명은 접두사 없이 설정했었다. hasRole(), hasAnyRole() 메서드에서는 역할명 앞에 자동으로 접두사 ROLE_이 붙기 때문이다.

그래서 회원가입/로그인 시 이 점에 주의해야한다.

인증 로직 구현과 매핑 작업은 안 했는데?

로그인 시 존재하는 회원인지 확인하기 위한 검증 로직은 짰다. 하지만 폼과 회원을 비교하는 인증(Authentication) 로직을 따로 작성한 적이 없다.

또한 로그인 폼에서 action으로 지정해둔 loginProc과 관련한 PostMapping을 컨트롤러에서 해주지도 않았다.

그럼에도 불구하고 로그인 폼을 submit하면 로그인이 잘 동작한다.

그 이유는 바로 스프링 시큐리티를 사용할 때 로그인 요청이 필터 단에서 처리되기 때문이다.

request -> filters -> servlet(controller) 순으로 접근하게 되는데, 필터에서 로그인 요청이 먼저 처리되기 때문에 컨트롤러에서 설정할 필요가 없었던 것이다.

security configuration에서 /admin 경로에 대해 ADMIN 역할을 필요로 하도록 설정해두었다면, /admin 접근 시 로그인 창으로 가게 된다.
/admin 접근 시 나타나는 로그인 창에서 ADMIN 역할을 가진 사용자가 로그인을 하게 되면, 처음 요청했던 /admin 경로로 리다이렉션 된다.
이러한 로그인 후의 리다이렉션 처리도 필터에서 해주게 된다.

UserDetailsService의 정체

참고 : https://docs.spring.io/spring-security/reference/servlet/appendix/faq.html#appendix-faq-what-is-userdetailservice

특정한 user 계정의 데이터를 옮기기 위한 DAO 인터페이스일 뿐이다. 그저 프레임워크 내에서 사용하기 위해 데이터를 축적하기 위해 사용할 뿐 별다른 기능은 없다. 인증 기능은 없다는 말이다.

인증은 DaoAuthenticationProvider에서 하게 된다. UserDetailsService와 함께 주입되어, UserDetails의 데이터와 폼 데이터를 대조하여 인증한다.

결론

UsernamePasswordAuthenticationFilter의 인증 절차에서 실질적인 인증 과정은 DaoAuthenticationProvider에 의해 처리되기 때문에, 따로 인증을 위한 로직을 구현할 필요가 없다.(캡슐화) 또한 이 필터와 Provider는 기본적으로 등록이 되어있다. 대신, DaoAuthenticationProvider의 인증 절차로부터 사용되는 UserDetailsService, UserDetails의 구현체만 사용자가 만들어주면 된다.

profile
티스토리로 블로그 이전합니다. 최신 글들은 suhsein.tistory.com 에서 확인 가능합니다.

0개의 댓글