CustomUserDetailsManager과 CustomUserDetails 만들기

2024. 1. 26. 18:09
728x90

JPA를 활용해서 직접 사용자 정보를 관리해 보자. 

>> UserDetailsManager 인터페이스 구현.

 

1) JPA, SQLite 관련 의존성 추가

// build.gradle

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.xerial:sqlite-jdbc:3.41.2.2'
runtimeOnly 'org.hibernate.orm:hibernate-community-dialects:6.2.4.Final'

// application.yaml
spring:
  datasource:
    url: jdbc:sqlite:db.sqlite
    driver-class-name: org.sqlite.JDBC
    username: sa
    password: password

  jpa:
    hibernate:
      ddl-auto: create
    database-platform: org.hibernate.community.dialect.SQLiteDialect
    show-sql: true

 

2) User 엔티티 추가 (Security 내부의 User와 헷갈리기 때문에 UserEntity로 설정.

// UserEntity class
@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;
    @Column(nullable = false)
    private String password;

    private String email;
    private String phone;
}

 

3) JpaRepository 생성

// UserRepository interface

@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
    Optional<UserEntity> findByUsername(String username);

    boolean existsByUsername(String username);
}

 

4) UserDetailsManager 인터페이스를 구현하는 CustomUserDetailsManager 클래스 만들기.

 

- UserDetailsManager는 UserDetailsService를 상속받는 인터페이스이다.

- 결국 타고타고 UserDetailsService를 구현한 구현체라는 것이다. 그래서 loadUserByUsername이 정상 동작 해야 한다.

>> loadUserByUsername : Spring Security 내부에서 사용자 인증 과정에서 활용하는 메서드임!

더보기

- UserDetailsManager : new users를 생성하고, 이미 있는 유저를 업데이트 할 수 있는 기능을 가진 UserDetailsService의 확장.

- UserDetailsService : 프레임워크 내에서 User DAO로 사용된다. 유저의 정보를 load 하는 interface

그러면 CustomUserDetailsManager 만들어보자!! 

- 필요한 것들을 모두 오버라이딩했다. 여기서 일단은 createUser, loadUserByUsername, userExists 세 가지를 구현해 보기로 하였다. 

// CustomUserDetailsManager class
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomUserDetailsManager implements UserDetailsManager {
	//repository를 사용해야하기 때문에 DI 해주기
    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return null;
    }

    @Override
    public void createUser(UserDetails user) {}

    @Override
    public boolean userExists(String username) {
        return false;
    }

    @Override
    public void updateUser(UserDetails user) {
        throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED);
    }

    @Override
    public void deleteUser(String username) {
        throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED);
    }

    @Override
    public void changePassword(String oldPassword, String newPassword) {
        throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED);
    }


}

- 하나씩 살펴보자! 

 

1) loadUserByUsername(String username)

// CustomUserDetailsManager class

	//formLogin 등 Spring Security 내부에서 인증을 처리할 때 사용하는 메서드.	
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // repository에서 username으로 찾아오기
        Optional<UserEntity> optionalUser = userRepository.findByUsername(username);
        // username이 없다면 예외처리
        if (optionalUser.isEmpty()) {
            throw new UsernameNotFoundException("username not found");
        }

        // 있다면 엔티티 가져오기
        UserEntity userEntity = optionalUser.get();
        
        // 리턴을 UserDetails로 해야하기 때문에 UserDetails 형태로 바꿔주기
        UserDetails userDetails = User.withUsername(userEntity.getUsername())
                .password(userEntity.getPassword())
                .build();

        return userDetails;
    }

 

** 근데, 커스텀 하는김에 UserDetails도 커스텀할 수 있다는 사실~!~! 아시나요~! ㅋ ㅎ ㅋ ㅎ 전 몰랐슴다 

여기에 대한 설명은 아래에 접어두겠다.

더보기

똑같은 원리로 UserDetails도 interface로 core user information을 제공한다. 

- 설명을 보자면 Spring Security에서 구현체들이 바로 사용되지는 않는다. 그저 사용자의 정보를 저장할 뿐 > 이후 Authentication object로 캡슐화될 것이라는 것. 

 

- 그리고 얘를  구현한 구현체!는 User라고 적혀있다. 그럼 User를 살펴보자

core user information을 model 한다!! UserDetails에 있었던 메서드들을 오버라이딩하여 구현해 둔 것.

그렇지만, 개발자들이 자신만의 UserDetails 구현체를 만들 수도 있다고 한다. 그것이 CustomUserDetails겠죠!? 

 

- 간단하게 만들어본 CustomUserDetails.   :: username, password 제외하고 id, email, phone도 가져올 수 있도록 Getter도 붙여주었다. >> 똑같이 UserDetails 형태가 필요한 곳에서 사용 가능하다.

(참고로 나는 바보처럼 밑에 있는 isAccounrt관련 boolean값들을 기본으로  되어있던 return false로 놔두고 계속 시도해서 로그인 시도가 계속 막혔다... ㅎㅋ 바보인증)

// CustomUserDetails class

