이 장에서는 다음 주제를 다룹니다:
- Spring Security를 사용한 첫 번째 프로젝트 생성
- 인증과 권한 부여를 위한 기본 구성 요소 활용 및 간단한 기능 설계
- 기본 개념 이해 및 이를 특정 프로젝트에 적용하는 방법 탐색
- 기본 인터페이스 구현 및 그 상호 연관성 파악
- 주요 책임의 사용자 정의 구현 작성
- Spring Boot의 기본 Spring Security 설정 재정의
Spring Boot와 자동 구성의 장점
Spring Boot는 Spring Framework 기반 애플리케이션 개발의 진화된 형태로, 사전 구성된 기본 설정을 제공하여 필요 시 일부만 재정의하도록 지원합니다. 이러한 방식은 “컨벤션 오버 구성”(Convention over Configuration) 접근법으로, 복잡한 초기 설정 없이 개발 효율성을 높입니다.
오늘날 대부분의 개발자들이 Spring Boot 3을 사용하며, 이는 현대 소프트웨어 개발에 적합한 철학을 제공합니다.
자동 구성의 특징
Spring Boot의 자동 구성은 애플리케이션에 필요한 기본 설정을 미리 제공하므로, 개발자는 필요한 부분만 변경해 사용하면 됩니다. 예를 들어, 데이터베이스 연결이나 보안 설정도 기본값으로 제공되며, 필요한 경우에만 수정할 수 있습니다.
과거 개발 방식과의 비교
Spring Boot 이전에는 모든 애플리케이션 구성 코드(보안, 데이터베이스 설정 등)를 개발자가 직접 작성해야 했습니다. 이는 모놀리식 아키텍처 환경에서는 큰 문제가 아니었지만, 서비스 지향 아키텍처(SOA)나 마이크로서비스 환경에서는 각 서비스별로 반복적인 설정 작업이 요구되었습니다.
Willie Wheeler와 Joshua White의 Spring in Practice 3장에서는 Spring 3을 사용해 웹 애플리케이션을 작성할 때의 복잡한 구성 과정을 다루고 있습니다. 이는 Spring Boot가 얼마나 간소화된 개발 방식을 제공하는지 더 깊이 이해할 수 있는 좋은 참고 자료입니다.
관련 내용은 Spring in Practice Chapter 3에서 확인할 수 있습니다.
Spring Security 도입과 학습 단계
이 장에서는 Spring Security의 기본 개념을 간단한 프로젝트에 적용하는 과정을 다룹니다. Spring Boot의 기본 설정을 활용하면서 인증 및 권한 부여를 설계하고, 기본값을 사용자 정의해보는 단계까지 진행합니다.
주요 학습 단계는 다음과 같습니다:
- Spring Security와 Web 의존성만 포함된 프로젝트 생성 및 기본 동작 확인.
- 기본 설정을 재정의하여 사용자 정의 사용자 계정 추가.
- 기본적으로 모든 엔드포인트가 인증 요구 대상임을 확인하고, 이를 사용자 지정.
- 다양한 아키텍처 스타일을 적용하며 최적의 구성 방법 탐색.
이 과정을 통해 Spring Security의 기본 개념부터 실무 적용까지 점진적으로 이해할 수 있습니다.
2.1 첫 번째 프로젝트 시작하기
이 섹션에서는 간단한 Spring Security 프로젝트를 생성하여 기본 인증과 권한 부여를 경험해 봅니다. 이번 예제는 HTTP Basic Authentication을 활용하여 REST 엔드포인트를 보호하는 방법을 보여줍니다.
프로젝트 설정 및 종속성 추가
작은 웹 애플리케이션을 생성하고 필요한 종속성을 추가합니다. 이 애플리케이션은 REST 엔드포인트 /hello를 노출하며, HTTP 요청 헤더에서 제공된 자격 증명(사용자 이름 및 비밀번호)을 통해 인증합니다.
필수 종속성:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
위 종속성들은 pom.xml에 추가해야 합니다. Spring Boot는 이 설정을 기반으로 기본적인 Spring Security 구성을 자동 적용합니다.
REST 엔드포인트 생성
다음은 HelloController 클래스의 정의입니다. 이 클래스는 /hello 경로를 처리하는 엔드포인트를 제공합니다.
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello!";
}
}
- @RestController: 스프링 컨텍스트에 빈을 등록하고 REST 컨트롤러로 사용됨을 명시.
- @GetMapping("/hello"): /hello 경로의 GET 요청을 처리하도록 메서드를 매핑.
애플리케이션 실행
애플리케이션을 실행하면 콘솔에 기본 보안 비밀번호가 출력됩니다.
Using generated security password: a753068f-6697-4535-88bb-3fdab495d9b0
테스트하기
- 인증 없이 호출응답:인증 정보가 없기 때문에 HTTP 401 상태 코드가 반환됩니다.
- { "status": 401, "error": "Unauthorized", "message": "Unauthorized", "path": "/hello" }
- curl http://localhost:8080/hello
- 올바른 인증 정보로 호출
기본 사용자 이름(user)과 콘솔에 출력된 비밀번호를 사용합니다.응답:요청이 성공하며, 컨트롤러에서 정의한 응답 메시지가 반환됩니다. - Hello!
- curl -u user:a753068f-6697-4535-88bb-3fdab495d9b0 http://localhost:8080/hello
HTTP 상태 코드
- 401 Unauthorized: 인증 정보가 없거나 잘못된 경우.
- 403 Forbidden: 인증은 성공했으나 권한이 부족한 경우.
이 예제를 통해 Spring Security의 기본 인증 메커니즘과 기본 구성 요소를 이해할 수 있습니다. 이제, 추가 사용자 정의를 통해 보다 복잡한 보안 시나리오로 확장할 수 있습니다.
HTTP BASIC 인증을 사용하여 엔드포인트 호출하기
Spring Security로 보호된 엔드포인트에 HTTP Basic 인증을 통해 접근하는 과정을 단계별로 살펴보겠습니다.
1. cURL로 간단히 호출하기 (-u 플래그 사용)
-u 플래그를 사용하면 사용자 이름과 비밀번호를 쉽게 전달할 수 있습니다.
curl -u user:a753068f-6697-4535-88bb-3fdab495d9b0 http://localhost:8080/hello
결과:
Hello!
이 방식은 간단하지만, 내부적으로 어떤 일이 벌어지는지 이해하기 위해 Authorization 헤더를 직접 구성해 보겠습니다.
2. Authorization 헤더 생성 (Base64 인코딩 사용)
a. 사용자 이름과 비밀번호를 Base64로 인코딩하기
<username>:<password> 형식의 문자열을 Base64로 인코딩합니다. 리눅스 또는 Git Bash에서 다음 명령어를 사용하세요:
echo -n user:a753068f-6697-4535-88bb-3fdab495d9b0 | base64
출력 예시:
dXNlcjphNzUzMDY4Zi02Njk3LTQ1MzUtODhiYi0zZmRhYjQ5NWQ5YjA=
b. Authorization 헤더를 수동으로 구성하기
Base64로 인코딩된 문자열을 Authorization 헤더에 포함하여 HTTP 요청을 생성합니다.
curl -H "Authorization: Basic dXNlcjphNzUzMDY4Zi02Njk3LTQ1MzUtODhiYi0zZmRhYjQ5NWQ5YjA=" http://localhost:8080/hello
c. 생성된 HTTP 요청 헤더 예시
GET /hello HTTP/1.1
Host: localhost:8080
Authorization: Basic dXNlcjphNzUzMDY4Zi02Njk3LTQ1MzUtODhiYi0zZmRhYjQ5NWQ5YjA=
User-Agent: curl/7.68.0
Accept: */*
응답:
Hello!
3. Default Security Configuration의 역할
현재 사용 중인 디폴트 보안 구성은 기본적으로 인증만 확인합니다.
- 장점:
기본 보안이 올바르게 동작하는지 확인하는 데 적합. - 제한점:
- Default 사용자 이름과 자동 생성된 비밀번호를 사용하는 것은 프로덕션 환경에 적합하지 않음.
- 권한 관리를 포함한 더 구체적인 설정이 필요함.
4. 다음 단계: 커스터마이징
기본 구성을 확인한 후, 다음으로는 Spring Boot가 자동으로 구성하는 Spring Security 설정을 구체적으로 이해하고 이를 프로젝트 요구 사항에 맞게 조정합니다.
앞으로 다룰 내용:
- 사용자 정의 인증과 권한 부여.
- 기본 사용자 및 비밀번호 대신 사용자 정의 데이터베이스 또는 인메모리 사용자 설정.
- 엔드포인트별 세부 보안 규칙 정의.
이번 간단한 예제를 통해 Spring Security의 기본 기능을 확인했으며, 다음 단계에서는 이를 발전시켜 실무적인 보안 요구사항을 충족시킬 수 있습니다.
2.2 Spring Security 클래스 설계의 큰 그림
Spring Security의 인증 및 권한 부여 아키텍처는 다양한 컴포넌트가 상호작용하여 보안 기능을 제공합니다. 이 섹션에서는 아키텍처의 주요 컴포넌트와 그 역할, 그리고 인증 흐름을 간략히 살펴보겠습니다.
Spring Security 아키텍처의 주요 컴포넌트
- Authentication Filter
- 클라이언트 요청을 가로채 인증 정보를 처리.
- 인증 요청을 Authentication Manager로 위임.
- 성공적인 인증 후 Security Context에 인증 정보를 저장.
- Authentication Manager
- 인증 과정을 조율.
- 하나 이상의 Authentication Provider를 통해 인증 수행.
- Authentication Provider
- 실제 인증 로직을 정의.
- UserDetailsService 및 PasswordEncoder를 사용해 사용자 인증 처리.
- UserDetailsService
- 사용자 정보를 로드하는 책임.
- Spring Security가 인증을 위해 사용자 정보를 조회.
- 기본적으로 메모리에 사용자 데이터를 저장하며, 이는 프로덕션 수준에는 적합하지 않음.
- PasswordEncoder
- 비밀번호를 안전하게 관리.
- 비밀번호를 인코딩하고, 저장된 값과 비교해 검증.
- Security Context
- 인증 후 인증 데이터를 보관.
- 인증된 사용자의 정보를 애플리케이션의 다른 부분에서 접근 가능하도록 유지.
기본 구현 및 동작 방식
- Spring Boot는 기본적으로 HTTP Basic 인증을 사용.
- 애플리케이션이 시작되면, 메모리에 기본 사용자 계정을 생성 (user라는 사용자 이름과 UUID 기반 랜덤 비밀번호).
- 인증 요청 시, Base64로 인코딩된 사용자 이름과 비밀번호를 HTTP 헤더로 전송.
- 기본 구현은 사용자 정보를 메모리에만 저장하며, 비밀번호는 평문으로 처리.
UserDetailsService와 PasswordEncoder
- UserDetailsService
- 사용자 정보를 로드하는 인터페이스.
- 기본적으로 메모리에 사용자 데이터를 저장하지만, 커스터마이징 가능 (예: 데이터베이스 연동).
- PasswordEncoder
- 비밀번호를 안전하게 관리하는 도구.
- 기본적으로 평문 비밀번호를 사용하지만, BCrypt 같은 해싱 알고리즘 사용을 권장.
HTTP Basic 인증
- HTTP Authorization 헤더를 통해 Basic <Base64-encoded-credentials> 형태로 사용자 정보를 전달.
- 인증 요청 예시:
curl -u user:<random-password> http://localhost:8080/hello
- 기본 제공 설정은 테스트와 학습에 적합하지만, 프로덕션 환경에서는 HTTPS와 더 강력한 인증 방식이 필요.
HTTPS를 통한 보안 강화
- HTTPS는 데이터를 암호화하여 전송 중 자격 증명 도난을 방지.
- 자체 서명된 인증서 생성 및 구성:
- OpenSSL을 사용해 인증서 생성:
openssl req -newkey rsa:2048 -x509 -keyout key.pem -out cert.pem -days 365 openssl pkcs12 -export -in cert.pem -inkey key.pem -out certificate.p12 -name "certificate"
- application.properties에 HTTPS 설정 추가:
server.ssl.key-store-type=PKCS12 server.ssl.key-store=classpath:certificate.p12 server.ssl.key-store-password=<password>
- OpenSSL을 사용해 인증서 생성:
- HTTPS 엔드포인트 호출:
- curl -k -u user:<random-password> https://localhost:8080/hello
추가 구성 요소와 주의 사항
- 엔드포인트 보안:
- Spring Security는 모든 엔드포인트를 기본적으로 보호.
- 특정 엔드포인트만 허용하거나 보안을 비활성화하려면 SecurityFilterChain을 커스터마이징.
- 다중 사용자 계정:
- application.yml 또는 UserDetailsService 커스터마이징으로 사용자 계정 관리.
- 실무에서의 권장사항:
- HTTPS 사용 필수.
- 인증 정보는 평문으로 저장하지 말 것.
- 적절한 권한 부여와 세부 보안 설정 필요.
Spring Security는 유연하고 강력한 보안 아키텍처를 제공하며, 각 컴포넌트를 적절히 조정해 애플리케이션 요구사항에 맞게 설정할 수 있습니다.
2.3 기본 구성 재정의
이 섹션에서는 Spring Security에서 제공하는 기본 컴포넌트를 어떻게 재정의하고, 애플리케이션에 적합한 보안을 구성할 수 있는지 알아봅니다. 기본 구현은 빠르게 보안을 설정하는 데 유용하지만, 실제 애플리케이션에서는 요구사항에 따라 재정의가 필요합니다.
Spring Security 구성 방식
Spring Security는 높은 유연성을 제공하며, 보안 구성을 여러 방식으로 정의할 수 있습니다. 하지만 이 유연성이 코드의 유지 보수성과 이해도를 낮출 수 있으므로 일관된 스타일을 유지하는 것이 중요합니다.
- Bean 정의를 통한 구성
- Spring Context에서 Bean을 정의해 보안 컴포넌트를 재정의합니다.
- 예: @Bean 어노테이션을 사용해 UserDetailsService와 PasswordEncoder를 명시적으로 등록.
- 메서드 재정의
- 특정 설정을 사용자화하기 위해 Spring Security의 구성 메서드를 재정의합니다.
- 예: configure(HttpSecurity http)를 재정의해 보안 규칙 설정.
UserDetailsService 재정의
기본 UserDetailsService는 메모리 내에서 사용자 데이터를 관리하지만, 실제 애플리케이션에서는 데이터베이스 또는 외부 서비스와 연동해야 합니다. 이를 위해 사용자 정의 UserDetailsService를 정의할 수 있습니다.
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(
User.withUsername("user")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build(),
User.withUsername("admin")
.password(passwordEncoder().encode("adminpass"))
.roles("ADMIN")
.build()
);
}
- InMemoryUserDetailsManager: 메모리 내 사용자 관리. 학습과 테스트에 적합.
- 프로덕션 환경에서는 데이터베이스 기반 JdbcUserDetailsManager를 사용할 수도 있음.
PasswordEncoder 재정의
기본 PasswordEncoder는 평문 비밀번호를 사용하지만, 해싱을 통해 비밀번호를 안전하게 관리해야 합니다. BCryptPasswordEncoder를 사용하는 것이 일반적입니다.
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
- BCryptPasswordEncoder: 비밀번호를 해싱하여 저장. 보안성이 높고 추천되는 방식.
- Spring Security는 다른 구현도 제공 (예: Pbkdf2PasswordEncoder, Argon2PasswordEncoder).
보안 규칙 설정
보안 규칙을 커스터마이징하려면 SecurityFilterChain 또는 WebSecurityConfigurerAdapter를 사용해 설정을 정의합니다. Spring Boot 2.7+에서는 SecurityFilterChain을 사용하는 방식이 권장됩니다.
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(auth -> auth
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
.anyRequest().authenticated()
)
.formLogin()
.and()
.httpBasic();
return http.build();
}
- authorizeRequests: 각 경로에 대한 접근 권한 정의.
- formLogin: 폼 기반 로그인 활성화.
- httpBasic: HTTP Basic 인증 활성화.
중요 고려사항
- 구성 방식의 일관성 유지
- 프로젝트 내에서 Bean 정의와 메서드 재정의 방식을 혼용하지 않는 것이 좋습니다.
- 코드 가독성과 유지 보수성을 위해 한 가지 스타일로 통일.
- 보안 설정의 테스트 및 검증
- 기본 설정을 변경할 때 예상치 못한 보안 취약점이 생기지 않도록 철저히 테스트.
- 프로덕션 수준 보안 구현
- 데이터는 반드시 암호화된 채널(HTTPS)로 전송.
- 강력한 암호화 알고리즘을 사용한 비밀번호 관리 필수.
결론
Spring Security는 기본값을 제공하여 빠르게 애플리케이션을 보호하지만, 실제 애플리케이션에서는 요구사항에 맞는 커스터마이징이 필수입니다. UserDetailsService와 PasswordEncoder를 올바르게 재정의하고, 보안 규칙을 체계적으로 설정하면 더욱 안전하고 확장 가능한 보안 구조를 구현할 수 있습니다.
2.3.1 사용자 세부 정보 관리 커스터마이징
이 섹션에서는 Spring Security의 UserDetailsService 컴포넌트를 재정의하여 사용자 인증 정보를 관리하는 방법을 다룹니다. 이를 통해 Spring Boot의 기본 구성을 대체하고, InMemoryUserDetailsManager 구현을 사용하여 애플리케이션에서 자체 관리하는 사용자 인증 자격 증명을 설정합니다.
1. UserDetailsService 재정의
Spring Security는 기본적으로 UserDetailsService를 사용하여 사용자 인증 데이터를 관리합니다. 우리는 다음과 같은 구성 클래스를 정의해 InMemoryUserDetailsManager를 빈으로 등록할 수 있습니다.
Listing 2.3: 기본 UserDetailsService 빈 구성
@Configuration
public class ProjectConfig {
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(); // 기본 UserDetailsService를 재정의
}
}
- @Configuration: 클래스가 구성 클래스로 사용됨을 Spring에 알립니다.
- @Bean: 해당 메서드가 반환하는 객체를 Spring Context에 빈으로 등록합니다.
결과:
Spring Boot의 자동 생성 비밀번호는 더 이상 표시되지 않으며, 정의된 UserDetailsService를 사용합니다. 그러나 애플리케이션이 동작하려면 추가적인 설정이 필요합니다.
2. 문제 해결
구성을 수정한 후 두 가지 주요 문제가 발생합니다:
- 사용자가 없음
- 인증에 필요한 사용자 데이터가 없습니다.
- PasswordEncoder가 없음
- 비밀번호 매칭 과정에서 암호화된 비밀번호와 비교할 수 없습니다.
이 문제를 해결하기 위해 다음 단계를 수행합니다.
3. 사용자 추가 및 구성
Spring Security는 사용자 정보를 관리하기 위해 UserDetails 객체를 사용합니다. 이를 생성하고 InMemoryUserDetailsManager에 추가하려면 다음과 같이 작성합니다.
Listing 2.4: 사용자 추가 코드
@Configuration
public class ProjectConfig {
@Bean
public UserDetailsService userDetailsService() {
var user = User.withUsername("john") // 사용자 이름 설정
.password("12345") // 비밀번호 설정
.authorities("read") // 권한 설정
.build(); // UserDetails 객체 생성
return new InMemoryUserDetailsManager(user); // 사용자 추가
}
}
- User.builder(): 사용자 객체를 쉽게 생성할 수 있는 빌더 메서드.
- authorities(String... roles): 사용자의 권한 또는 역할을 정의.
이 예제에서는 InMemoryUserDetailsManager에 한 명의 사용자를 추가했지만, 여러 사용자를 추가하려면 User.withUsername() 메서드를 반복 호출하여 인스턴스를 생성하면 됩니다.
4. PasswordEncoder 정의
Spring Security는 비밀번호를 암호화된 형태로 저장하고 비교합니다. 그러나 기본 구성에서는 암호화를 처리하는 PasswordEncoder가 없습니다. 이를 수동으로 추가해야 합니다.
Listing 2.5: PasswordEncoder 추가
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance(); // 암호화를 적용하지 않는 엔코더
}
- NoOpPasswordEncoder: 비밀번호를 평문으로 처리하며 학습 목적으로만 사용.
- 경고: NoOpPasswordEncoder는 보안에 취약하므로 프로덕션 환경에서는 사용하지 않습니다.
5. 전체 구성 클래스
다음은 수정된 전체 구성 클래스입니다.
@Configuration
public class ProjectConfig {
@Bean
public UserDetailsService userDetailsService() {
var user = User.withUsername("john")
.password("12345")
.authorities("read")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
6. 테스트
이제 설정이 완료되었으므로 애플리케이션이 정상적으로 작동하는지 테스트할 수 있습니다. 다음은 curl 명령어를 사용한 간단한 인증 테스트입니다.
curl -u john:12345 http://localhost:8080/hello
결과:
Hello!라는 응답이 반환됩니다.
주의 사항
- 프로덕션 환경에서는 안전한 암호화 사용
- 비밀번호는 반드시 해싱 알고리즘을 사용해 저장해야 합니다.
- 예: BCryptPasswordEncoder.
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
- InMemoryUserDetailsManager는 테스트 용도로만 사용
- 실제 애플리케이션에서는 데이터베이스나 외부 서비스와 연동하는 구현을 사용합니다.
- 예: JdbcUserDetailsManager 또는 사용자 정의 UserDetailsService.
- 일관된 구성 관리
- 구성 파일과 클래스 정의 간의 일관성을 유지하여 유지 보수성을 높입니다.
이 섹션에서는 Spring Security의 기본 UserDetailsService를 대체하고, 사용자 데이터를 정의하며, 인증을 지원하는 PasswordEncoder를 추가하는 방법을 살펴보았습니다. 이는 애플리케이션 보안을 커스터마이징하는 첫 번째 단계로, 이후 단계에서는 더 복잡한 보안 요구 사항을 처리하게 될 것입니다.
2.3.2 엔드포인트 레벨에서의 권한 적용
2.3.1절에서 사용자 관리를 재구성한 후, 이제 애플리케이션의 엔드포인트에 대해 권한을 구성하는 방법을 살펴보겠습니다. Spring Security를 사용하면 SecurityFilterChain 빈을 정의하여 인증 및 권한 부여를 커스터마이징할 수 있습니다.
1. 기본 SecurityFilterChain 정의
Spring Security는 요청의 인증과 권한 부여를 처리하기 위해 HttpSecurity 객체를 제공합니다. 이를 사용해 디폴트 구성을 재정의할 수 있습니다.
Listing 2.6: SecurityFilterChain 빈 정의
@Configuration
public class ProjectConfig {
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
return http.build(); // 기본 SecurityFilterChain 반환
}
}
2. 기본 구성의 커스터마이징
기본적으로 Spring Security는 모든 요청에 대해 인증을 요구하고, HTTP Basic 인증을 사용합니다. 이를 명시적으로 구성하려면 아래와 같이 작성합니다.
Listing 2.7: HttpSecurity를 사용한 기본 구성 변경
@Configuration
public class ProjectConfig {
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults()); // HTTP Basic 인증 활성화
http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated()); // 모든 요청에 인증 요구
return http.build();
}
}
- httpBasic(Customizer.withDefaults())
HTTP Basic 인증 방식을 활성화합니다. - authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
모든 요청에 대해 인증을 요구합니다.
3. 인증 없이 모든 요청 허용
모든 엔드포인트에 자격 증명 없이 접근 가능하도록 구성하려면, permitAll() 메서드를 사용합니다.
Listing 2.8: 인증 없이 요청 허용
@Configuration
public class ProjectConfig {
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults()); // HTTP Basic 인증 활성화
http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()); // 모든 요청 허용
return http.build();
}
}
- anyRequest().permitAll()
인증 없이 모든 요청을 허용합니다.
결과:
자격 증명이 없어도 /hello 엔드포인트에 접근 가능하며, 다음과 같은 응답을 확인할 수 있습니다:
Hello!
4. 주요 구성 메서드
- httpBasic()
- 애플리케이션이 HTTP Basic 인증을 사용할 수 있도록 설정합니다.
- authorizeHttpRequests()
- 특정 요청에 대한 권한 부여 규칙을 정의합니다.
- 예: 인증이 필요한 요청(authenticated()) 또는 인증 없이 허용(permitAll()).
5. Customizer 객체의 역할
Customizer는 특정 요소를 구성하는 함수형 인터페이스입니다. **Customizer.withDefaults()**는 기본 설정을 적용하며, 복잡한 구성을 위해 람다나 별도의 클래스를 사용해 정의할 수도 있습니다.
Customizer 인터페이스:
@FunctionalInterface
public interface Customizer<T> {
void customize(T t);
static <T> Customizer<T> withDefaults() {
return (t) -> {};
}
}
이전 Spring Security 버전에서는 Chaining 방식을 사용했지만, Customizer를 사용하면 더 유연한 구성이 가능합니다.
예:
http.authorizeHttpRequests()
.anyRequest().authenticated();
위 코드는 Customizer 없이 구성하며, 간단하지만 복잡한 앱에서는 덜 유연합니다.
6. Spring Security 최신 구성 방식
이전에는 WebSecurityConfigurerAdapter를 상속해 보안 구성을 작성했습니다. 하지만 Spring Security는 최신 버전에서 이 방식을 사용하지 않으며, SecurityFilterChain을 사용하는 것을 권장합니다.
이전 방식:
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic();
http.authorizeRequests().anyRequest().authenticated();
}
}
현재 권장 방식:
@Configuration
public class ProjectConfig {
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults());
http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
return http.build();
}
}
결론
- Spring Security의 HttpSecurity 객체를 사용해 엔드포인트 레벨에서 인증과 권한 부여를 구성할 수 있습니다.
- 최신 Spring Security에서는 Customizer와 SecurityFilterChain을 활용해 더 유연하고 명확한 구성이 가능합니다.
- 인증 및 권한 부여 규칙은 애플리케이션의 요구에 따라 적절히 변경하며, 복잡한 구성을 위해 별도의 클래스로 분리하는 것이 좋습니다.
2.3.3 다양한 방식으로 구성하기
Spring Security는 다양한 방법으로 구성을 할 수 있는 유연성을 제공합니다. 이 섹션에서는 UserDetailsService와 PasswordEncoder를 설정하는 대체 방법을 살펴보겠습니다. 이를 이해하는 것은 다양한 예제에서 적용된 방법을 인식하고, 자신의 애플리케이션에서 이를 적절히 활용하는 데 도움이 됩니다.
1. UserDetailsService와 PasswordEncoder 설정
첫 번째 프로젝트에서는 **UserDetailsService**와 **PasswordEncoder**를 Spring 컨텍스트에 빈으로 추가하여 구성할 수 있었습니다. 또 다른 방법은 SecurityFilterChain 빈 내에서 이들을 설정하는 것입니다.
Listing 2.9: SecurityFilterChain 빈을 사용하여 UserDetailsService 설정
@Configuration
public class ProjectConfig {
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults()); // HTTP Basic 인증 설정
http.authorizeHttpRequests(
c -> c.anyRequest().authenticated() // 모든 요청에 인증 요구
);
var user = User.withUsername("john") // A: 사용자 정의
.password("12345")
.authorities("read")
.build();
var userDetailsService = new InMemoryUserDetailsManager(user); // B: InMemoryUserDetailsManager 사용
http.userDetailsService(userDetailsService); // C: userDetailsService 설정
return http.build();
}
}
- A: 사용자 정의 (username: "john", password: "12345", 권한: "read")
- B: InMemoryUserDetailsManager를 사용하여 사용자 정보를 메모리에 저장
- C: HttpSecurity에서 userDetailsService() 메서드를 호출하여 UserDetailsService를 설정
이 예제는 SecurityFilterChain 빈 내에서 UserDetailsService를 로컬로 설정하는 방법을 보여줍니다.
Listing 2.10: 전체 구성 클래스 예제
@Configuration
public class ProjectConfig {
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults());
http.authorizeHttpRequests(c -> c.anyRequest().authenticated());
var user = User.withUsername("john") // A
.password("12345")
.authorities("read")
.build();
var userDetailsService = new InMemoryUserDetailsManager(user); // B
http.userDetailsService(userDetailsService); // C
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance(); // 비밀번호 인코더 설정
}
}
이 방법은 UserDetailsService를 SecurityFilterChain 내에서 설정하는 방법을 보여주며, 별도의 빈으로 설정할 수 있습니다.
2. AuthenticationProvider를 사용한 사용자 정의 인증 로직 구현
Spring Security의 유연성은 AuthenticationProvider를 사용하여 인증 로직을 사용자 정의할 수 있다는 점에서 더욱 강화됩니다. 기본 AuthenticationProvider는 UserDetailsService와 PasswordEncoder를 사용하여 사용자 인증을 수행하지만, 이를 오버라이드하여 더 복잡한 인증 로직을 구현할 수 있습니다.
Listing 2.11: AuthenticationProvider 인터페이스 구현
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 인증 로직 구현
}
@Override
public boolean supports(Class<?> authenticationType) {
// 인증 타입 지원 여부 체크
}
}
Listing 2.12: 인증 로직 구현
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName(); // A
String password = String.valueOf(authentication.getCredentials());
if ("john".equals(username) && "12345".equals(password)) { // B
return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
} else {
throw new AuthenticationCredentialsNotFoundException("Error!");
}
}
- A: authentication.getName()으로 사용자 이름을 가져옴
- B: 사용자 이름과 비밀번호를 비교하여 인증 성공 여부를 판단
이 로직에서는 UserDetailsService와 PasswordEncoder를 사용하지 않고 직접 사용자의 자격 증명을 확인합니다. 그러나 Spring Security의 기본 아키텍처에서는 이들을 사용하여 인증 로직을 분리하는 것이 더 좋습니다.
Listing 2.13: CustomAuthenticationProvider 전체 구현
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = String.valueOf(authentication.getCredentials());
if ("john".equals(username) && "12345".equals(password)) {
return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
} else {
throw new AuthenticationCredentialsNotFoundException("Error!");
}
}
@Override
public boolean supports(Class<?> authenticationType) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);
}
}
이제 이 사용자 정의 인증 제공자를 SecurityFilterChain에서 사용할 수 있습니다.
Listing 2.14: AuthenticationProvider를 SecurityFilterChain에 등록
@Configuration
public class ProjectConfig {
private final CustomAuthenticationProvider authenticationProvider;
public ProjectConfig(CustomAuthenticationProvider authenticationProvider) {
this.authenticationProvider = authenticationProvider;
}
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults());
http.authenticationProvider(authenticationProvider); // 사용자 정의 인증 제공자 등록
http.authorizeHttpRequests(c -> c.anyRequest().authenticated());
return http.build();
}
}
이 구성에서는 **CustomAuthenticationProvider**를 사용하여 인증 로직을 처리합니다. 이제 username이 "john"이고 password가 "12345"인 사용자만 인증될 수 있습니다.
결론
- **UserDetailsService**와 **PasswordEncoder**는 다양한 방법으로 설정할 수 있으며, 이를 SecurityFilterChain 빈 내에서 설정하는 방법도 가능합니다.
- AuthenticationProvider를 사용하면 더 복잡한 인증 로직을 구현할 수 있으며, 이를 통해 Spring Security의 기본 동작을 사용자 정의할 수 있습니다.
- 인증 로직을 구현할 때, Spring Security의 아키텍처를 그대로 따르는 것이 좋지만, 필요한 경우 이를 재정의하여 더욱 맞춤화된 로직을 제공할 수 있습니다.
2.3.5 여러 구성 클래스를 사용하기
구성 클래스를 여러 개로 분리하는 것은 Spring Security를 사용할 때 좋은 실천 방법입니다. 애플리케이션의 구성이 복잡해질수록 책임을 명확히 분리하는 것이 중요하며, 이를 통해 코드 유지보수성을 높이고 읽기 쉬운 구조를 만들 수 있습니다. 프로덕션 수준의 애플리케이션에서는 하나의 구성 클래스에 모든 구성을 포함하는 대신 여러 클래스로 분리하여 관리하는 것이 유리합니다.
구성 클래스를 분리하는 이유
- 구성 책임 분리: 각 클래스는 하나의 책임만 가지며, 이는 단일 책임 원칙(Single Responsibility Principle)을 따릅니다. 예를 들어, 사용자 관리와 권한 부여는 서로 다른 책임이므로 두 클래스로 분리할 수 있습니다.
- 복잡한 구성을 다루기 용이: 애플리케이션 구성이 커질수록 한 클래스에 모든 설정을 담기보다는 각기 다른 영역에 맞는 클래스를 분리하여 구성하는 것이 더 효과적입니다.
예제: 사용자 관리와 권한 부여 구성을 분리하기
Listing 2.15: 사용자 관리 구성 클래스 정의
@Configuration
public class UserManagementConfig {
@Bean
public UserDetailsService userDetailsService() {
var userDetailsService = new InMemoryUserDetailsManager();
var user = User.withUsername("john")
.password("12345")
.authorities("read")
.build();
userDetailsService.createUser(user);
return userDetailsService;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance(); // 비밀번호 인코딩을 하지 않는 예시
}
}
- UserManagementConfig 클래스는 사용자 관리와 관련된 빈을 설정합니다. 여기서는 InMemoryUserDetailsManager를 사용하여 사용자 정보를 메모리에 저장하고, NoOpPasswordEncoder를 사용하여 평문 비밀번호를 처리합니다.
Listing 2.16: 권한 부여 관리 구성 클래스 정의
@Configuration
public class WebAuthorizationConfig {
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults());
http.authorizeHttpRequests(
c -> c.anyRequest().authenticated() // 모든 요청에 인증 요구
);
return http.build();
}
}
- WebAuthorizationConfig 클래스는 인증 및 권한 부여 규칙을 설정하는 빈을 포함합니다. HttpSecurity를 사용하여 인증 방식과 요청에 대한 권한을 설정하고 있습니다.
구성 클래스 분리의 이점
- 책임 분리: 사용자 관리와 권한 부여는 다른 기능이므로 이를 각기 다른 클래스로 분리하여 관리할 수 있습니다.
- 코드 유지보수성 향상: 복잡한 애플리케이션에서 구성을 효율적으로 관리할 수 있습니다. 각 구성 클래스는 특정 책임만을 가지므로 변경 시 다른 부분에 영향을 미치지 않습니다.
- 가독성 향상: 각 구성 클래스가 독립적으로 존재함으로써 코드가 더 직관적이고 쉽게 이해할 수 있습니다.
2.4 요약
- Spring Security 기본 구성: Spring Boot에서 Spring Security를 사용하면 기본적으로 몇 가지 기본적인 구성을 제공하며, 이를 통해 인증과 권한 부여를 설정할 수 있습니다.
- 구성 요소:
- UserDetailsService: 사용자 정보를 관리하는 서비스, 예시로 InMemoryUserDetailsManager가 제공됩니다.
- PasswordEncoder: 비밀번호 암호화를 담당하는 인터페이스로, NoOpPasswordEncoder는 암호화를 하지 않고 평문으로 처리하는 예시입니다.
- AuthenticationProvider: 사용자 정의 인증 로직을 구현할 수 있는 인터페이스입니다.
- 여러 구성 클래스를 사용하는 이유: 책임 분리를 통해 코드가 복잡해지더라도 관리하기 쉬운 구조로 만들 수 있습니다. 예를 들어, 사용자 관리와 권한 부여를 각기 다른 클래스로 나누어 구성할 수 있습니다.
- 구성 방법 선택: 다양한 구성 방법이 있지만, 애플리케이션에서는 하나의 접근 방식을 선택하고 이를 일관되게 사용하는 것이 중요합니다. 이렇게 하면 코드가 깔끔하고 이해하기 쉬워집니다.
'Spring Security in Action' 카테고리의 다른 글
8장 Configuring endpoint-level authorization: Applying restrictions (0) | 2024.12.01 |
---|---|
7장 Configuring endpoint-level authorization: Restricting access (0) | 2024.12.01 |
5장 A web app's security begins with filters (0) | 2024.12.01 |
4장 비밀번호 인코더 구현 및 작업 (0) | 2024.12.01 |
3장 Managing users (0) | 2024.12.01 |