11 KiB
아래는 "스프링 시큐리티" 책의 4장에 포함될 "인메모리 인증 및 데이터베이스 연동을 통한 인증", "커스텀 UserDetailsService 구현", 그리고 "비밀번호 암호화와 보안"에 대한 내용입니다. 실무에서의 활용을 고려해 실습 가능하고 명확한 설명을 제공했습니다.
4장. 사용자 인증 구현
4.1 In-Memory 인증 설정
In-Memory 인증은 스프링 시큐리티에서 사용자 정보를 애플리케이션 메모리에 저장하고 관리하는 가장 간단한 인증 방식입니다. 별도의 데이터베이스나 외부 시스템 없이 빠르게 설정할 수 있어 학습이나 프로토타입 개발에 적합합니다.
스프링부트에 스프링 시큐리티를 추가하면 기본적으로 In-Memory 인증이 활성화됩니다. 이 경우 user라는 이름의 사용자와 무작위 비밀번호가 생성되며, 콘솔에 출력됩니다. 하지만 실무에서는 사용자를 직접 정의하고 싶을 때가 많습니다. 이를 위해 설정 클래스를 작성할 수 있습니다.
설정 예제
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 인터페이스를 제공하며, 데이터베이스와 연동해 사용자 정보를 조회할 수 있습니다.
데이터베이스 설정
먼저, 사용자 정보를 저장할 테이블을 설계해야 합니다. 예를 들어:
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를 통해 데이터베이스와 바로 연동할 수 있습니다.
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 인터페이스를 직접 구현해 커스텀 로직을 추가할 수 있습니다.
구현 예제
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)를 포함합니다.
설정에 통합
@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)을 어렵게 함.
설정 예제
@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 등)을 지원할 수 있습니다. - 비밀번호 정책: 강력한 비밀번호를 요구하거나 주기적 변경을 유도하세요.
- 암호화 키 관리: 외부 설정 파일이나 환경 변수를 통해 암호화 키를 관리하세요.
위 내용은 인증 방식의 기본부터 실무 적용까지 다루며, 비밀번호 암호화로 보안을 강화하는 방법을 설명했습니다. 코드 예제나 특정 부분에 대한 추가 설명이 필요하면 말씀해 주세요!