UserDetailsService
데이터베이스에서 조회한 회원의 인증 정보를 기반으로
인증을 처리하는 커스텀 UserDetailsService를 구현해 보겠다.
UserDetailsService는 스프링 시큐리티에서 제공해 주는 컴포넌트로
인증에 필요한 사용자의 정보를 로드하는 인터페이스다.
인메모리 데이터베이스를 사용할 때 보았던
InMemoryUserDetailsManager는 UserDetailsManager의 구현체이고
UserDetailsManager는 UserDetailsService를 상속한 인터페이스다.
즉, UserDetailsService = UserDetailsManager + UserDetailsService
CustomUserDetailsService
@Component
public class MyCustomUserDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
private final CustomAuthorityUtils authorityUtils;
// 의존관계 주입
public MyCustomUserDetailsService(MemberRepository memberRepository, CustomAuthorityUtils authorityUtils) {
this.memberRepository = memberRepository;
this.authorityUtils = authorityUtils;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(
() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)
);
Collection<? extends GrantedAuthority> authorities =
authorityUtils.createAuthorities(findMember.getEmail());
return new User(findMember.getEmail(), findMember.getPassword(), authorities);
}
}
UserDetailsService 인터페이스를 구현한 커스텀 MyCustomUserDetailsService 클래스를 만든다.
데이터베이스에서 사용자 인증 정보를 조회해야 하기 때문에 MemberRepository와
조회한 사용자의 권한 정보를 생성할 CustomAuthorityUtils 클래스를 주입받는다.
(CustomAuthorityUtils 클래스는 아직 구현하지 않았다.)
loadUserByUsername 추상 메서드를 구현하기 위해 오버라이딩 한다.
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(
() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND)
);
우선 데이터베이스에서 사용자 인증 정보를 조회한다
Collection<? extends GrantedAuthority> authorities =
authorityUtils.createAuthorities(findMember.getEmail());
CustomAuthorityUtils의 createAuthorities 메서드를 사용해
역할 기반의 권한 정보 컬렉션을 생성한다.
return new User(findMember.getEmail(), findMember.getPassword(), authorities);
UserDetails 인터페이스의 구현체인 User 클래스의 객체를 통해
조회한 인증 정보와 생성한 권한 정보를 스프링 시큐리티에 제공한다.
즉, 데이터베이스에서 인증 정보를 조회해서 권한 정보를 생성한 후에
실제 인증 처리는 스프링 시큐리티에 넘겨준다.
UserDetails는 UserDetailsService에 의해 로드되어
인증을 위한 핵심 User 정보를 표현하는 인터페이스다.
@Component
public class CustomAuthorityUtils {
@Value("${mail.address.admin}")
private String adminMailAddress;
private final List<GrantedAuthority> ADMIN_ROLES = AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER");
private final List<GrantedAuthority> USER_ROLES = AuthorityUtils.createAuthorityList("ROLE_USER");
public List<GrantedAuthority> createAuthorities(String email) {
if (email.equals(adminMailAddress)) {
return ADMIN_ROLES;
}
return USER_ROLES;
}
}
MyCustomUserDetailsService에서 역할 기반의 유저 권한을 생성할 때 사용할
CustomAuthorityUtils 클래스의 코드를 살펴보겠다.
@Value("${mail.address.admin}")
private String adminMailAddress;
@Value 어노테이션을 사용하여 yml 파일에 있는 프로퍼티를 가져와서
관리자용 이메일 주소로 정의한다.
private final List<GrantedAuthority> ADMIN_ROLES = AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER");
private final List<GrantedAuthority> USER_ROLES = AuthorityUtils.createAuthorityList("ROLE_USER");
스프링 시큐리티에서 제공하는 AuthorityUtils 클래스를 사용하여
관리자와 유저의 사용 권한 목록을 생성한다.
public List<GrantedAuthority> createAuthorities(String email) {
if (email.equals(adminMailAddress)) {
return ADMIN_ROLES;
}
return USER_ROLES;
}
createAuthorities 메서드에서는 파라미터로 전달받은 이메일이
프로퍼티로 가져온 관리자용 이메일 주소와 같다면
관리자 계정으로 만들기 위해 관리자 권한을 부여하고
아니라면 일반 권한을 부여한다.
학습을 위한 간단한 코드이기 때문에
실제로는 관리자 권한을 이렇게 간단하게 부여하면 안 된다.
리팩토링
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> optionalMember = memberRepository.findByEmail(username);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
return new HelloUserDetails(findMember);
}
private final class HelloUserDetails extends Member implements UserDetails {
HelloUserDetails(Member member) {
setMemberId(member.getMemberId());
setFullName(member.getFullName());
setEmail(member.getEmail());
setPassword(member.getPassword());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorityUtils.createAuthorities(this.getEmail());
}
@Override
public String getUsername() {
return 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;
}
}
기존의 MyCustomUserDetailsService의 loadUserByUsername을
조금 더 유연하게 리팩토링 한 코드이다.
private final class HelloUserDetails extends Member implements UserDetails {
HelloUserDetails(Member member) {
setMemberId(member.getMemberId());
setFullName(member.getFullName());
setEmail(member.getEmail());
setPassword(member.getPassword());
}
// ...생략
}
기존에 User 객체를 직접 생성하여 리턴하던 역할을 수행하기 위해
UserDetails 클래스를 구현해야 한다.
생성자의 파라미터로 데이터베이스에서 조회한 사용자 인증 정보를 받아온다.
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorityUtils.createAuthorities(this.getEmail());
}
역할 기반의 권한 정보 컬렉션 생성하던 코드도 옮겨서 오버라이딩 해준다.
@Override
public String getUsername() {
return getEmail();
}
getUsername 메서드도 이메일을 리턴하는 것으로 오버라이딩 해준다.
이외 나머지 오버라이딩 하는 부분은 당장은 크게 중요하지 않은 부분이기에
true 값을 리턴하도록 설정하였다.
코드를 리팩토링 하여 기존에 조회한 사용자 정보를 User 객체로 변환하는 과정과
권한 정보를 생성하는 과정을 캡슐화할 수 있게 되었다.
역할을 데이터베이스에서 관리하기
역할을 데이터베이스에서 관리를 하기 위해서는
일단 역할의 권한 정보를 저장할 테이블을 생성하여
회원 가입 시 사용자의 권한 정보를 저장하거나
로그인 시 사용자의 권한 정보를 조회해야 한다.
@Entity
public class Member extends Auditable implements Principal{
// ...생략
@ElementCollection(fetch = FetchType.EAGER)
private List<String> roles = new ArrayList<>();
// ...생략
}
우선은 기존의 엔티티의 매핑을 수정해준다.
컬렉션 타입의 필드에 @ElementCollection 어노테이션을 추가하면
권한 정보와 관련된 별도의 엔티티 클래스를 생성할 필요가 없다.
private final List<String> ADMIN_ROLES_STRING = List.of("ADMIN", "USER");
private final List<String> USER_ROLES_STRING = List.of("USER");
public List<String> createRoles(String email) {
if (email.equals(adminMailAddress)) {
return ADMIN_ROLES_STRING;
}
return USER_ROLES_STRING;
}
CustomAuthorityUtils 클래스에 역할별 권한을 리턴하는
createRoles 메서드를 추가한다.
public void createMember(Member member) {
String encryptedPassword = passwordEncoder.encode(member.getPassword());
member.setPassword(encryptedPassword);
// 추가된 부분
List<String> roles = authorityUtils.createRoles(member.getEmail());
member.setRoles(roles);
memberRepository.save(member);
}
기존에 회원 가입을 처리하던 서비스 메서드에
회원의 권한 정보를 데이터베이스 저장하는 코드를 추가하였다.
CustomAuthorityUtils에 만들어두었던 createRoles 메서드를 사용하여
해당 사용자의 권한들을 가져온 후에 데이터베이스에 저장한다.
private final class HelloUserDetails extends Member implements UserDetails {
HelloUserDetails(Member member) {
setMemberId(member.getMemberId());
setFullName(member.getFullName());
setEmail(member.getEmail());
setPassword(member.getPassword());
setRoles(member.getRoles());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorityUtils.createAuthorities(this.getRoles());
}
}
회원가입은 수정했으니 로그인도 위의 코드처럼 수정하면 된다.
기존 멤버 엔티티의 수정자 메서드를 사용하던 부분에
권한 정보를 추가하는 수정자 메서드를 추가한다.
기존에는 이메일로 권한 목록을 생성하던 createAuthorities 메서드의
파라미터도 데이터베이스에서 가져온 역할로 권한 목록을 생성하게 수정한다.
public List<GrantedAuthority> createAuthorities(List<String> roles) {
List<GrantedAuthority> authorities = roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role)) // (2)
.collect(Collectors.toList());
return authorities;
}
바꾼 파라미터에 맞게 createAuthorities 메서드를 오버라이딩 한다.