//UserEntity를 바탕으로 Spring Security 내부에서
// 사용자 정보를 주고받기 위한 객체임을 나타내는 interface UserDetails의 커스텀 구현체

@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CustomUserDetails implements UserDetails {

    @Getter
    private Long id;
    private String username;
    private String password;
    @Getter
    private String email;
    @Getter
    private String phone;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Set.of();
    }

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

    @Override
    public String getUsername() {
        return this.username;
    }
    
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

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

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

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

** 커스텀한 UserDetails로 해본 버전!!

// CustomUserDetailsManager class

	@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // repository에서 username으로 찾아오기
        Optional<UserEntity> optionalUser = userRepository.findByUsername(username);
        // username이 없다면 예외처리
        if (optionalUser.isEmpty()) {
            throw new UsernameNotFoundException("username not found");
        }

        // 있다면 엔티티 가져오기
        UserEntity userEntity = optionalUser.get();
        // 리턴을 UserDetails로 해야하기 때문에 UserDetails 형태로 바꿔주기
        UserDetails userDetails = CustomUserDetails.builder()
                .id(userEntity.getId())
                .username(userEntity.getUsername())
                .password(userEntity.getPassword())
                .email(userEntity.getEmail())
                .phone(userEntity.getPhone())
                .build();
        return userDetails;
    }

 

2) userExists(String username)

- 이번에는 username으로 사용자가 존재하는지를 확인하는 메서드를 구현해보자.

얘는 간단하게 JpaRepository로 구현한 것을 가져오면 된다.

// CustomUserDetailsManager class
	@Override
    public boolean userExists(String username) {
        return userRepository.existsByUsername(username);
    }

 

 

3) createUser(UserDetails user)

- 이번에는 UserDetails형태로 넘어온 사용자 정보를 통해 등록하는 과정을 구현해 보자. (단, UserDetails는 비밀번호 암호화가 되어있는 상태라고 가정하고 진행한다.)

// CustomUserDetailsManager class
    @Override
    public void createUser(UserDetails user) {
        // 만약 등록하려는 사용자의 유저네임이 겹치면 bad request
        if (this.userExists(user.getUsername())) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
        }
        // 겹치지 않으면 등록 과정을 진행한다.
        UserEntity newUser = UserEntity.builder()
                .username(user.getUsername())
                .password(user.getPassword())
                .build();
        userRepository.save(newUser);
    }

- 이렇게 하면 유저의 유저네임과 비밀번호가 저장이 된다. 

음, 그러나, 이메일과 휴대폰번호는 저장이 되지 않는다. 왜냐하면 넘어온 UserDetails는 구현체가 User이기 때문이다. 

그러면 이메일과 휴대폰번호까지 포함해서 저장하고 싶다면!?

 

=> CustomUserDetails를 사용하는 것이다.

- CustomUserDetails 사용 ver.    :: 받아온 UserDetails를 CustomUserDetails로 클래스캐스팅 해주어야 한다.

    @Override
    public void createUser(UserDetails user) {
        // 만약 등록하려는 사용자의 유저네임이 겹치면 bad request
        if (this.userExists(user.getUsername())) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
        }
        // 겹치지 않으면 등록 과정을 진행한다.(try-catch로 Exception 처리)
        try {
            CustomUserDetails userDetails = (CustomUserDetails) user; //클래스 캐스팅해줌.
            UserEntity newUser = UserEntity.builder()
                    .username(userDetails.getUsername())
                    .password(userDetails.getPassword())
                    .email(userDetails.getEmail())
                    .phone(userDetails.getPhone())
                    .build();
            userRepository.save(newUser);
        } catch (ClassCastException e) { //ClassCasting 예외발생시 에러로깅, 서버에러 예외처리
            log.error("Failed Cast to: {}", CustomUserDetails.class);
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

 

+ 테스트용으로 사용자를 하나 추가해 준다. (Constructor)

// CustomUserDetailsManager class
	public CustomUserDetailsManager(UserRepository userRepository,
                                    PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        CustomUserDetails userDetails = CustomUserDetails.builder()
                .id(1L)
                .username("user1")
                .password(passwordEncoder.encode("password1"))
                .email("email@naver.com")
                .phone("01011112222")
                .build();
        createUser(userDetails);
    }

 

>> 짠 ! 결과가 잘 나왔다~~~~~~ 

 

>> 새로 회원가입을 해도 잘 나온다. (phone, email은 입력하지 않아서 nulll로 나온다!)

 

 

 

 

난 있는 대로 쓰고, 주는 대로 먹는 찐딴데.. 커스텀을 여기서 하네 ^.^ 헷 
그래도 재밌다 내 멋대로 만드는 유저디테일 ㅋㅎ

 

728x90

'Programming > Spring, SpringBoot' 카테고리의 다른 글

HandlerInterceptor & Filter / SecurityFilterChain  (0) 2024.01.27
Spring의 Dispatcher Servlet  (3) 2024.01.26
Spring Security... 그리고 인터페이스  (0) 2024.01.25
Testing  (1) 2024.01.24
Exception & Validation  (0) 2024.01.23

BELATED ARTICLES

more