UserDetails
: 스프링 시큐리티에서의 사용자를 기술GrantedAuthority
: 사용자가 실행할 수 있는 작업을 정의UserDetailsService
를 확장하는 UserDetailsManager
: 상속된 동작 외에 사용자 만들기, 사용자 암호 수정, 삭제 등의 작업도 지원한다.UserDetailsSerivce
와 PasswordEncoder
는 사용자 세부 정보 및 자격 증명을 직접 처리하는 구성 요소다.
사용자 관리를 위해서는 UserDetailsSerivce
과 UserDetailsManager
인터페이스를 이용해야하는데, UserDetailsService
는 사용자 명으로 사용자를 검색하는 역할만 한다. (프레임워크가 인증을 완료하는 데 반드시 필요한 유일한 작업)
UserDetailsManager
는 UserDetailsService
를 확장해, 대부분의 애플리케이션에 필요한 사용자 추가, 수정, 삭제 작업을 추가한 것이다. (인터페이스 분리 원칙의 예시. 유연성 향상)
만약 인증하는 기능만 필요하다면 UserDetailsService
계약만 구현하면 된다.
사용자 관리를 위해서는 사용자를 나타내는 방법이 필요한데, 이는 UserDetails
계약의 구현을 통해 프레임워크가 이해할 수 있는 방식으로 이루어져야 한다. 사용자는 사용자가 수행할 수 있는 작업을 나타내는 이용 권리의 집합(보통 하나 이상)을 가지며, 이는 GrantedAuthority
인터페이스로 나타낸다.
스프링 시큐리티에서 사용자에 대한 정의는 UserDetails
계약을 준수해야 한다.
public interface UserDetails extends Serializable {
//인증 관련
String getUsername();
String getPassword();
//애플리케이션의 리소스에 접근하기 위한 권한 부여 관련
Collection<? extends GrantedAuthority> getAuthorities();
//아래를 쓸 일이 없다면 그냥 true를 반환하도록 구현하면 됨
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
사용자 세부 정보의 정의에 이용하며, 사용자에 허가된 이용 권리를 나타낸다. 일반적으로 사용자는 하나 이상의 권한을 가진다. (e.g. READ, WRITE, ...)
GrantedAuthority g1 = () -> "READ"; //람다 식으로
GrantedAuthority g2 = new SimpleGrantedAuthority("READ"); //인스턴스를 만들어서 사용
람다 식으로 구현하는 경우
@FunctionalInterface
애노테이션을 지정해서 인터페이스가 함수형임을 지정하는 게 좋다. 람다 식을 쓰는 건 실제 프로젝트에서는 권장되지 않는다.
UserDetails
구현UserDetails
의 구현은 해당 인터페이스를 상속받는 클래스를 만들어 이루어진다. 일부 단순한 애플리케이션의 경우에는 스프링 시큐리티의 빌더 클래스로 사용자 인스턴스를 만들어도 된다.
UserDetails u2 = User.withUserDetails(u).build();
//기존에 있던 유저 디테일을 이용해서 사용자를 만들 수도 있음
User
구현사용자를 관리하는 테이블이 있을 경우, 이름, 암호, 권한 외에도 다른 컬럼을 가지는 경우가 있을 수 있다. 그렇다고 해서 아래와 같이 해서는 만들어서는 안 된다. (편의 상 하나의 권한만을 가진다고 가정)
@Entity
public class User implements UserDetails{
@Id
private int id;
private String username;
private String password;
private String authority;
//생략
public String getAuthority(){
return this.authority;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities(){
return List.of(()->"READ");
}
//생략
}
개발을 해나가다 보면 다른 엔티티와의 관계들도 추가될 수도 있는데, 위와 같이 필드와 메서드를 지정하면 너무 복잡해질 수 있다. getAuthority
와 getAuthorities
같이 혼동될 가능성이 높은 메서드들을 여러 개 만드는 것도 좋지 않다. 한 클래스에는 항상 하나의 책임만 가지도록 분리하는 것이 좋다.
User
클래스와 별도의 책임을 가지는 클래스(e.g. SecurityUser
)를 만들어,User
에는 JPA 엔티티로서의 책임만 남기고, User
는 SecurityUser
의 필드로 넣어 래핑해서 각종 인증 등의 역할은 SecurityUser
클래스를 통해 처리하도록 한다.
@Builder
public class SecurityUser implements UserDetails {
private final User user;
public SecurityUser(User user){
this.user = user;
}
//후략
}
자격 증명을 비교할 때는 어디서 가져오고, 새 사용자를 추가하거나 기존 사용자를 변경할 때는 어떻게 해야할까?
UserDetailsService
계약 구현시스템이 어떻게 작동하는지와 관계없이 스프링 시큐리티에 필요한 것은 사용자 이름으로 사용자를 검색하는 구현을 제공하는 것이다.
//인터페이스
public interface UserDetailsService {
UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException;
}
//커스텀 구현
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Supplier<UsernameNotFoundException> s = () -> new UsernameNotFoundException("username not found");
User user = userRepository.findByUsername(username).orElseThrow(s);
return User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRole().name())
.build();
}
}
loadUserByUsername()
메서드는 주어진 사용자 이름을 가진 사용자의 세부 정보를 가져온다. 이 때, 사용자 이름은 고유하다고 간주되며, 만약 그러한 사용자 이름이 없으면 예외를 던진다.
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);
}
생성, 수정, 삭제, 비밀번호 변경 등이 있는 인터페이스이다. 구현은? 알아서 해보자
참고문헌 : Laurentiu Splica, 스프링 시큐리티 인 액션, 위키북스, 2022