Spring Security는 UserDetail이라는 객체를 통해서 권한 및 인증을 관리한다.
UserDetail 인터페이스는 다음과 같이 생겼으며 이를 상속받아서 Account class를 구현하여 이를 사용해서 인증 및 권한 관리를 한다.
세부 권한은 별도의 Enum 클래스로 구현하여 관리한다.
UserDetail 인터페이스에서 중요한 부분은 3군데다.
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
Account.java
@Entity
@Getter @Setter @Builder
@NoArgsConstructor @AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Account implements UserDetails{
@Id @GeneratedValue
private Integer id;
private String name;
private String password;
@ElementCollection(fetch = FetchType.EAGER)
@Enumerated(value = EnumType.STRING)
private Set<AccountRole> roles;
// Security에서 사용하는 권한은 GrantedAuthority 객체로 관리되기 때문에 해당 객체로 매핑해준다.
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(r -> new SimpleGrantedAuthority("ROLE_" + r.name()))
.collect(Collectors.toSet());
}
// 이번 프로젝트에서 User Id는 name이라는 컬럼으로 사용했기 때문에 getUsername을 수정해준다.
// 그 외 getPassword는 컬럼 값이 같기 때문에 getter로 활용한다.
@Override
public String getUsername() {
return name;
}
// 아래 항목들은 UserDetail에서 사용하는 값이나 현 프로젝트에선 사용하지 않아 기본값인 True로 return 한다.
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
AccountRole.java
// 권한은 크게 ADMIN과 USER로 한다.
public enum AccountRole {
ADMIN, USER
}
유저 정보를 다루기 위한 AccountService를 구현해야한다.
Spring Security에서 사용하기 위해선 UserDetailSerivce를 상속해서 사용해야 한다.
UserDetailService는 다음과 같이 생겼다.
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
여기서 가장 중요한 건(하나밖에 없지만) loadUserByUsername 메소드다. Spring Security에서 DB에 저장된 유저 정보를 가져와 사용자와 비교하기 위해서 DB 조회 후 UserDetail 객체를 반환해줘야 한다.
AccountService.java
@Service
public class AccountService implements UserDetailsService {
@Autowired
private AccountRepository accountRepository;
@Autowired
private PasswordEncoder passwordEncoder;
// DB에서 Account 객체를 조회해와서 반환합니다.
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return accountRepository.findByName(s)
.orElseThrow(() -> new UsernameNotFoundException(s));
}
public Account create(Account account){
account.setPassword(passwordEncoder.encode(account.getPassword()));
return accountRepository.save(account);
}
}
TDD 순서에 맞진 않지만 AccountServiceTest를 구현하여 제대로 AccountSerivce가 구현됐는 지 확인한다.
확인해야할 사항은 다음과 같다.
@SpringBootTest
@ActiveProfiles("test")
class AccountServiceTest {
@Autowired
AccountService accountService;
public Account createAccount(){
return Account.builder()
.name("kimseonjin616")
.password("password")
.roles(Set.of(AccountRole.ADMIN, AccountRole.USER))
.build();
}
@Test
public void find_by_username_success(){
Account account = createAccount();
accountService.create(account);
UserDetails userdetails = accountService.loadUserByUsername(account.getName());
assertEquals(account.getUsername(), userdetails.getUsername());
assertEquals(account.getPassword(), userdetails.getPassword());
}
@Test
public void find_by_username_not_found(){
String wrongUsername = "";
UsernameNotFoundException exception =
assertThrows(UsernameNotFoundException.class,
() -> accountService.loadUserByUsername(wrongUsername));
assertEquals(exception.getCause(), new UsernameNotFoundException(wrongUsername).getCause());
assertEquals(exception.getMessage(), new UsernameNotFoundException(wrongUsername).getMessage());
}
}