Spring Security - 4. UserDetailsService 구현 해보기

INCHEOL'S·2021년 3월 18일
3

spring-security

목록 보기
5/5

안녕하세요. INCHEOL'S 입니다.

저번시간에는 UsernamePasswordFilter와 그와 관련된 AuthenticationManager, AuthenticationProvider에 대해서 말씀드렸었는데 오늘은 그 중 AuthenticationProvider와 직접 협력하며 저장된 UserDetails 객체를 반환해주는 UserDetailsService를 직접 구현해보도록 하겠습니다.

  1. UserDetailsService
  2. UserDetailsService 구현

1. UserDetailsService

UserDetailsService는 DaoAuthenticationProvider와 협력하는 인터페이스입니다.

DaoAuthenticationProvider는 요청받은 유저의 ID, Password와 저장된 ID, Password의 검증하는 책임을 갖고있는데요. 그래서 이 녀석은 저장된 ID, Password를 갖고오기 위하여 UserDetailsService와 협력합니다.

출처 - spring seuciry 레퍼런스
그림을 통해 DaoAuthenticationProvider가 UserDetailsService와 협력하는 것을 볼 수 있네요:)

그렇다면 UserDetailsService는 어떤 메세지를 정의 했는지 인터페이스를 한번 살펴보시죠.

public interface UserDetailsService {

	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

}

파라미터로 username(유저를 식별하는 수 있는 ID)을 받고 리턴값으로 UserDetails를 돌려주고 있네요.

DaoAuthenticationProvider에서는 돌려받은 UserDetails 객체를 가지고 최종적으로는 UsernamePasswordAuthentication 객체를 만들어 ProviderManager에게 돌려줍니다.

  • UserDetails 인터페이스
public interface UserDetails extends Serializable {

	Collection<? extends GrantedAuthority> getAuthorities();

	String getPassword();

	String getUsername();

	boolean isAccountNonExpired();

	boolean isAccountNonLocked();

	boolean isCredentialsNonExpired();

	boolean isEnabled();

}

Spring Security 에서는 UserDetails의 User라는 구현체를 기본으로 제공하고 있네요.
저는 이 기본 구현체를 이용하여 UserDetailsService를 간단히 구현해보겠습니다.

2. UserDetailsService 구현

저는 멤버를 데이터베이스에 미리 저장하고 저장된 것을 꺼내오는 방식으로 구현해보았습니다.

  • 사용 기술
    DB - H2,
    Data Access - Data Jpa

우선 Member를 위한 package를 만들어서 엔티티, 레파지토리, UserDetailsService구현체를 넣어주었습니다. (패키지 구성은 간편하게 했어요.)

그리곤 코드는 이렇게.. 간략하게 쓱싹

  • Member Entity
@Entity
@Getter
@Setter
@ToString
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    private String memberId;

    private String password;

}
  • Member Repository
public interface MemberRepository extends JpaRepository<Member, Long> {

    Optional<Member> findByMemberId(String memberId);
}
  • UserDetailsService 구현체
@RequiredArgsConstructor
@Component
@Slf4j
public class MyUserDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Member member = memberRepository.findByMemberId(username)
                .orElseThrow(() -> new UsernameNotFoundException("Could not found user" + username));

        log.info("Success find member {}", member);

        return User.builder()
                .username(member.getMemberId())
                .password(passwordEncoder.encode(member.getPassword()))
                .roles("USER")
                .build();
    }

}

주의 하실 점은 User객체를 만드는 코드에 PasswordEncoder를 사용하여 DB에 저장된 원래 패스워드를 암호화하도록 코드가 짜여져 있는데요. 원래는 DB에 저장된 값 자체가 암호화 되어있어야합니다.
즉. 이미 넣을때 passwordEncoder를 통해 암호화된 문자열이 들어가있어야 하는데 편의상 저는 User객체를 리턴해줄때 암호화를 하였습니다.

참고 사항으로 SpringSecurity는 PasswordEncoder 사용을 강제합니다.
실제로 DaoAuthenticationProvider 내부 코드에서도 비밀번호를 원문 텍스트로 비교하는 것이 아닌 passwordEncoder의 matches 메서드를 사용해 비교합니다.
보안상 Spring Security에서는 패스워드의 암호화 방식이 단방향 암호화이며 쉽게 복호화하여 사용자의 패스워드를 볼 수 없게 만들어놨고 암호화되어 저장된 패스워드 문자열을 matches라는 메서드를 통해 비교할 수 있도록 해놨습니다.

그리고 마지막으로 User는 미리 이렇게 data.sql을 이용하여 하나 넣어두었습니다.

  • data.sql 에 작성한 쿼리
insert into member(id, member_id, password) values('0','incheol', 'password');

자, 이제 테스트를 한번 해보고 정상적으로 UserDetailsService 구현체를 통해 로그인이 되는지 확인해보겠습니다.

MyUserDetailsService 에 찍어둔 로그가 출력이 되었고
미리 등록해둔 AuthenticationSuccessHandler가 콘솔로그를 잘 찍혔음을 확인했습니다.

다음에는 Test 코드를 활용하여 인증된 유저가 api 호출을 정상적으로 할 수 있는지 확인해보도록 하겠습니다:)

profile
제주하르방백년초콜릿 먹고싶네요. 아, 저는 백엔드 개발자입니다.

3개의 댓글

comment-user-thumbnail
2021년 6월 27일

크 미치셨네요 새벽에 다 보고 감탄하고 갑니다. 머리속에 정리하는것만으로도 2-3주는 넘기셨을꺼 같은데 글로도 정리하다니요.. 존경합니다 정말

1개의 답글
comment-user-thumbnail
2024년 8월 10일

크 구글링 검색 시, 첫 번째로 나오는 글, 내용 또한 매우 좋ㅎ습니다 ^___^

답글 달기