위 아키텍쳐는 스프링 시큐리티가 구현하는 인증 프로세스의 근간으로, 모든 스프링 시큐리티 구현이 이에 의존한다.
사용자 관리를 위해서는 UserDetailsService 또는 UserDetailsManager 인터페이스를 구현해야 한다.
사용자를 인증하는 기능만 필요한 경우 UserDetailsService 인터페이스만 구현하고, 사용자 추가, 수정, 삭제 작업 등의 추가적인 사용자 관리 기능이 필요하다면 UserDetailsManager 인터페이스를 구현한다.
위 아키텍쳐에 따른 각 구성 요소의 역할은 다음과 같다.
스프링 시큐리티가 사용자를 이해할 수 있으려면, 사용자 정의는 UserDetails 인터페이스를 준수해야 한다. UserDetails 인터페이스의 구조와, 선언된 메서드의 역할은 다음과 같다.
public interface UserDetails extends Serializable {
String getUsername();
String getPassword();
Collection<? extends GrantedAuthority> getAuthorities();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
UserDetails 구현 방법
- UserDetails 인터페이스 구현
- 빌더를 이용한 인스턴스 생성
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 String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> "READ");
}
@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 인터페이스의 맞춤형 구현이 필요 없다면 스프링 시큐리티에서 제공하는 빌더 클래스로 간단한 불변 사용자 인스턴스를 만들 수 있다.
UserDetails u = User.withUsername("seungsu")
.password("1111")
.authorities("read", "write")
.accountExpired(false)
.disabled(true)
.build();
엔티티가 직접 UserDetails를 구현하는 방식으로 설계하게 되면 두 책임이 한 클래스에 전가되는 문제가 있다. 따라서 User 엔티티와 SecurityUser 클래스로 책임을 나누어 User 엔티티가 여러 다른 작업을 구현하지 않도록 설계한다.
User 엔티티
User 클래스는 JPA 엔티티 책임만 담당한다.
@Entity
public class User {
@Id
private Long id;
private String username;
private String password;
private String authority;
@Builder
public User(String username, String password, String authority) {
this.username = username;
this.password = password;
this.authority = authority;
}
// ...
}
SecurityUser
SecurityUser 클래스는 사용자 세부 정보를 스프링 시큐리티가 이해할 수 있는 UserDetails 인터페이스에 매핑하는 작업만 담당한다.
public class SecurityUser implements UserDetails {
private final User user;
public SecurityUser(User user) {
this.user = user;
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(user::getAuthority);
}
// ...
}
사용자 세부 정보의 정의에 이용되며 사용자에게 허가된 이용 권리를 나타내는 인터페이스.
일반적으로 사용자는 하나 이상의 권한을 가진다.
@FunctionalInterface
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
GrantedAuthority를 구현하는 두가지 방법
- @FunctionalInterface 어노테이션을 지정하고 람다식으로 구현
- SimpleGrantedAuthority 클래스로 GrantedAuthority 형식의 불변 인스턴스 생성
GrantedAuthority g1 = () -> "READ"; GrantedAuthority g2 = new SimpleGrantedAuthority("READ");
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
인증 구현은 loadUserByUsername 메서드를 호출해 주어진 사용자 이름을 가진 사용자 세부 정보를 얻는다. 이 메서드가 반환하는 사용자는 UserDetails 인터페이스의 구현체로, 사용자를 찾을 수 없는 경우 UsernameNotFoundException 런타임 예외가 발생한다.
즉 인증 공급자(AuthenticationProvider)는 인증 논리에서 UserDetailsService의 loadByUsername 메서드를 호출하여 사용자 세부 정보를 가져온다.
loadByUsername 메서드는 데이터베이스, 외부 시스템, 볼트 등에서 사용자를 로드하도록 구현한다.
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);
}
UserDetailsManager는 사용자를 관리하는 기능을 포함하기 위해 UserDetailsService 인터페이스를 확장하고 메서드를 추가한다.
대표적으로 InMemoryUserDetailsManager와 JdbcUserDetailsManager가 존재하며, JdbcUserDetailsManager는 데이터베이스에 저장된 사용자를 관리하고 JDBC를 통해 데이터베이스와 직접 연결된다.
- UserDetails 인터페이스는 스프링 시큐리티에서 사용자를 기술하는 데 이용된다.
- UserDetailsService는 애플리케이션이 사용자 세부 정보를 얻는 방법을 설명하기 위해 스프링 시큐리티의 인증 아키텍쳐에서 구현해야 하는 인터페이스이다.
- UserDetailsManager는 UserDetailsService를 확장하고 사용자 생성, 변경, 삭제와 관련된 동작을 추가한다.
대표적인 UserDetailsManager의 구현체
- InMemoryUserDetailsManager
- JdbcUserDetailsManager
- LdapUserDetailsManager