Files
spring-boot-examples/docs/security/03_UserDetailsService 구현.md
2025-04-08 19:56:24 +09:00

227 lines
11 KiB
Markdown

아래는 "스프링 시큐리티" 책의 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 등)을 지원할 수 있습니다.
- **비밀번호 정책**: 강력한 비밀번호를 요구하거나 주기적 변경을 유도하세요.
- **암호화 키 관리**: 외부 설정 파일이나 환경 변수를 통해 암호화 키를 관리하세요.
---
위 내용은 인증 방식의 기본부터 실무 적용까지 다루며, 비밀번호 암호화로 보안을 강화하는 방법을 설명했습니다. 코드 예제나 특정 부분에 대한 추가 설명이 필요하면 말씀해 주세요!