사용자 관리를 위해서는 UserDetailsService
및 UserDetailsManager
인터페이스를 이용한다.
UserDetailsService
: 사용자 이름으로 사용자를 검색하는 역할만 한다.UserDetailsManager
: 대부분의 애플리케이션에 필요한 사용자 추가, 수정, 삭제 작업을 추가한다.UserDetailsManager (UserDetailsManager는 UserDetailsService 계약을 확장한다.)-> UserDetailsService (UserDetailsService는 UserDetails 계약을 이용한다.)-> UserDetails <-(UserDetails는 하나 이상의 권한을 가진다.)GrantedAuthority
애플리케이션은 사용자가 누구인지에 따라 특정 기능을 호출할 수 있는지 여부를 결정한다.
UserDetails
계약은 스프링 시큐리티가 이해하는 방식으로 사용자를 나타낸다.public interface UserDetails extends Serializable {
// 사용자 자격 증명을 반환하는 메서드
// 사용자 암호를 반환
String getPassword();
// 사용자 이름을 반환
String getUsername();
// 앱 사용자가 수행할 수 있는 작업을 GrantedAuthority 인스턴스의 컬렉션으로 반환
// 사용자에게 부여된 그룹을 반환
Collection<? extends GrantedAuthority> getAuthorities();
// 사용자 계정을 필요에 따라 활성화 또는 비활성화하는 메서드
// 계정 만료 여부 - true: 만료 안됨
boolean isAccountNonExpired();
// 계정 장금 여부 - true: 장금 안됨
boolean isAccountNonLocked();
// 자격(비밀번호) 증명 만료 여부 - true: 만료 안됨
boolean isCredentialsNonExpired();
// 계정 비활성화 여부 - 활성화됨
boolean isEnabled();
}
public class DummyUser implements UserDetails {
private final String username;
private final String password;
public DummyUser(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// return List.of(()->"READ");
// or
return List.of(new SimpleGrantedAuthority("READ"));
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
// 모든 기능 활성화 -true
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
org.springframework.security.core.userdtails
패키지의 User
클래스는 UserDetails
형식의 인스턴스를 간단하게 만드는 방법이며 이 클래스로 UserDetails
의 변경이 불가능한 인스턴스를 만들 수 있다. 이 방법으로 사용자를 만들면 UserDetails 계약의 구현을 이용할 필요가 없다.
// User.withUsername(String username) -> User 클래스에 중첩된 빌더 클래스 UserBuilder 인스턴스를 반환
UserDetails u = User.withUsername("bill")
.password("1234")
.authorities("read", "write")
.accountExpired(false)
.disabled(true)
.build();
@Entity
public class User implements UserDetails {
@Id
private String username;
private String password;
private String authority;
public User(String username, String password, String authority) {
this.username = username;
this.password = password;
this.authority = authority;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> authority);
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return 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;
}
}
하나의 클래스에 Jpa 기능과 Security 기능을 사용하는 것을 올바르지 않다.(여러 책임을 혼합하는 것은 올바르지 않다.) 코드가 매우 복잡해진다.
@Entity
public class User{
@Id
private String username;
private String password;
private String authority;
public User(String username, String password, String authority) {
this.username = username;
this.password = password;
this.authority = authority;
}
// .....
}
ublic class SecurityUser implements UserDetails {
private final User user;
public SecurityUser(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return getAuthorities();
}
@Override
public String getPassword() {
return getUsername();
}
@Override
public String getUsername() {
return getPassword();
}
// ...........
}
별도의 클래스를 정의해서 책임을 분리해야 코드이 복잡성을 줄일 수 있다.
GrantedAuthority
인터페이스GrantedAuthority
인터페이스는 사용자 세부 정보의 정의에 이용되며 사용자에게 허가된 이용 권리를 나타낸다. 사용자는 권한이 하나도 없거나 여러 권한을 가질 수 있지만 일반적으로 하나 이상의 권한을 가진다.
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
권한을 만들려면 나중에 권한 부여 규칙을 작성할 때 참조할 수 있게 해당 이용 권리의 이름만 찾으면 된다.
// 권한 이름을 String으로 반환하는 getAuthority() 메서드 구현
GrantedAuthority g1 = () -> "READ"
GrantedAuthority g2 = new SimpleGrantedAuthority("READ");
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
인증 구현은 loadUserByUsername(String username)
메서드를 호출해 주어진 사용자 이름을 가진 사용자의 정보를 얻는다. 사용자 이름이 존재하지 않으면 메서드가 UsernameNotFoundException
을 던진다.
`UsernameNotFoundException` 은 `AuthenticationException`를 상속하고 `AuthenticationException`은 `RuntimeException`을 상속한다. 즉 `RuntimeException`을 발생시킨다.
public class InMemoryUserDetailsService implements UserDetailsService {
// UserDetailsService는 메모리 내 사용자의 목록을 관리
private final List<UserDetails> user;
public InMemoryUserDetailsService(List<UserDetails> user) {
this.user = user;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return user.stream()
.filter( // 사용자의 목록에서 요청된 사용자 이름과 일치하는 항목을 필터링
u->u.getUsername().equals(username)
).findFirst() // 일치하는 사용자가 있으면 반환
.orElseThrow( // 이 사용자 이름이 존재하지 않으면 예외를 던진다.
()->new UsernameNotFoundException("User not found")
);
}
}
UserDetailsManager
인터페이스는 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);
}