티스토리 뷰
[Spring-Security] JdbcUserDetailsManager와 CustomDetailsManager
heemang.dev 2024. 4. 28. 16:24
Username과 Password를 입력하여 인증하는 방법으로, Spring Security에서 사용되는 3가지 방식은 다음과 같습니다.
- InMemoryUserDetailsManager
- JdbcUserDetailsManager
- CustomUserDetailsManager
AuthenticationManager는 사용자의 Username과 Password에 기반하여 인증을 처리하도록 AuthenticationProviders에 인증 책임을 위임합니다. Authentication Providers의 구현체 중 하나인 DaoAuthenticationProvider가 인증을 담당합니다. DaoAuthenticationProvider 또한 UserDetailsService에 인증 책임을 위임합니다.
InMemoryUserDetails는 이름에서도 알 수 있듯이 메모리에 사용자의 정보를 저장하여 사용하기 때문에 prod 환경에서는 사용되지 않을 것이라 생각합니다. 보통 데이터베이스에 사용자 정보를 저장하고 사용하기 때문에 나머지 2가지 방식에 대해서 알아보겠습니다.
JdbcUserDetailsManager
UserDetailsManager의 구현체 중 하나인 JdbcUserDetailsManager는 데이터베이스에 저장된 Username과 Password에 접근하여 사용자 인증을 진행합니다. DataSource를 생성하여 데이터베이스에 접근할 수 있도록 해야 합니다.
데이터베이스 정보 입력
application.yml에 데이터베이스 연결 정보를 입력합니다. 예시에는 MySQL을 사용하고 있지만, 다른 데이터베이스를 사용한다면 그에 맞게 입력하면 됩니다.
spring:
datasource:
url: {본인이 사용하는 DB 정보}
username: {본인이 사용하는 DB 정보}
password: {본인이 사용하는 DB 정보}
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
Bean 등록
Spring은 application.yml 작성한 데이터베이스 정보를 가지고 DataSource를 생성합니다. DataSource는 데이터베이스 커넥션을 가져와서 작업을 수행합니다.
// 커스텀 SecurityFilterChain
@Configuration
public class ProjectSecurityConfig {
.. 생략
/**
* JdbcUserDetailsManager : SpringSecurity에서 요구하는 DB 스키마 정보를 따라야 한다. DB에 저장된 자격증명을 사용한다.
* DataSource는 application.yml에 작성한 DB 정보로 생성한다.
*/
@Bean
public UserDetailsService userDetailsService(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}
/**
* NoOpPasswordEncoder() : 비밀번호를 일반 텍스트로 저장한다. 비밀번호 암호화 및 해싱을 수행하지 않기 때문에 prod 환경에서 사용 금지.
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
JdbcUserDetailsManager는 JdbcDaoImpl을 상속받고 있습니다. 특이한 점은 Select 절이 미리 정의되어 있습니다. -> select username, password, enabled...
따라서 데이터베이스에 users 테이블을 생성할 때, 미리 정의된 필드를 사용해야 합니다.
데이터베이스 생성
JdbcUserDetailsManager를 사용하기 위해서는 users 테이블에 id, username, password, enabled 필드가 존재해야 합니다.
미리 사용자 정보를 저장하겠습니다.
사용자 인증
localhost:8080/myAccount에 접속해보도록 하겠습니다. SecurityFilterChain에 "/myAccount"에 대해서 인증 정보를 요구하도록 하였기 때문에 인증을 요구할 것입니다.
SecurityFilterChain
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> {
requests.requestMatchers("/myAccount", "/myBalance", "/myLoans", "/myCards")
.authenticated()
.requestMatchers("/notices", "/contact", "/register").permitAll();
}).formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
DB에 입력한 사용자 정보를 입력하니 인증이 성공적으로 성공되었음을 알 수 있습니다.
CustomUserDetailsManager
JdbcUserDetailsManager의 문제점은 미리 정의된 테이블 형식을 따라야 하는 것입니다. 그러나 운영 환경에는 다른 데이터를 담고 있기 때문에 JdbcUserDetailsManager 방식은 적합하지 않습니다.
운영 환경에 적합하도록 직접 작성한 UserDetailsManager를 사용하는 방법을 알아보겠습니다.
DB 생성
id, email, pwd, role 필드를 갖는 customer 테이블을 생성합니다.
미리 데이터도 삽입하겠습니다.
CustomUserDetailsManager 생성
UserDetailsService의 loadUserByUsername()은 username을 기반으로 사용자의 상세 정보를 조회하는 메서드입니다. 조회된 정보는 UserDetails 객체로 생성되어 반환됩니다.
UserDetailsService를 구현하는 CustomUserDetails를 생성합니다.
Customer 엔티티
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String pwd;
private String role;
}
Customer Repository
public interface CustomerRepository extends JpaRepository {
List findByEmail(String email);
}
loadUserByUsername()는 입력된 username을 통해 해당 사용자의 상세 정보를 가져옵니다. Repository를 통해 조회된 데이터가 1개 이상 조회되면 해당 사용자의 email, password, authorities를 사용하여 User 객체를 생성하여 반환합니다. User 객체는 UserDetails를 상속하고 있으며, Spring Security에서 사용자의 정보를 담는 클래스입니다.
@Service
@RequiredArgsConstructor
public class EazyBankUserDetails implements UserDetailsService {
private final CustomerRepository customerRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<Customer> customer = customerRepository.findByEmail(username);
if(customer.size() == 0) {
throw new UsernameNotFoundException("User details not found for user : " + username);
}
String userName = customer.get(0).getEmail();
String password = customer.get(0).getPwd();
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(customer.get(0).getRole()));
return new User(username, password, authorities);
}
}
사용자 인증
SecurityFilterChain
// 커스텀 SecurityFilterChain
@Configuration
public class ProjectSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((requests) -> {
requests.requestMatchers("/myAccount", "/myBalance", "/myLoans", "/myCards")
.authenticated()
.requestMatchers("/notices", "/contact", "/register").permitAll();
}).formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
/**
* NoOpPasswordEncoder() : 비밀번호를 일반 텍스트로 저장한다. 비밀번호 암호화 및 해싱을 수행하지 않기 때문에 prod 환경에서 사용 금지.
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
localhost:8080/myLoans에 접속하여 DB에 저장된 사용자 정보를 입력하니 정상적으로 인증되었음을 확인할 수 있습니다.