본문 바로가기
Course/Spring Security

[netplix-security-a] UserDetails와 영속성 엔티티의 분리된 구현

by Lpromotion 2024. 9. 29.
Netplix 구독형 멤버십 프로젝트로 배우는 SpringSecurity
[Ch 3. UserDetails와 사용자 관리] - 01. UserDetails 살펴보기
강의를 바탕으로 실습 내용을 정리하였습니다.

목차
 

 

이번 시간에는 UserDetails 인터페이스에 대해 조금 더 디테일한 레벨에서 알아보자

계정 만료, 계정 잠금, 자격 증명 만료, 계정 비활성화 기능을 통해 사용자를 애플리케이션 수준에서 제한할 수 있음

 

1. 간단한 UserDetails 구현해보기

UserDetails, GrantedAuthority 등을 이용하여 기본적인 UserDetails 를 구현해보자

 

요구사항

  • 사용자의 이름은 ”jinny.lee”
  • 비밀번호는 평문으로 “12345”
  • 읽기 권한이 필요함 (READ)

 

user 라는 패키지를 생성하고 하위에 JinnyUser 클래스 생성

  • UserDetails 인터페이스를 상속받도록 구현

 

Username 과 Password 구현

Username 과 Password 를 요구사항에 맞춰 입력하며 고정된 값을 반환하도록 함

 

Authorities 구현

읽기 (READ) 권한 추가

 

사용자 제한 메소드 구현

사용자를 제한하는 로직을 포함시키지 않을 것이기 때문에 별도의 코드 작성은 필요 없음

  • default 메소드이기 때문에 이미 true 를 반환하고 있음

 

JinnyUser 에 대한 테스트 작성

간단한 단위 테스트를 작성하여 DannyUser 가 요구사항대로 구현되었는지 확인

package fast.campus.fcss01.user;

...

class JinnyUserTest {

    @Test
    void jinnyUserTest() {
        // given & when
        JinnyUser jinny = new JinnyUser();

        // than
        assertThat(jinny.getUsername()).isEqualTo("jinny.lee");
        assertThat(jinny.getPassword()).isEqualTo("12345");
        assertThat(jinny.getAuthorities().size()).isEqualTo(1); // size는 READ 권한 1개 있기 때문에 1

        Optional<? extends GrantedAuthority> read = jinny.getAuthorities()
                .stream()
                .filter(authority -> authority.getAuthority().equals("READ"))
                .findFirst();
        read.ifPresent(each -> assertThat(each.getAuthority()).isEqualTo("READ"));
    }
}

 

2. 좀 더 현실적인 UserDetails 활용법

방금까지 살펴본 방식은 JinnyUser 라는 클래스가 한 명의 사용자를 나타냄

  • 여러 인스턴스를 만들더라도 같은 사용자를 의미함
  • 실제 애플리케이션에서는 해당 방법을 사용할 수 없음

 

각기 다른 사용자를 표현할 수 있는 클래스 형태로 만들어야 함

  • username 과 password 를 입력받을 수 있도록 별도 변수를 추가
  • 생성자를 통해 username 과 password 를 입력받음
package fast.campus.fcss01.user;

...

public class SimpleUser implements UserDetails {

    private final String username;
    private final String password;

    public SimpleUser(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(() -> "READ");
    }

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

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

 

UserDetails 생성을 위해 빌더 패턴 활용하기

스프링 시큐리티 라이브러리에서 제공하는 User 클래스에는 UserDetails 생성을 위해 빌더를 제공함

  • 이 빌더를 활용하기 위해서는 사용자 이름과 암호가 필요함
  • 정보 입력 후 .build() 를 통해 UserDetails 를 생성

다른 필드에 대한 빌더도 제공됨

 

User 엔티티와 UserDetails (1)

일반적으로 사용자 정보는 데이터베이스에서 관리되기 때문에 영속성 엔티티를 표현하는 클래스가 필요함

  • 영속성 엔티티 클래스 뿐만 아니라 별도로 사용자를 표현하는 클래스도 필요함 (다른 시스템으로 전송하거나 내부적으로 활용할 목적으로)

 

사용자에 대한 영속성 엔티티는 다음과 같이 표현할 수 있음

  • spring-data-jpa 에 대한 의존성을 추가
  • @Entity 어노테이션 활용
  • 엔티티임을 표현하기 위해 @Id 어노테이션 활용
dependencies {
    ...
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    ...
}
package fast.campus.fcss01.user;

...

@Entity
@Getter
public class UserEntity {
    @Id
    private Long id;
    
    private String username;
    private String password;
    private String authority;
}

 

User 엔티티와 UserDetails (2)

영속성 엔티티에 스프링 시큐리티의 사용자 세부 정보까지 동일한 클래스로 구현하면 어떻게 될까?

  • UserDetails 에서 재정의해야 하는 getUsername 과 영속성 엔티티에서 @Getter 로 제공되는 getUsername 이 동일해지는 상황이 발생
  • 만약 다른 로직이 적용되어야 하는 상황이 된다면 변수명을 다르게 하거나 로직이 상당히 복잡해질 수 있음

 

이런 코드 작성은 안티 패턴으로 지양해야 함

  • 두 책임 (책임 1. 데이터베이스 관련 영속성 엔티티, 책임 2. 스프링 시큐리티의 사용자 정보)이 혼합되어 있기 때문

 

User 엔티티와 UserDetails (3)

해결을 위해서는 별도의 클래스를 구현해서 사용해야 함

  • 영속성 엔티티를 표현하는 UserEnttiy
  • 스프링 시큐리티의 사용자 정보를 표현하는 User
package fast.campus.fcss01.user;

...

@Entity
@Getter
public class **UserEntity** {
    @Id
    private Long id;

    private String username;
    private String password;
    private String authority;
}
package fast.campus.fcss01.user;

...

public class **User** implements UserDetails {

    private final UserEntity userEntity;

    // 여기서 영속성 엔티티를 바로 입력하는 것은 좋지 않을수도 있음
    public User(UserEntity userEntity) {
        this.userEntity = userEntity;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(userEntity::getAuthority);
    }

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

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

핵심은 클래스를 분리하는 것

 

 

GitHub

https://github.com/lpromotion/fcss-01/commit/0bbdf08faee91d3aabb789d36f6f5cf616f30b69

반응형

댓글