위의 그림을 보면 아래와 같은 흐름으로 시큐리티가 구성된다.
위에 3번을 보면 인증 공급자는 인증 논리를 구현한다라고 했는데 인증 논리는 자동으로 구성되는 UserDetailsService
PasswordEncoder
빈을 이용해서 구현한다.
여기서 passwordEncoder는 인코딩, 일치여부는 판단해주지만 디코딩은 제공하지 않는다.
스프링 시큐리티는 기본으로 인증공급자를 제공하는데 이 공급자는 UserDetailsService와 PasswordEncoder를 이용해서 인증을 한다.
이러한 인증 공급자를 재정의해서 인증을 할 수 있는데 일반적으로는 기본으로 등록된걸 쓰지만 아래는 가능하다는 것을 알아보는 예제코드이다.
먼저 AuthenticationProvider를 상속받은 클래스를 재정의한다.
여기서 authenticate 메서드의 if절은 UserDetailsService와 PasswordEncode의 책임을 대신해 인증을 하는 로직이다.
class CustomAuthenticationProvider : AuthenticationProvider {
override fun authenticate(authentication: Authentication): Authentication {
val username = authentication.name
val password: String = authentication.credentials.toString()
if ("john" == username && "12345" == password) return UsernamePasswordAuthenticationToken(
username,
password,
listOf()
)
throw AuthenticationCredentialsNotFoundException("Error in authentication!")
}
override fun supports(authentication: Class<*>): Boolean =
UsernamePasswordAuthenticationToken::class.java.isAssignableFrom(authentication)
}
그리고 설정파일에 해당 클래스를 빈으로 등록해주면 인증관련된 구현을 담당하게 된다.
@Bean
fun customAuthenticationProvider(): AuthenticationProvider {
return CustomAuthenticationProvider()
}
위에 그림처럼 사용자는 4개의 인터페이스와 연관이 있다.
사용자를 나타내며 시큐리티에 인증되는 사용자 정보를 나타낸다(단순히 인증용 사용자이다 시큐리티에서 사용되는 실제 사용자 정보는 Authentication이다).
아래와 같은 인터페이스를 구성하며 아래 boolean을 반환하는 메서드들은 활성화여부와 관련된 메서드들이라 나의 여태 경험으로는 따로 설정을 안하고 true를 반환하는 경우가 대부분이였다.
그리고 위에 3개의 메서드들은 인증에 사용되는 객체를 이용해 사용자 정보를 반환해주면 된다.
여기서 주의할점!
JPA를 사용시 엔티티를 직접 사용하지 말고 래퍼클래스를 만들어 사용하던가 DTO로 변환해서 사용하자!
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
사용자의 권한을 나타낸다.
해당 인터페이스는 사용자 이름으로 찾은 사용자 세부 정보를 반환한다.
이 인터페이스는 loadUserByUsername(String username)이라는 메서드 하나만을 가지고 있는데 인증 공급자(AuthenticationProvider)는 이 인터페이스에 메서드를 이용해 사용자 정보를 가져온다.
사용자의 암호생성, 삭제, 변경 등의 작업을 추가할 수 있다.
해당 인터페이스는 아래와 같은 구조를 가지고 있다.
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);
}