스프링 시큐리티가 사용자를 이해하는 방법

목표

 

Spring Security가 사용자를 이해하기 위한 인터페이스, UserDetails를 이해한다.

사용자를 찾고 관리하는 UserDetailsService을 이해한다.

최소한의 정보를 가진 User Entity를 사용하여 엔드포인트에 접근하는 방법을 배운다.

 

시작하기에 앞서, 스프링 시큐리티의 전체 인증 프로세스를 보자.

오늘은 아래 프로세스 중에, 사용자를 식별하는 User details service에 대한 내용을 정리할 것이다.

 

UserDetails

 

스프링 시큐리티가 사용자의 인증/인가에 필요한 정보를 정의한 인터페이스이다.

다르게 표현하면, 시큐리티가 바라보는 사용자를 정의한 것이다.

코드는 아래와 같다.

package org.springframework.security.core.userdetails;

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

 

사용자의 이름과 암호, 권한을 반환하는 메서드와 활성화에 대한 메서드가 정의되어 있다.

사용자 권한은 인가에서 다뤄질 내용이기 때문에 지금은 ROLE_USER, ROLE_MANAGER 처럼 일반 사용자나 관리자 정도로 알아두자.

아래 활성화 관련 메서드는 true를 활성화한 유저에 대해, false를 그렇지 않은 유저로 반환하기 위해서 이중 부정으로 정의되었다.

 

UserDetailsService

 

사용자를 찾고 관리하기 위해서 사용하는 인터페이스이다.

사용자 이름으로 사용자를 검색하는 역할을 한다. 따라서 오직 하나의 메서드를 갖고, 그 메서드는 UserDetails를 반환한다.

package org.springframework.security.core.userdetails;

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

 

사용자를 추가, 수정, 삭제하기 위해서는 UserDetailsService를 상속하는 UserDetailsManager 인터페이스를 구현한다.

public interface UserDetailsManager extends UserDetailsService {
    void createUser(UserDetails user);

    void updateUser(UserDetails user);

    void deleteUser(String username);

    void changePassword(String oldPassword, String newPassword);

    boolean userExists(String username);
}

 

유저 엔터티를 이용하여 엔드포인트에 접근하기

 

이제 프로젝트에 적용할 수 있도록 유저 엔터티를 정의하고 UserDetailsService를 구현하자.

 

테스트 엔드포인트

테스트하기 위한 엔드포인트를 정의한다.

@RestController
public class TestController {

    @GetMapping("/test")
    public String test(){
        return "test";
    }
}

 

유저 엔터티

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private String email;

    public User() {
    }

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

    public String getEmail() {
        return email;
    }

    public String getPassword() {
        return password;
    }
}

간편한 진행을 위해 H2 데이터베이스를 사용한다. h2에서 테이블 이름은 user를 사용할 수 없기 때문에 users로 수정한다.

그리고 UserDetails 구현 클래스와 다르게 표현하기 위해서 email을 추가한다.

email은 unique하기 때문에 선택했다.

 

UserDetails 구현

public class SecurityUser implements UserDetails {

    private final User user;

    public SecurityUser(User user) {
        this.user = user;
    }

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

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

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

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

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

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

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

위 코드를 실제 프로덕션 상황에서 설명하자면,

수많은 필드를 가진 user 엔터티에서 인증에 필요한 부분만을 바라보기 위해 UserDetails로 감싼다고 생각할 수 있다.

이제 빈에 주입할 UserDetailsService를 구현하자.

 

UserDetailsService 구현

public class InMemoryUserDetailsService implements UserDetailsService {

    private final List<UserDetails> users;

    public InMemoryUserDetailsService(List<UserDetails> users) {
        this.users = users;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        return users.stream()
                .filter(
                        u -> u.getUsername().equals(username)
                )
                .findAny()
                .orElseThrow(
                        () -> new UsernameNotFoundException("유저를 찾을 수 없습니다.")
                );
    }
}

간단한 설명을 위해 메모리에서 유저 정보를 포함하도록 구현했다.

 

마지막으로 Configuration 클래스를 추가한다.

 

@Configuration
public class UserDetailServiceConfig {

    @Bean
    public UserDetailsService userDetailService() {
        User user = new User("마잡개", "1357", "test@test.com");
        SecurityUser securityUser = new SecurityUser(user);
        return new InMemoryUserDetailsService(List.of(securityUser));
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

 

결과

사용자 이름과 암호를 사용하여 접근하는데 성공한다.

구현에 따라 username이 아니라 unique한 email을 사용했음에 주의하자.

 

정리하면, Spring Security는 사용자를 UserDetails에서 규약한 대로 바라본다.

그리고 사용자를 식별하기 위해서 UserDetailsService를 통해 사용자를 찾는다.

'스프링' 카테고리의 다른 글

[Spring Security] HTTP Basic Authentication를 사용한 간단한 인증 처리  (0) 2024.03.26
QueryDSL 비벼먹기  (0) 2023.10.24
QueryDSL 쪄먹기  (1) 2023.10.23
QueryDSL 볶아먹기  (0) 2023.10.22
QueryDSL 다져먹기  (0) 2023.10.21