이 블로그는 개인의 공부 목적으로 작성된 블로그입니다. 왜곡된 정보가 포함되어 있을 수 있습니다.
UserDetailsService
사용자에 관해서는 UserDetailsService가 역활을 하고 있다. 이때 UserDetailsService는 사용자 검색, UserDetailManager는 사용자 CRUD역활을 가진다. UserDetailsService는 UserDetails에 정의된 사용자 정보를 이용하고, UserDetails는 GrantedAuthority인터페이스에 추상화된 권한을 일대다 관계로 가진다.
UserDetails 정의
public interface UserDetails extends Serializable {
String getUserName();
String getPassword();
Collection<? extends GrantedAuthority>getAuthorities();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
UserDetails 인터페이스를 정의하여 사용자정보를 확인할 수 있다. 재미있는 부분이 있는데 getAuthrortites()에서 사용자가 가지고 있는 권한을 모두 Collection에 담는 것을 짐작할 수 있다. isAccountNonExpired과 같이 Non을 붙여 혼동을 주는 네이밍이라고 생각할수 있는데, 이는 일반적으로 권한을 있는 상태를 True 그렇지 않은 상태를 False로 상태를 나타내기 때문에 isAccountNonExpired의 경우 계정 만료가 되지 않은 True상태가 권한이 있는 상태이므로 Non을 붙여서 구현했다.
GrantedAuthority
grantedAuthority 인터페이스는 사용자의 세부 정의 즉 허가된 권한에 대한 인터페이스이다.
public interface GrantedAuthority extends Serializable {
/**
* If the <code>GrantedAuthority</code> can be represented as a <code>String</code>
* and that <code>String</code> is sufficient in precision to be relied upon for an
* access control decision by an {@link AccessDecisionManager} (or delegate), this
* method should return such a <code>String</code>.
* <p>
* If the <code>GrantedAuthority</code> cannot be expressed with sufficient precision
* as a <code>String</code>, <code>null</code> should be returned. Returning
* <code>null</code> will require an <code>AccessDecisionManager</code> (or delegate)
* to specifically support the <code>GrantedAuthority</code> implementation, so
* returning <code>null</code> should be avoided unless actually required.
* @return a representation of the granted authority (or <code>null</code> if the
* granted authority cannot be expressed as a <code>String</code> with sufficient
* precision).
*/
String getAuthority();
}
GrantedAuthority 인터페이스의 내부로 함수형 인터페이스임을 확 인 할수 있다.
GrantedAuthority g1=()->"READ";
GrantedAuthority g2=new SimpleGrantedAuthority("READ");
위코드 처럼 새로운 권한을 GrantedAuthority 생성할 수 있다.
단순한 UserDetail 실습
위에서 알아본 내용으로 단순한 UserDetailsl을 구현해보자.
public class DummyUser implements UserDetails{
@Override
public String getUserName() {
return "bill";
}
@Override
public String getPassword() {
return "12345";
}
@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;
}
}
spring 자체적으로 UserDetails 있는데 바로 User 클래스이다. 빌더 패턴을 이용하여 UserDetails을 지원하고 있다.
UserDetails user= (UserDetails) User.withUsername("bill")
.password("12345")
.authorities("read,","write")
.accountExpired(false)
.disabled(true)
.build();
JPA와 UserDetail
그러면 엔티티와 UserDetail을 분리해야할까? 결론부터 이야기 하면 그렇다. 만약 결합해서 사용한다면 책임이 합쳐지기 때문에 복잡해질수 있다.
@Entity
public class User {
@Id
private Long id;
private String username;
private String password;
private String authority;
}
일반적인 User 엔티티다 (getter setter constructor은 생략하였음)
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());
}
}
SecurityUser에 User를 선언하여 Mapping 시켜서 사용하여 책임을 분리하였다. 그런데 책임을 굳이 분리 해야하는가에 대한 의문이 들었는데(나의 추측은 엔티티의 책임과 Details의 책임이 거이 같다는 생각이다.) UserDetails를 권한을 확인해야하는 경우 사용하고, 그렇지 않은 경우는 엔티티를 사용한다고 생각하면 어느정도 납득된다.(테이블도 2개를 생성해야된다...)
사용자관리
우리는 이제 UserDetails로 계약을 구현하여 사용자를 분류할 수 있다. 이제 사용자를 어떻게 관리하는 지 알아보자. 맨위 다이어그램에 따르면 UserDetailsService가 사용자를 관리를 하고 이떼, UserDetailsManager가 UserDetailServices를 상속하여 CRUD를 지원한다고 한다.
UserDetailService
public interface UserDetailsService {
/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
* @param username the username identifying the user whose data is required.
* @return a fully populated user record (never <code>null</code>)
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailService 내부에는 loadUserByUsername에의해 사용자 이름에 해당하는 UserDetails을 획득한다.
public class User implements UserDetails{
private final String username;
private final String password;
private final String authority;
public User(String username, String password, String authority) {
this.username = username;
this.password = password;
this.authority = authority;
}
@Override
public String getUserName() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(()->authority);
}
//생략
}
'보안' 카테고리의 다른 글
[Spring Security in action] SecurityContext (0) | 2024.01.18 |
---|---|
[Spring Security in action] AuthenticationProvider을 사용한 인증 (0) | 2024.01.16 |
[Spring Security in action] 스프링 시큐리티 시작하기 (0) | 2024.01.11 |
[보안] 전자 지불 시스템, 전자 화페 (0) | 2023.12.08 |
[보안] 전자서명 (0) | 2023.12.02 |