아래는 "스프링 시큐리티" 책의 4장에 포함될 "인메모리 인증 및 데이터베이스 연동을 통한 인증", "커스텀 UserDetailsService 구현", 그리고 "비밀번호 암호화와 보안"에 대한 내용입니다. 실무에서의 활용을 고려해 실습 가능하고 명확한 설명을 제공했습니다. --- ### 4장. 사용자 인증 구현 #### 4.1 In-Memory 인증 설정 **In-Memory 인증**은 스프링 시큐리티에서 사용자 정보를 애플리케이션 메모리에 저장하고 관리하는 가장 간단한 인증 방식입니다. 별도의 데이터베이스나 외부 시스템 없이 빠르게 설정할 수 있어 학습이나 프로토타입 개발에 적합합니다. 스프링부트에 스프링 시큐리티를 추가하면 기본적으로 In-Memory 인증이 활성화됩니다. 이 경우 `user`라는 이름의 사용자와 무작위 비밀번호가 생성되며, 콘솔에 출력됩니다. 하지만 실무에서는 사용자를 직접 정의하고 싶을 때가 많습니다. 이를 위해 설정 클래스를 작성할 수 있습니다. ##### 설정 예제 ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; @Configuration public class SecurityConfig { @Bean public UserDetailsService userDetailsService() { var user = User.withUsername("john") .password("{noop}password123") // {noop}은 암호화 없음을 의미 .roles("USER") .build(); var admin = User.withUsername("admin") .password("{noop}admin123") .roles("ADMIN") .build(); return new InMemoryUserDetailsManager(user, admin); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .anyRequest().authenticated()) .formLogin(); return http.build(); } } ``` 위 코드에서 `InMemoryUserDetailsManager`를 사용해 두 명의 사용자(`john`과 `admin`)를 메모리에 등록했습니다. 이들은 각각 `USER`와 `ADMIN` 역할을 가지며, 로그인 시 이를 기반으로 인증됩니다. `{noop}`은 비밀번호 암호화를 사용하지 않음을 의미하며, 이후 암호화 섹션에서 개선 방법을 다룹니다. In-Memory 인증은 간단하지만 사용자 수가 많거나 동적으로 변경될 경우 한계가 있습니다. 이를 해결하기 위해 데이터베이스 연동으로 넘어갑니다. #### 4.2 데이터베이스 연동을 통한 사용자 인증 실제 애플리케이션에서는 사용자 정보를 데이터베이스에 저장하고, 이를 기반으로 인증을 처리하는 경우가 많습니다. 스프링 시큐리티는 이를 위해 `UserDetailsService` 인터페이스를 제공하며, 데이터베이스와 연동해 사용자 정보를 조회할 수 있습니다. ##### 데이터베이스 설정 먼저, 사용자 정보를 저장할 테이블을 설계해야 합니다. 예를 들어: ```sql CREATE TABLE users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, enabled BOOLEAN NOT NULL ); CREATE TABLE authorities ( username VARCHAR(50) NOT NULL, authority VARCHAR(50) NOT NULL, FOREIGN KEY (username) REFERENCES users(username) ); ``` 위 테이블은 사용자 정보(`users`)와 권한 정보(`authorities`)를 분리해 저장합니다. ##### 기본 연동 설정 스프링 시큐리티는 `JdbcUserDetailsManager`를 통해 데이터베이스와 바로 연동할 수 있습니다. ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.JdbcUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import javax.sql.DataSource; @Configuration public class SecurityConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("classpath:schema.sql") .build(); } @Bean public UserDetailsService userDetailsService(DataSource dataSource) { return new JdbcUserDetailsManager(dataSource); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .anyRequest().authenticated()) .formLogin(); return http.build(); } } ``` `JdbcUserDetailsManager`는 기본적으로 `users`와 `authorities` 테이블을 조회하며, 사용자 이름, 비밀번호, 활성화 여부, 권한 등을 가져옵니다. `schema.sql` 파일에 위의 테이블 생성 쿼리를 추가하면 H2 데이터베이스가 자동으로 설정됩니다. #### 4.3 커스텀 UserDetailsService 구현 기본 제공되는 `JdbcUserDetailsManager`는 편리하지만, 테이블 구조나 비즈니스 로직이 복잡할 경우 커스터마이징이 필요합니다. 이때 `UserDetailsService` 인터페이스를 직접 구현해 커스텀 로직을 추가할 수 있습니다. ##### 구현 예제 ```java import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @Service public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public CustomUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserEntity user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); return org.springframework.security.core.userdetails.User .withUsername(user.getUsername()) .password(user.getPassword()) .authorities(user.getRoles().stream().map(Role::getName).toArray(String[]::new)) .disabled(!user.isEnabled()) .build(); } } ``` 위 코드에서: - `UserRepository`는 JPA나 MyBatis 같은 ORM으로 데이터베이스에서 사용자 정보를 조회합니다. - `loadUserByUsername` 메서드는 사용자 이름을 기반으로 사용자 정보를 가져와 `UserDetails` 객체로 변환합니다. - `UserEntity`는 커스텀 엔티티 클래스이며, 역할(`roles`)과 활성화 상태(`enabled`)를 포함합니다. ##### 설정에 통합 ```java @Configuration public class SecurityConfig { @Bean public UserDetailsService userDetailsService(UserRepository userRepository) { return new CustomUserDetailsService(userRepository); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .formLogin(); return http.build(); } } ``` 이렇게 하면 인증 시 `CustomUserDetailsService`가 호출되어 데이터베이스에서 사용자 정보를 가져옵니다. #### 4.4 비밀번호 암호화와 보안 비밀번호를 평문으로 저장하는 것은 보안상 매우 위험합니다. 스프링 시큐리티는 이를 방지하기 위해 비밀번호 암호화를 권장하며, `PasswordEncoder` 인터페이스를 제공합니다. ##### PasswordEncoder 소개 스프링 시큐리티에서 기본으로 사용하는 암호화 방식은 **BCrypt**입니다. BCrypt는 단방향 해시 함수로, 동일한 입력에 대해 항상 다른 출력(솔트 포함)을 생성해 보안을 강화합니다. 주요 특징은 다음과 같습니다: - **솔팅(Salting)**: 동일한 비밀번호라도 다른 해시값을 생성. - **작업 비용(Work Factor)**: 계산 비용을 조정해 무차별 대입 공격(Brute-force)을 어렵게 함. ##### 설정 예제 ```java @Configuration public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) { var user = User.withUsername("john") .password(passwordEncoder.encode("password123")) .roles("USER") .build(); return new InMemoryUserDetailsManager(user); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .formLogin(); return http.build(); } } ``` - `passwordEncoder.encode()`로 비밀번호를 암호화합니다. - 로그인 시 입력된 비밀번호와 저장된 해시값을 비교하려면 `passwordEncoder.matches()`를 내부적으로 사용합니다. ##### 데이터베이스 연동 시 적용 데이터베이스에 저장할 때는 암호화된 비밀번호를 저장하고, 인증 시 `PasswordEncoder`를 `CustomUserDetailsService`와 함께 사용합니다. 스프링 시큐리티는 이를 자동으로 처리하므로, `UserDetails` 객체에 암호화된 비밀번호만 제공하면 됩니다. ##### 추가 보안 팁 - **다양한 암호화 방식**: `DelegatingPasswordEncoder`를 사용하면 BCrypt 외에 다른 알고리즘(PBKDF2, SCrypt 등)을 지원할 수 있습니다. - **비밀번호 정책**: 강력한 비밀번호를 요구하거나 주기적 변경을 유도하세요. - **암호화 키 관리**: 외부 설정 파일이나 환경 변수를 통해 암호화 키를 관리하세요. --- 위 내용은 인증 방식의 기본부터 실무 적용까지 다루며, 비밀번호 암호화로 보안을 강화하는 방법을 설명했습니다. 코드 예제나 특정 부분에 대한 추가 설명이 필요하면 말씀해 주세요!