2025-04-08T19:56:24

This commit is contained in:
2025-04-08 19:56:24 +09:00
parent a75a1dbd0f
commit eef061c1c9
100 changed files with 18639 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
아래는 "스프링 시큐리티" 책의 3장 일부에 해당하는 "인증과 인가 소개"와 "기본 인증 메커니즘"에 대한 내용입니다. 자연스럽고 명확한 설명을 목표로 작성했습니다.
---
### 3장. 스프링 시큐리티 기본 동작 이해
#### 3.1 인증(Authentication)과 인가(Authorization)
애플리케이션 보안을 다룰 때 가장 중요한 두 개념은 **인증(Authentication)**과 **인가(Authorization)**입니다. 이 두 가지는 보안 시스템의 핵심이며, 서로 밀접하게 연관되어 있지만 역할은 다릅니다.
**인증(Authentication)**은 "당신이 누구인지" 확인하는 과정입니다. 사용자가 시스템에 접근하려 할 때, 그들이 주장하는 신원을 검증합니다. 예를 들어, 사용자가 로그인 폼에 아이디와 비밀번호를 입력하면 시스템은 이를 데이터베이스나 다른 인증 제공자와 비교해 사용자가 맞는지 확인합니다. 인증이 성공하면 사용자는 시스템에 "로그인"한 상태가 됩니다.
**인가(Authorization)**는 "당신이 무엇을 할 수 있는지" 결정하는 과정입니다. 인증이 사용자의 신원을 확인했다면, 인가는 그 사용자가 접근할 수 있는 리소스나 수행할 수 있는 작업을 정의합니다. 예를 들어, 일반 사용자는 자신의 프로필만 수정할 수 있고, 관리자는 모든 사용자의 데이터를 삭제할 수 있는 권한을 가질 수 있습니다. 인가는 보통 역할(Role)이나 권한(Authority)을 기반으로 설정됩니다.
스프링 시큐리티는 이 두 과정을 효과적으로 관리하며, 복잡한 보안 요구사항을 간단한 설정으로 처리할 수 있게 도와줍니다. 인증과 인가가 제대로 작동해야 사용자는 안전하게 시스템을 사용할 수 있고, 개발자는 보안 위협으로부터 애플리케이션을 보호할 수 있습니다.
#### 3.2 기본 인증 메커니즘
스프링 시큐리티는 다양한 인증 방식을 지원하지만, 기본적으로 제공하는 인증 메커니즘은 **폼 기반 로그인(Form-based Authentication)**과 **HTTP 기본 인증(HTTP Basic Authentication)**입니다. 스프링부트 프로젝트에 스프링 시큐리티를 추가하면 별도의 설정 없이도 이 기본 인증이 즉시 동작합니다. 이를 이해하려면 먼저 기본 동작을 살펴보겠습니다.
##### 폼 기반 로그인 (Form-based Authentication)
스프링 시큐리티를 추가하면, 애플리케이션의 모든 엔드포인트는 기본적으로 보호됩니다. 보호된 리소스에 접근하려는 사용자는 스프링 시큐리티가 자동으로 제공하는 로그인 페이지로 이동합니다. 이 페이지는 간단한 HTML 폼으로, 사용자가 아이디와 비밀번호를 입력하면 이를 제출해 인증을 시도합니다.
기본 설정에서는 메모리에 저장된 사용자(예: `user`라는 이름과 무작위 비밀번호)를 사용하며, 이 정보는 애플리케이션 시작 시 콘솔에 출력됩니다. 인증이 성공하면 사용자는 원래 요청한 페이지로 이동하고, 실패하면 오류 메시지가 표시됩니다. 이 과정은 스프링 시큐리티의 **UsernamePasswordAuthenticationFilter**가 처리하며, 이후 자세히 다룰 예정입니다.
##### HTTP 기본 인증 (HTTP Basic Authentication)
또 다른 기본 인증 방식은 HTTP 기본 인증입니다. 이 방식은 브라우저가 제공하는 팝업 창을 통해 사용자 이름과 비밀번호를 입력받습니다. 사용자가 보호된 리소스에 접근하면, 서버는 `401 Unauthorized` 상태 코드와 함께 `WWW-Authenticate` 헤더를 반환합니다. 브라우저는 이를 감지해 인증 창을 띄우고, 입력된 자격 증명은 Base64로 인코딩되어 요청 헤더(`Authorization`)에 포함됩니다.
HTTP 기본 인증은 설정이 간단하고 REST API나 서버 간 통신에서 유용하지만, 사용자 경험이 제한적이고 비밀번호가 암호화되지 않은 상태로 전송될 수 있어 HTTPS와 함께 사용하는 것이 권장됩니다.
##### 기본 인증의 동작 원리
두 방식 모두 스프링 시큐리티의 **Security Filter Chain**을 통해 처리됩니다. 이 필터 체인은 요청을 가로채 사용자가 인증되었는지, 권한이 있는지 확인한 뒤 적절히 처리합니다. 기본 설정은 빠르게 테스트하거나 학습할 때 유용하지만, 실무에서는 데이터베이스 연동, OAuth2, JWT 등 더 강력한 인증 방식을 사용하게 될 것입니다.
이 장에서는 기본 인증 메커니즘을 기반으로 스프링 시큐리티의 동작을 이해하고, 이후 장에서 이를 커스터마이징하거나 확장하는 방법을 배워보겠습니다.
---
위 내용은 독자가 인증과 인가의 개념을 명확히 이해하고, 스프링 시큐리티의 기본 인증 방식을 쉽게 파악할 수 있도록 작성되었습니다. 추가로 다루고 싶은 세부 사항이나 예제가 있다면 말씀해 주세요!

View File

@@ -0,0 +1,58 @@
아래는 "스프링 시큐리티" 책의 3장에 포함될 "Security Filter Chain의 구조"와 "기본 로그인 페이지와 동작 원리"에 대한 내용입니다. 기술적인 깊이를 유지하면서도 초보자가 이해하기 쉽도록 설명했습니다.
---
### 3장. 스프링 시큐리티 기본 동작 이해
#### 3.3 Security Filter Chain의 구조
스프링 시큐리티의 핵심은 **Security Filter Chain**입니다. 이는 HTTP 요청을 처리하기 전에 보안 로직을 적용하는 일련의 필터들로 구성된 체인입니다. 스프링 시큐리티가 동작하는 방식은 이 필터 체인을 통해 요청을 가로채고, 인증 및 인가를 수행하며, 필요에 따라 요청을 차단하거나 허용하는 구조로 설계되었습니다.
##### Security Filter Chain이란?
Security Filter Chain은 서블릿 필터(Servlet Filter)의 확장된 형태로, 스프링의 `FilterChainProxy`라는 특별한 필터가 이를 관리합니다. 이 체인은 여러 개의 보안 필터로 이루어져 있으며, 각 필터는 특정 보안 작업을 담당합니다. 예를 들어, 사용자의 로그인 요청을 처리하거나, CSRF 공격을 방어하거나, 세션을 관리하는 등의 역할을 합니다.
##### 주요 필터의 역할
스프링 시큐리티는 기본적으로 여러 필터를 제공하며, 이들은 순서대로 실행됩니다. 몇 가지 중요한 필터를 소개하면 다음과 같습니다:
- **SecurityContextPersistenceFilter**: 요청 사이에 `SecurityContext`(인증 정보)를 유지합니다. 세션을 통해 사용자의 인증 상태를 저장하고 불러오는 역할을 합니다.
- **UsernamePasswordAuthenticationFilter**: 폼 기반 로그인을 처리합니다. 사용자가 제출한 아이디와 비밀번호를 받아 인증을 시도합니다.
- **BasicAuthenticationFilter**: HTTP 기본 인증을 처리합니다. 요청 헤더에서 자격 증명을 추출해 인증을 수행합니다.
- **FilterSecurityInterceptor**: 인가(Authorization)를 담당합니다. 요청된 리소스에 접근할 권한이 있는지 확인하고, 없으면 예외를 발생시킵니다.
##### 동작 흐름
1. 클라이언트가 HTTP 요청을 보내면, `FilterChainProxy`가 이를 가로챕니다.
2. 요청은 Security Filter Chain의 필터들을 순차적으로 통과합니다.
3. 각 필터는 자신의 역할에 따라 요청을 처리하거나 다음 필터로 넘깁니다.
4. 모든 필터를 통과하면 요청은 컨트롤러로 전달되고, 그렇지 않으면 보안 예외(예: 403 Forbidden)가 발생합니다.
필터의 순서와 동작은 설정에 따라 커스터마이징할 수 있으며, 이는 스프링 시큐리티의 유연성을 보여줍니다. 기본 설정에서는 이러한 필터들이 자동으로 구성되어 간단한 보안 요구사항을 충족하지만, 실무에서는 필요에 따라 필터를 추가하거나 순서를 조정할 수 있습니다.
#### 3.4 기본 로그인 페이지와 동작 원리
스프링 시큐리티를 스프링부트 프로젝트에 추가하면, 별도의 설정 없이도 모든 엔드포인트가 보호되고, 기본 로그인 페이지가 활성화됩니다. 이 기본 로그인 페이지는 스프링 시큐리티의 간편함을 보여주는 좋은 예이며, 그 동작 원리를 이해하면 커스터마이징의 기초를 다질 수 있습니다.
##### 기본 로그인 페이지의 모습
프로젝트에 스프링 시큐리티 의존성을 추가하고 애플리케이션을 실행한 뒤, 브라우저에서 어떤 URL(예: `http://localhost:8080`)에 접속하면 자동으로 `/login` 경로로 리다이렉트됩니다. 이 경로에는 스프링 시큐리티가 생성한 기본 로그인 폼이 나타납니다. 이 폼은 다음과 같은 요소를 포함합니다:
- 사용자 이름 입력 필드 (기본값: `username`)
- 비밀번호 입력 필드 (기본값: `password`)
- "로그인" 버튼
기본 설정에서는 메모리에 저장된 사용자(`user`)와 애플리케이션 시작 시 생성된 무작위 비밀번호가 사용됩니다. 이 비밀번호는 콘솔 로그에 출력되며, 이를 입력하면 인증이 완료됩니다.
##### 동작 원리
기본 로그인 페이지의 동작은 Security Filter Chain과 밀접하게 연관되어 있습니다. 아래는 그 과정을 단계별로 설명한 것입니다:
1. **요청 차단**: 사용자가 보호된 리소스(예: `/home`)에 접근하려 하면, `FilterSecurityInterceptor`가 이를 감지하고 인증 여부를 확인합니다. 인증되지 않았다면 `/login`으로 리다이렉트됩니다.
2. **로그인 페이지 표시**: `/login` 경로는 스프링 시큐리티가 내부적으로 제공하는 기본 컨트롤러에 의해 렌더링됩니다. 이 페이지는 간단한 HTML로 구성되어 있습니다.
3. **인증 요청 처리**: 사용자가 폼에 아이디와 비밀번호를 입력하고 "로그인" 버튼을 누르면, POST 요청(`/login`)이 발생합니다. 이 요청은 `UsernamePasswordAuthenticationFilter`가 가로채 처리합니다.
- 입력된 자격 증명은 `AuthenticationManager`로 전달되어 인증이 시도됩니다.
- 기본 설정에서는 `InMemoryUserDetailsManager`가 사용자 정보를 확인합니다.
4. **성공 또는 실패 처리**:
- 인증 성공 시, `SecurityContext`에 인증 정보가 저장되고, 사용자는 원래 요청한 페이지(예: `/home`)로 리다이렉트됩니다.
- 실패 시, `/login?error`로 리다이렉트되며 오류 메시지가 표시됩니다.
5. **세션 관리**: 인증이 완료되면 세션이 생성되고, `SecurityContextPersistenceFilter`가 이를 유지합니다.
##### 한계와 확장
기본 로그인 페이지는 빠르게 테스트하거나 프로토타입을 만들 때 유용하지만, 실무에서는 디자인과 기능을 커스터마이징해야 합니다. 예를 들어, 데이터베이스에서 사용자 정보를 가져오거나, 소셜 로그인을 추가하는 방식으로 확장할 수 있습니다. 이후 장에서 이러한 커스터마이징 방법을 자세히 다룰 것입니다.
---
위 내용은 Security Filter Chain의 구조와 기본 로그인 페이지의 동작을 체계적으로 설명하며, 스프링 시큐리티의 내부 동작을 이해하는 데 초점을 맞췄습니다. 추가적인 예제 코드나 다이어그램이 필요하다면 말씀해 주세요!

View File

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

View File

@@ -0,0 +1,111 @@
아래는 "스프링 시큐리티" 책의 5장에 포함될 "역할(Role)과 권한(Authority)의 차이"와 "URL 기반 접근 제어"에 대한 내용입니다. 개념을 명확히 설명하고 실습 가능한 예제를 포함해 실무에서의 활용성을 높였습니다.
---
### 5장. 인가와 권한 관리
#### 5.1 역할(Role)과 권한(Authority)의 차이
스프링 시큐리티에서 **역할(Role)**과 **권한(Authority)**은 사용자가 시스템에서 무엇을 할 수 있는지를 정의하는 핵심 개념입니다. 두 용어는 종종 혼용되지만, 미묘한 차이가 있으며 이를 이해하면 인가를 더 세밀하게 관리할 수 있습니다.
##### 역할(Role)이란?
역할은 사용자가 속한 그룹이나 직책을 나타내는 상위 수준의 개념입니다. 예를 들어, "관리자(ADMIN)", "일반 사용자(USER)", "게스트(GUEST)" 같은 이름으로 정의됩니다. 역할은 보통 사용자의 주요 책임이나 접근 범위를 나타내며, 스프링 시큐리티에서는 `ROLE_` 접두사를 붙여 표현합니다(예: `ROLE_ADMIN`).
역할은 단순하고 직관적이어서 소규모 애플리케이션이나 기본적인 권한 관리에 적합합니다. 예를 들어, 관리자 역할은 모든 기능을 사용할 수 있고, 일반 사용자는 제한된 기능만 접근할 수 있도록 설정할 수 있습니다.
##### 권한(Authority)이란?
권한은 더 세분화된 접근 제어 단위로, 특정 작업이나 리소스에 대한 권한을 나타냅니다. 예를 들어, "게시글 작성(WRITE_POST)", "댓글 삭제(DELETE_COMMENT)", "사용자 관리(MANAGE_USERS)" 같은 구체적인 권한을 정의할 수 있습니다. 권한은 `ROLE_` 접두사 없이도 사용 가능하며, 역할보다 유연하게 설계할 수 있습니다.
권한은 역할에 포함될 수 있으며, 복잡한 시스템에서 세밀한 접근 제어를 구현할 때 유용합니다. 예를 들어, `ROLE_USER``READ_POST``WRITE_POST` 권한을 가질 수 있고, `ROLE_ADMIN`은 추가로 `MANAGE_USERS` 권한을 가질 수 있습니다.
##### 차이점과 활용
- **수준**: 역할은 상위 수준(추상적), 권한은 하위 수준(구체적).
- **표기**: 역할은 `ROLE_` 접두사를 기본으로 사용, 권한은 자유롭게 정의.
- **복잡성**: 역할은 단순한 분류에 적합, 권한은 세부적인 제어에 유리.
스프링 시큐리티에서는 `GrantedAuthority` 인터페이스를 통해 두 개념을 통합적으로 관리하며, 설정에 따라 역할과 권한을 혼합해 사용할 수 있습니다. 예를 들어, `ROLE_ADMIN` 역할을 가진 사용자가 `MANAGE_USERS` 권한을 추가로 가질 수 있습니다.
#### 5.2 URL 기반 접근 제어
URL 기반 접근 제어는 스프링 시큐리티에서 가장 흔히 사용되는 인가 방식으로, 요청 URL 패턴에 따라 사용자의 접근을 허용하거나 차단합니다. 이를 통해 애플리케이션의 각 엔드포인트를 역할이나 권한에 따라 보호할 수 있습니다.
##### 기본 설정
스프링 시큐리티는 `HttpSecurity`를 사용해 URL 기반 접근 제어를 설정합니다. 기본적으로 모든 요청을 인증된 사용자만 접근 가능하도록 설정할 수 있습니다.
```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("user")
.password("{noop}password")
.roles("USER")
.build();
var admin = User.withUsername("admin")
.password("{noop}adminpass")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN") // /admin 하위는 ADMIN만 접근
.requestMatchers("/user/**").hasRole("USER") // /user 하위는 USER만 접근
.requestMatchers("/public/**").permitAll() // /public은 누구나 접근 가능
.anyRequest().authenticated() // 나머지 요청은 인증 필요
)
.formLogin(); // 기본 로그인 폼 활성화
return http.build();
}
}
```
##### 주요 메서드 설명
- **`requestMatchers()`**: 특정 URL 패턴을 지정합니다. 와일드카드(`**`, `*`)를 사용해 여러 경로를 매핑할 수 있습니다.
- **`hasRole()`**: 주어진 역할이 있는 사용자만 접근을 허용합니다. `ROLE_` 접두사는 자동으로 추가됩니다(예: `hasRole("ADMIN")``ROLE_ADMIN`).
- **`permitAll()`**: 인증 없이 누구나 접근 가능하도록 설정합니다.
- **`anyRequest()`**: 위에서 정의되지 않은 모든 요청에 대해 적용할 규칙을 지정합니다.
- **`authenticated()`**: 인증된 사용자만 접근 가능하도록 설정합니다.
##### 동작 원리
1. 사용자가 `/admin/dashboard`에 접근하면, `FilterSecurityInterceptor`가 요청을 가로챕니다.
2. `SecurityConfig`에 정의된 규칙을 확인해 사용자가 `ROLE_ADMIN` 역할을 가졌는지 검사합니다.
3. 역할이 일치하면 요청이 통과되고, 그렇지 않으면 `403 Forbidden` 오류가 발생하거나 로그인 페이지로 리다이렉트됩니다.
##### 권한 기반 제어
역할 대신 권한으로 제어하려면 `hasAuthority()`를 사용합니다:
```java
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/posts/write").hasAuthority("WRITE_POST")
.requestMatchers("/posts/delete").hasAuthority("DELETE_POST")
.anyRequest().authenticated()
);
```
이 경우, `UserDetailsService`에서 사용자에게 `WRITE_POST``DELETE_POST` 같은 권한을 부여해야 합니다.
##### 추가 설정
- **순서 중요성**: 규칙은 위에서 아래로 평가되므로, 더 구체적인 규칙을 먼저 작성해야 합니다. 예를 들어, `/public/**``anyRequest()`보다 뒤에 두면 적용되지 않습니다.
- **로그인 페이지 접근 허용**: 기본 로그인 페이지(`/login`)는 `permitAll()`로 열어둬야 인증되지 않은 사용자도 접근할 수 있습니다.
- **커스터마이징**: `.access()` 메서드를 사용하면 더 복잡한 조건(예: IP 주소나 시간 기반)을 추가할 수 있습니다.
##### 실습 예제
1. `/public/welcome`은 누구나, `/user/profile``USER`, `/admin/manage``ADMIN`만 접근 가능하도록 설정하세요.
2. 브라우저에서 각 URL에 접근해 결과를 확인하세요.
3. `hasAuthority()``MANAGE_USERS` 권한을 추가하고 테스트해보세요.
---
위 내용은 역할과 권한의 차이를 명확히 하고, URL 기반 접근 제어의 설정과 동작을 실습 가능하도록 설명했습니다. 추가적인 코드 예제나 세부 사항이 필요하면 말씀해 주세요!

View File

@@ -0,0 +1,136 @@
아래는 "스프링 시큐리티" 책의 5장에 포함될 "메서드 수준 보안 설정"과 "@PreAuthorize, @Secured 어노테이션 활용"에 대한 내용입니다. 이 설명은 개념을 명확히 하고 실무에서 사용할 수 있는 예제를 포함해 작성되었습니다.
---
### 5장. 인가와 권한 관리
#### 5.3 메서드 수준 보안 설정
URL 기반 접근 제어는 웹 요청 단위로 보안을 적용하는 데 유용하지만, 더 세밀한 제어가 필요한 경우가 있습니다. 예를 들어, 특정 비즈니스 로직이나 서비스 메서드에 접근을 제한하고 싶을 때 **메서드 수준 보안 설정**을 사용합니다. 스프링 시큐리티는 이를 위해 메서드 호출 시점에서 권한을 검사하는 기능을 제공하며, 주로 어노테이션 기반으로 구현됩니다.
##### 메서드 수준 보안의 필요성
- **세밀한 제어**: URL 패턴만으로는 컨트롤러 내부 메서드의 개별 로직을 구분하기 어렵습니다.
- **비즈니스 로직 보호**: 데이터베이스 작업이나 민감한 연산을 호출하는 메서드를 보호할 수 있습니다.
- **재사용성**: 여러 엔드포인트에서 호출되는 서비스 메서드에 일관된 보안을 적용할 수 있습니다.
스프링 시큐리티는 메서드 수준 보안을 활성화하려면 `@EnableMethodSecurity`를 설정 클래스에 추가해야 합니다. 이후 `@PreAuthorize`, `@Secured` 같은 어노테이션을 사용해 권한을 검사합니다.
##### 기본 설정
```java
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
// 다른 설정 (UserDetailsService, SecurityFilterChain 등) 생략
}
```
`@EnableMethodSecurity`는 메서드 보안을 활성화하며, 기본적으로 `@PreAuthorize`, `@PostAuthorize`, `@Secured` 어노테이션을 지원합니다.
#### 5.4 @PreAuthorize와 @Secured 활용
스프링 시큐리티는 메서드 수준 보안을 위해 두 가지 대표적인 어노테이션을 제공합니다: **`@Secured`**와 **`@PreAuthorize`**. 두 어노테이션은 비슷한 목적을 가지지만, 사용법과 유연성에서 차이가 있습니다.
##### @Secured 어노테이션
`@Secured`는 메서드에 접근할 수 있는 역할(Role)을 지정하는 간단한 어노테이션입니다. 주로 역할 기반 접근 제어에 사용되며, 설정이 직관적입니다.
###### 사용 예제
```java
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Service;
@Service
public class AdminService {
@Secured("ROLE_ADMIN")
public String manageUsers() {
return "User management page for admins only";
}
@Secured({"ROLE_USER", "ROLE_ADMIN"})
public String viewProfile() {
return "Profile visible to users and admins";
}
}
```
- `@Secured("ROLE_ADMIN")`: `ROLE_ADMIN` 역할이 있는 사용자만 `manageUsers()` 메서드를 호출할 수 있습니다.
- `@Secured({"ROLE_USER", "ROLE_ADMIN"})`: `ROLE_USER` 또는 `ROLE_ADMIN` 중 하나라도 가진 사용자가 `viewProfile()`를 호출할 수 있습니다.
###### 동작 원리
1. 메서드 호출 시 스프링 시큐리티의 AOP(Aspect-Oriented Programming) 프록시가 개입합니다.
2. 현재 인증된 사용자의 `GrantedAuthority` 목록을 확인해 지정된 역할이 있는지 검사합니다.
3. 역할이 없으면 `AccessDeniedException`이 발생하고, 호출이 차단됩니다.
###### 한계
- 역할만 지원하며, 권한(Authority)이나 복잡한 조건은 처리 불가.
- SpEL(Spring Expression Language)을 지원하지 않아 유연성이 제한적.
##### @PreAuthorize 어노테이션
`@PreAuthorize``@Secured`보다 더 강력하고 유연한 어노테이션으로, SpEL을 사용해 복잡한 조건을 정의할 수 있습니다. 역할, 권한, 메서드 매개변수, 인증 객체 등을 기반으로 접근을 제어할 수 있습니다.
###### 사용 예제
```java
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class PostService {
@PreAuthorize("hasRole('ADMIN')")
public String deleteAllPosts() {
return "All posts deleted by admin";
}
@PreAuthorize("hasAuthority('WRITE_POST')")
public String createPost(String content) {
return "Post created: " + content;
}
@PreAuthorize("#username == authentication.name")
public String viewOwnProfile(String username) {
return "Profile for " + username;
}
@PreAuthorize("hasRole('USER') and #postId > 0")
public String editPost(int postId, String content) {
return "Post " + postId + " updated: " + content;
}
}
```
- **`hasRole('ADMIN')`**: `ROLE_ADMIN` 역할 검사. (`@Secured`와 유사하지만 SpEL 사용 가능)
- **`hasAuthority('WRITE_POST')`**: `WRITE_POST` 권한 검사.
- **`#username == authentication.name`**: 메서드 매개변수(`username`)가 현재 인증된 사용자의 이름과 같은지 확인.
- **`hasRole('USER') and #postId > 0`**: `ROLE_USER` 역할이 있고, `postId`가 양수일 때만 허용.
###### 동작 원리
1. `@PreAuthorize`는 메서드 실행 전에 SpEL 표현식을 평가합니다.
2. 표현식이 `true`면 메서드가 실행되고, `false``AccessDeniedException`이 발생합니다.
3. `authentication` 객체(현재 사용자 정보)와 메서드 매개변수를 활용해 동적 조건을 검사합니다.
###### 장점
- **유연성**: 역할, 권한, 매개변수 기반의 복잡한 로직 가능.
- **조건문**: `and`, `or`, `not` 같은 연산자 사용 가능.
- **커스터마이징**: SpEL을 통해 비즈니스 로직에 맞춘 조건 설정 가능.
##### @Secured vs @PreAuthorize 비교
| 특징 | `@Secured` | `@PreAuthorize` |
|-------------------|---------------------------|---------------------------|
| 지원 범위 | 역할(Role)만 | 역할, 권한, SpEL 조건 |
| 유연성 | 낮음 | 높음 |
| 사용 난이도 | 간단 | 약간 복잡 |
| 예제 | `@Secured("ROLE_ADMIN")` | `@PreAuthorize("hasRole('ADMIN') and #id > 0")` |
##### 실습 예제
1. 컨트롤러에 `@Secured("ROLE_USER")`를 추가해 특정 엔드포인트 보호.
2. 서비스 메서드에 `@PreAuthorize("#id == authentication.principal.username")`를 적용해 사용자 본인의 데이터만 수정 가능하도록 설정.
3. `ROLE_ADMIN``MANAGE_USERS` 권한을 가진 사용자로 로그인해 두 어노테이션의 동작 확인.
##### 주의사항
- **성능**: 메서드 수준 보안은 AOP 프록시를 통해 동작하므로, 과도한 사용은 성능에 영향을 줄 수 있습니다.
- **예외 처리**: `AccessDeniedException`을 적절히 핸들링해 사용자 친화적인 오류 메시지를 제공하세요.
- **설정 활성화**: `@EnableMethodSecurity`가 없으면 어노테이션이 동작하지 않으니 반드시 추가하세요.
---
위 내용은 메서드 수준 보안의 필요성과 `@Secured`, `@PreAuthorize` 어노테이션의 활용 방법을 체계적으로 설명했습니다. 추가 예제나 특정 상황에 대한 설명이 필요하면 말씀해 주세요!

View File

@@ -0,0 +1,209 @@
아래는 "스프링 시큐리티" 책의 6장에 포함될 "커스텀 로그인 페이지 제작", "로그아웃 기능 구현", 그리고 "세션 관리와 타임아웃 설정"에 대한 내용입니다. 실습 가능한 예제와 함께 실무에서 유용한 설정을 설명했습니다.
---
### 6장. 로그인/로그아웃 커스터마이징
#### 6.1 커스텀 로그인 페이지 제작
스프링 시큐리티의 기본 로그인 페이지는 간단한 테스트에는 유용하지만, 실무에서는 디자인과 기능을 사용자 맞춤으로 변경해야 합니다. **커스텀 로그인 페이지**를 제작하면 애플리케이션의 UI/UX를 개선하고, 추가적인 인증 로직을 통합할 수 있습니다.
##### 커스텀 로그인 페이지 설정
1. **HTML 페이지 제작**: 먼저, 로그인 폼을 포함한 커스텀 페이지를 만듭니다. 예를 들어, `src/main/resources/templates/login.html`:
```html
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<h2>로그인</h2>
<form method="post" action="/login">
<div>
<label>아이디</label>
<input type="text" name="username" required>
</div>
<div>
<label>비밀번호</label>
<input type="password" name="password" required>
</div>
<button type="submit">로그인</button>
</form>
<p th:if="${param.error}" style="color:red">아이디 또는 비밀번호가 잘못되었습니다.</p>
</body>
</html>
```
- `action="/login"`: 스프링 시큐리티의 기본 인증 엔드포인트.
- `name="username"`, `name="password"`: 기본 필드 이름(변경 가능).
- `${param.error}`: Thymeleaf를 사용해 로그인 실패 시 오류 메시지 표시.
2. **SecurityConfig 설정**:
```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.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login") // 커스텀 로그인 페이지 경로
.defaultSuccessUrl("/home") // 로그인 성공 시 리다이렉트
.permitAll() // 로그인 페이지 접근 허용
);
return http.build();
}
}
```
- `loginPage("/login")`: 기본 로그인 대신 커스텀 페이지를 사용.
- `defaultSuccessUrl("/home")`: 인증 성공 후 이동할 경로.
- `permitAll()`: 인증되지 않은 사용자도 로그인 페이지에 접근 가능.
3. **컨트롤러 추가**:
```java
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login"; // login.html 반환
}
}
```
##### 동작 원리
- 사용자가 보호된 리소스에 접근하면 `/login`으로 리다이렉트됩니다.
- 커스텀 폼에서 입력된 `username``password`는 POST 요청으로 `/login`에 전달됩니다.
- `UsernamePasswordAuthenticationFilter`가 이를 처리해 인증을 수행합니다.
##### 추가 커스터마이징
- **실패 처리**: `.failureUrl("/login?error")`를 추가해 실패 시 오류 메시지를 전달.
- **필드 이름 변경**: `.usernameParameter("id")`, `.passwordParameter("pass")`로 기본 이름 변경 가능.
#### 6.3 로그아웃 기능 구현
로그아웃은 사용자의 세션을 종료하고 인증 상태를 해제하는 기능입니다. 스프링 시큐리티는 기본 로그아웃 설정을 제공하지만, 커스터마이징으로 사용자 경험을 개선할 수 있습니다.
##### 기본 로그아웃 설정
```java
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(form -> form.loginPage("/login").permitAll())
.logout(logout -> logout
.logoutUrl("/logout") // 로그아웃 요청 URL
.logoutSuccessUrl("/login?logout") // 로그아웃 성공 시 리다이렉트
.permitAll() // 로그아웃 접근 허용
);
return http.build();
}
}
```
- `logoutUrl("/logout")`: 기본 로그아웃 엔드포인트(POST 요청 필요).
- `logoutSuccessUrl("/login?logout")`: 로그아웃 후 이동할 경로.
##### 로그아웃 버튼 추가
`login.html`에 로그아웃 버튼을 추가하려면:
```html
<form method="post" action="/logout">
<button type="submit">로그아웃</button>
</form>
```
- POST 요청이어야 하며, CSRF 토큰이 필요합니다(기본적으로 활성화).
##### CSRF 비활성화 시 주의
CSRF 보호를 비활성화하면 GET 요청으로도 로그아웃 가능:
```java
http
.csrf(csrf -> csrf.disable())
.logout(logout -> logout.logoutUrl("/logout"));
```
이 경우 `<a href="/logout">로그아웃</a>`로 간단히 구현 가능하지만, 보안성이 낮아지므로 주의하세요.
##### 커스터마이징
- **핸들러 추가**: `.addLogoutHandler()`로 커스텀 로그아웃 로직(예: 로그 기록) 추가.
- **성공 핸들러**: `.logoutSuccessHandler()`로 리다이렉트 대신 JSON 응답 반환 가능.
#### 6.4 세션 관리와 타임아웃 설정
세션 관리는 사용자의 인증 상태를 유지하고, 세션 타임아웃을 통해 보안을 강화하는 데 중요합니다. 스프링 시큐리티는 세션 설정을 세밀하게 조정할 수 있는 옵션을 제공합니다.
##### 기본 세션 관리
스프링 시큐리티는 인증 성공 시 `SecurityContext`를 세션에 저장합니다. `SecurityContextPersistenceFilter`가 이를 관리하며, 기본적으로 서블릿 컨테이너의 세션 설정을 따릅니다.
##### 세션 설정 예제
```java
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(form -> form.loginPage("/login").permitAll())
.logout(logout -> logout.logoutUrl("/logout").logoutSuccessUrl("/login?logout"))
.sessionManagement(session -> session
.maximumSessions(1) // 최대 세션 수 제한 (1명만 로그인 가능)
.maxSessionsPreventsLogin(true) // 중복 로그인 차단
.expiredUrl("/login?expired") // 세션 만료 시 리다이렉트
);
return http.build();
}
}
```
- `maximumSessions(1)`: 한 사용자가 동시에 여러 세션을 가질 수 없도록 제한.
- `maxSessionsPreventsLogin(true)`: 새 로그인 시 기존 세션을 무효화 대신 차단.
- `expiredUrl("/login?expired")`: 세션 만료 후 이동 경로.
##### 타임아웃 설정
세션 타임아웃은 서블릿 컨테이너 수준에서 설정하거나, 애플리케이션에서 조정 가능:
1. **application.properties**:
```properties
server.servlet.session.timeout=1800 # 30분 (초 단위)
```
2. **코드로 설정**:
```java
import org.springframework.context.annotation.Bean;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
@Configuration
public class SessionConfig {
@Bean
public SessionRepositoryFilter<?> sessionRepositoryFilter(RedisIndexedSessionRepository sessionRepository) {
sessionRepository.setDefaultMaxInactiveInterval(1800); // 30분
return new SessionRepositoryFilter<>(sessionRepository);
}
}
```
Redis와 같은 외부 저장소를 사용하면 세션 지속성을 높일 수 있습니다.
##### 세션 관리 추가 옵션
- **세션 고정 보호**: `.sessionFixation().migrateSession()`으로 세션 고정 공격 방지.
- **무효화**: `.invalidSessionUrl("/login?invalid")`로 무효 세션 처리.
- **동시 세션 모니터링**: `HttpSessionEventPublisher`를 추가해 세션 생성/소멸 이벤트 감지.
##### 실습 예제
1. 커스텀 로그인 페이지를 만들고, 로그인 후 `/home`으로 이동.
2. 로그아웃 버튼을 추가해 `/login?logout`으로 리다이렉트 확인.
3. 세션 타임아웃을 1분으로 설정하고, 만료 후 동작 테스트.
---
위 내용은 커스텀 로그인/로그아웃 구현과 세션 관리 방법을 실습 가능하도록 설명했습니다. 추가적인 예제나 설정이 필요하면 말씀해 주세요!

View File

@@ -0,0 +1,260 @@
아래는 "스프링 시큐리티" 책의 7장에 포함될 "OAuth2 기본 개념", "구글 등 소셜 로그인 구현", 그리고 "커스텀 OAuth2 클라이언트 설정"에 대한 내용입니다. 개념을 명확히 하고 실습 가능한 예제를 포함해 실무 적용성을 높였습니다.
---
### 7장. OAuth2와 소셜 로그인
#### 7.1 OAuth2 기본 개념
**OAuth2**는 인증(Authentication)과 권한 부여(Authorization)를 위한 표준 프로토콜로, 사용자가 자신의 자격 증명을 직접 공유하지 않고도 제3자 애플리케이션이 리소스에 접근할 수 있도록 합니다. 소셜 로그인(구글, 페이스북 등)이나 API 인증에 널리 사용됩니다.
##### OAuth2의 주요 구성 요소
- **Resource Owner**: 리소스를 소유한 사용자(예: 구글 계정 소유자).
- **Client**: 리소스에 접근하려는 애플리케이션(우리의 스프링 앱).
- **Authorization Server**: 사용자를 인증하고 토큰을 발급하는 서버(예: 구글 인증 서버).
- **Resource Server**: 보호된 리소스를 제공하는 서버(예: 구글 API).
- **Access Token**: 클라이언트가 리소스에 접근할 때 사용하는 키.
##### 인증 흐름 (Authorization Code Grant)
가장 흔히 사용되는 흐름으로, 소셜 로그인에 적합합니다:
1. 사용자가 클라이언트에서 "구글로 로그인" 버튼을 클릭.
2. 클라이언트가 사용자를 Authorization Server로 리다이렉트.
3. 사용자가 로그인 후 권한을 승인하면 Authorization Code가 클라이언트로 반환.
4. 클라이언트가 코드를 Access Token으로 교환.
5. Access Token으로 Resource Server에서 사용자 정보를 가져옴.
스프링 시큐리티는 OAuth2를 쉽게 통합할 수 있도록 `spring-security-oauth2-client` 모듈을 제공하며, 최소한의 설정으로 소셜 로그인을 구현할 수 있습니다.
#### 7.3 구글, 깃허브 등 소셜 로그인 구현
스프링 시큐리티를 사용하면 구글, 깃허브 같은 소셜 로그인을 빠르게 구현할 수 있습니다. 여기서는 구글 로그인을 예로 설명합니다.
##### 1. 의존성 추가
`pom.xml`에 다음 의존성을 추가:
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
```
##### 2. 구글 OAuth2 클라이언트 등록
1. [Google Cloud Console](https://console.cloud.google.com)에서 프로젝트 생성.
2. "OAuth 2.0 클라이언트 ID" 생성:
- 애플리케이션 유형: 웹 애플리케이션.
- 리다이렉트 URI: `http://localhost:8080/login/oauth2/code/google`.
3. 클라이언트 ID와 클라이언트 비밀번호(Secret)를 발급받음.
##### 3. 설정 파일 작성
`application.yml`에 구글 OAuth2 설정 추가:
```yaml
spring:
security:
oauth2:
client:
registration:
google:
client-id: your-google-client-id
client-secret: your-google-client-secret
scope: profile, email
provider:
google:
authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
token-uri: https://oauth2.googleapis.com/token
user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
user-name-attribute: email
```
##### 4. SecurityConfig 설정
```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.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login") // 커스텀 로그인 페이지
.defaultSuccessUrl("/home") // 로그인 성공 시 이동
)
.logout(logout -> logout.logoutSuccessUrl("/"));
return http.build();
}
}
```
##### 5. 로그인 페이지 제작
`src/main/resources/templates/login.html`:
```html
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<h2>로그인</h2>
<a href="/oauth2/authorization/google">구글로 로그인</a>
</body>
</html>
```
- `/oauth2/authorization/google`은 스프링 시큐리티가 자동 생성한 구글 로그인 경로.
##### 6. 사용자 정보 확인
로그인 성공 후 `Principal` 객체로 사용자 정보를 가져올 수 있습니다:
```java
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/home")
public String home(@AuthenticationPrincipal OAuth2User oAuth2User, Model model) {
model.addAttribute("name", oAuth2User.getAttribute("name"));
model.addAttribute("email", oAuth2User.getAttribute("email"));
return "home";
}
}
```
`home.html`:
```html
<!DOCTYPE html>
<html>
<body>
<h1>환영합니다, <span th:text="${name}"></span>!</h1>
<p>이메일: <span th:text="${email}"></span></p>
<a href="/logout">로그아웃</a>
</body>
</html>
```
##### 깃허브 로그인 추가
`application.yml`에 깃허브 설정 추가:
```yaml
spring:
security:
oauth2:
client:
registration:
github:
client-id: your-github-client-id
client-secret: your-github-client-secret
scope: read:user
```
로그인 링크: `<a href="/oauth2/authorization/github">깃허브로 로그인</a>`.
##### 동작 원리
1. 사용자가 구글 로그인 링크를 클릭하면 구글 인증 서버로 리다이렉트.
2. 구글에서 인증 후 리다이렉트 URI로 코드를 반환.
3. 스프링 시큐리티가 코드를 토큰으로 교환하고, 사용자 정보를 가져와 `OAuth2User`로 저장.
4. 인증 성공 시 `/home`으로 이동.
#### 7.4 커스텀 OAuth2 클라이언트 설정
스프링 시큐리티의 기본 설정으로 지원되지 않는 제공자(예: 네이버, 카카오)나 고급 요구사항을 처리하려면 커스텀 OAuth2 클라이언트를 설정해야 합니다.
##### 네이버 로그인 예제
1. **네이버 개발자 센터**에서 클라이언트 ID와 Secret 발급.
2. `application.yml`에 추가:
```yaml
spring:
security:
oauth2:
client:
registration:
naver:
client-id: your-naver-client-id
client-secret: your-naver-client-secret
redirect-uri: "{baseUrl}/login/oauth2/code/naver"
authorization-grant-type: authorization_code
scope: name, email
provider:
naver:
authorization-uri: https://nid.naver.com/oauth2.0/authorize
token-uri: https://nid.naver.com/oauth2.0/token
user-info-uri: https://openapi.naver.com/v1/nid/me
user-name-attribute: response # 네이버는 사용자 정보가 'response' 객체에 포함됨
```
3. **커스텀 User Service**:
네이버의 사용자 정보 형식이 구글과 다르므로 `OAuth2UserService`를 커스터마이징:
```java
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
OAuth2User oAuth2User = super.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
if ("naver".equals(registrationId)) {
Map<String, Object> response = (Map<String, Object>) oAuth2User.getAttributes().get("response");
return new org.springframework.security.oauth2.core.user.DefaultOAuth2User(
oAuth2User.getAuthorities(),
response, // 네이버의 사용자 정보
"id" // 고유 식별자
);
}
return oAuth2User;
}
}
```
4. **SecurityConfig에 등록**:
```java
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomOAuth2UserService customOAuth2UserService) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
.defaultSuccessUrl("/home")
);
return http.build();
}
}
```
##### 커스터마이징 포인트
- **토큰 처리**: `.tokenEndpoint()`로 커스텀 토큰 요청 설정.
- **사용자 매핑**: 데이터베이스에 사용자 정보를 저장하거나, 추가 속성을 매핑.
- **에러 처리**: `.failureHandler()`로 인증 실패 시 커스텀 로직 추가.
##### 실습 예제
1. 네이버 로그인 버튼 추가: `<a href="/oauth2/authorization/naver">네이버로 로그인</a>`.
2. 로그인 후 반환된 사용자 정보(이름, 이메일 등)를 화면에 출력.
3. 데이터베이스에 신규 사용자를 등록하는 로직 추가.
---
위 내용은 OAuth2의 기본 개념과 구글 소셜 로그인 구현, 커스텀 OAuth2 클라이언트 설정 방법을 설명했습니다. 추가적인 설정이나 예제가 필요하면 말씀해 주세요!

254
docs/security/08_JWT.md Normal file
View File

@@ -0,0 +1,254 @@
아래는 "스프링 시큐리티" 책의 8장에 포함될 "JWT(Json Web Token) 개요", "스프링 시큐리티와 JWT 연동", 그리고 "Stateless 인증 설계"에 대한 내용입니다. 개념을 명확히 하고, 스프링부트에서의 실습 가능한 구현 방법을 포함했습니다.
---
### 8장. JWT와 토큰 기반 인증
#### 8.1 JWT(Json Web Token) 개요
**JWT(Json Web Token)**는 인증 및 권한 부여를 위해 사용되는 표준 토큰 형식으로, 클라이언트와 서버 간에 안전하게 정보를 전달합니다. 세션 기반 인증과 달리 서버가 상태를 유지하지 않는(stateless) 특성을 가지며, REST API와 같은 분산 환경에서 널리 사용됩니다.
##### JWT 구조
JWT는 세 부분으로 구성되며, 점(`.`)으로 구분됩니다:
1. **Header**: 토큰의 유형(보통 "JWT")과 서명 알고리즘(예: HMAC SHA256)을 정의.
- 예: `{"alg": "HS256", "typ": "JWT"}`
2. **Payload**: 사용자 정보(클레임, Claims)를 포함. 표준 클레임(예: `sub`, `exp`)과 커스텀 클레임으로 구성.
- 예: `{"sub": "user123", "roles": ["USER"], "exp": 1698765432}`
3. **Signature**: Header와 Payload를 비밀 키로 서명한 값으로, 무결성을 보장.
- `HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)`
결과적으로 JWT는 `header.payload.signature` 형태로 인코딩됩니다(예: `eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.signature`).
##### 장점과 단점
- **장점**: 서버가 세션을 관리할 필요 없음, 확장성 좋음, 모바일 및 분산 시스템에 적합.
- **단점**: 토큰 크기가 클 수 있음, 취소(Revocation)가 복잡, Payload는 암호화되지 않음(기밀 데이터 주의).
#### 8.2 스프링 시큐리티와 JWT 연동
스프링부트에서 JWT를 사용하려면 토큰 생성, 검증, 인증 필터를 구현해야 합니다. 여기서는 기본적인 JWT 인증 흐름을 설정합니다.
##### 1. 의존성 추가
`pom.xml`에 JWT 라이브러리 추가:
```xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
```
##### 2. JWT 유틸리티 클래스
토큰 생성과 검증을 위한 유틸리티:
```java
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
@Component
public class JwtUtil {
private final String SECRET_KEY = "your-secret-key"; // 비밀 키 (환경 변수로 관리 권장)
private final long EXPIRATION_TIME = 1000 * 60 * 60; // 1시간
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY.getBytes())
.compact();
}
public String extractUsername(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY.getBytes())
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET_KEY.getBytes()).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
```
##### 3. JWT 인증 필터
HTTP 요청에서 JWT를 추출해 인증:
```java
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
if (jwtUtil.validateToken(token)) {
String username = jwtUtil.extractUsername(token);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, null);
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(request, response);
}
}
```
##### 4. SecurityConfig 설정
```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.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
```
##### 5. 로그인 엔드포인트
토큰 발급을 위한 컨트롤러:
```java
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthController {
private final JwtUtil jwtUtil;
public AuthController(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@PostMapping("/login")
public String login(@RequestBody LoginRequest request) {
// 실제 인증 로직 (예: DB에서 사용자 확인)은 생략
// 여기서는 단순히 username으로 토큰 생성
return jwtUtil.generateToken(request.getUsername());
}
}
class LoginRequest {
private String username;
private String password;
// Getters, Setters
public String getUsername() { return username; }
public String getPassword() { return password; }
public void setUsername(String username) { this.username = username; }
public void setPassword(String password) { this.password = password; }
}
```
##### 동작 흐름
1. 클라이언트가 `/login`에 POST 요청으로 사용자 이름과 비밀번호 전송.
2. 서버가 JWT를 생성해 반환.
3. 클라이언트가 후속 요청에 `Authorization: Bearer <token>` 헤더 포함.
4. `JwtAuthenticationFilter`가 토큰을 검증하고 인증 설정.
#### 8.4 Stateless 인증 설계
**Stateless 인증**은 서버가 클라이언트의 상태(세션)를 유지하지 않고, 각 요청마다 독립적으로 인증을 처리하는 방식입니다. JWT는 이를 구현하는 데 이상적입니다.
##### Stateless 설계 특징
- **세션 사용 안 함**: 서버는 세션 저장소를 유지하지 않음.
- **토큰 기반**: 모든 인증 정보가 JWT에 포함되어 클라이언트가 관리.
- **확장성**: 서버 부하가 줄어들고, 여러 서버 간 인증 공유가 쉬움.
##### Stateless 인증 구현
1. **세션 비활성화**: `SecurityConfig`에서 `SessionCreationPolicy.STATELESS` 설정.
```java
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
```
2. **CSRF 비활성화**: 세션을 사용하지 않으므로 CSRF 토큰도 필요 없음.
```java
.csrf(csrf -> csrf.disable())
```
3. **토큰 검증**: 매 요청마다 `JwtAuthenticationFilter`가 토큰을 확인해 인증.
##### 고려 사항
- **토큰 만료**: `EXPIRATION_TIME`을 적절히 설정(예: 1시간). 리프레시 토큰을 추가로 구현 가능.
- **토큰 취소**: Stateless 환경에서는 토큰 무효화가 어려움. 블랙리스트를 유지하거나 짧은 만료 시간을 설정.
- **보안**: 비밀 키를 안전하게 관리하고, HTTPS를 사용해 토큰 유출 방지.
##### 리프레시 토큰 추가 (선택)
만료된 액세스 토큰을 갱신하려면:
1. 로그인 시 액세스 토큰과 리프레시 토큰을 함께 발급.
2. 리프레시 토큰은 긴 만료 시간(예: 7일) 설정.
3. `/refresh` 엔드포인트에서 리프레시 토큰으로 새 액세스 토큰 발급.
```java
@PostMapping("/refresh")
public String refreshToken(@RequestBody String refreshToken) {
if (jwtUtil.validateToken(refreshToken)) {
String username = jwtUtil.extractUsername(refreshToken);
return jwtUtil.generateToken(username); // 새 액세스 토큰 반환
}
throw new RuntimeException("Invalid refresh token");
}
```
##### 실습 예제
1. `/login`으로 토큰 발급 후, 보호된 엔드포인트(`/home`)에 접근.
2. 토큰 없이 요청 시 403 확인.
3. 리프레시 토큰 로직을 추가해 토큰 갱신 테스트.
---
위 내용은 JWT의 개념, 스프링부트에서의 구현, 그리고 Stateless 인증 설계를 실습 가능하도록 설명했습니다. 추가적인 예제나 세부 사항이 필요하면 말씀해 주세요!

View File

@@ -0,0 +1,203 @@
아래는 "스프링 시큐리티" 책의 9장에 포함될 "CSRF(Cross-Site Request Forgery) 방어", "XSS(Cross-Site Scripting)와 콘텐츠 보안 정책", 그리고 "보안 헤더 추가"에 대한 내용입니다. 각 보안 위협의 개념과 스프링부트에서의 대응 방법을 실습 가능하도록 설명했습니다.
---
### 9장. 보안 강화 기법
#### 9.1 CSRF(Cross-Site Request Forgery) 방어
**CSRF(Cross-Site Request Forgery)**는 사용자가 의도하지 않은 요청을 악의적인 웹사이트를 통해 서버로 전송하게 만드는 공격입니다. 예를 들어, 사용자가 로그인한 상태에서 악성 사이트의 링크를 클릭하면, 사용자의 인증 쿠키를 활용해 은행 계좌 이체 같은 요청이 실행될 수 있습니다.
##### CSRF 동작 원리
- 공격자는 피해자가 로그인한 상태를 가정.
- 피해자가 공격자의 사이트에서 숨겨진 폼(예: `<form action="http://bank.com/transfer" method="post">`)을 실행.
- 브라우저가 자동으로 인증 쿠키를 포함해 요청 전송.
##### 스프링 시큐리티의 CSRF 보호
스프링 시큐리티는 기본적으로 CSRF 보호를 활성화하며, POST, PUT, DELETE 같은 상태 변경 요청에 CSRF 토큰을 요구합니다.
###### 기본 설정
```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.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin();
return http.build();
}
}
```
- CSRF 토큰은 `<input type="hidden" name="_csrf" value="token">` 형태로 폼에 자동 추가(Thymeleaf 사용 시).
- 토큰은 세션마다 고유하며, 요청 시 서버가 이를 검증.
###### Thymeleaf에서 사용
```html
<form method="post" th:action="@{/update}">
<input type="text" name="data">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<button type="submit">제출</button>
</form>
```
###### CSRF 비활성화
Stateless 인증(JWT 등)을 사용할 때는 CSRF 보호가 필요 없을 수 있습니다:
```java
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
```
##### 방어 팁
- **GET 요청 제한**: 상태 변경은 POST로만 처리.
- **토큰 확인**: 모든 변경 요청에 CSRF 토큰 포함.
- **SameSite 쿠키**: 세션 쿠키에 `SameSite=Strict` 설정 추가(아래 보안 헤더 참조).
#### 9.2 XSS(Cross-Site Scripting)와 콘텐츠 보안 정책
**XSS(Cross-Site Scripting)**는 공격자가 웹 페이지에 악성 스크립트를 삽입해 사용자의 브라우저에서 실행시키는 공격입니다. 이를 통해 세션 쿠키를 탈취하거나 페이지를 조작할 수 있습니다.
##### XSS 유형
- **Reflected XSS**: 악성 스크립트가 URL 파라미터 등으로 전달되어 즉시 실행.
- **Stored XSS**: 악성 스크립트가 데이터베이스에 저장되어 모든 사용자에게 노출.
- **DOM-based XSS**: 클라이언트 측 스크립트가 조작됨.
##### 스프링에서의 XSS 방어
1. **입력 검증**: 사용자 입력을 철저히 검증하고 sanitization 적용.
- `HtmlUtils.htmlEscape()`로 HTML 이스케이프:
```java
import org.springframework.web.util.HtmlUtils;
String safeInput = HtmlUtils.htmlEscape(userInput);
```
2. **템플릿 엔진**: Thymeleaf는 기본적으로 출력값을 이스케이프해 XSS 방어.
```html
<p th:text="${userInput}"></p> <!-- 자동 이스케이프 -->
<p th:utext="${userInput}"></p> <!-- 이스케이프 비활성화, 주의 필요 -->
```
##### 콘텐츠 보안 정책 (CSP)
**CSP(Content Security Policy)**는 브라우저가 허용된 소스에서만 리소스를 로드하도록 제한해 XSS를 방어합니다. HTTP 헤더로 설정합니다.
###### CSP 설정
```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.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.StaticHeadersWriter;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.addHeaderWriter(new StaticHeadersWriter("Content-Security-Policy",
"default-src 'self'; script-src 'self' https://trusted.cdn.com;"))
)
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin();
return http.build();
}
}
```
- `default-src 'self'`: 기본적으로 같은 출처에서만 리소스 로드.
- `script-src 'self' https://trusted.cdn.com`: 스크립트는 자체 및 신뢰된 CDN에서만 허용.
##### 방어 팁
- **출력 이스케이프**: 모든 동적 콘텐츠를 이스케이프 처리.
- **CSP 강화**: 외부 리소스 최소화 및 엄격한 정책 적용.
- **입력 제한**: 허용된 문자만 허용(예: 정규식 사용).
#### 9.3 보안 헤더 추가
보안 헤더는 브라우저의 기본 보안 기능을 강화해 다양한 공격을 방지합니다. 스프링 시큐리티는 이를 쉽게 추가할 수 있는 설정을 제공합니다.
##### 주요 보안 헤더
1. **X-Content-Type-Options**:
- `nosniff`: MIME 타입 스니핑 방지.
- 설정:
```java
http.headers(headers -> headers.contentTypeOptions());
```
2. **X-Frame-Options**:
- `DENY` 또는 `SAMEORIGIN`: 클릭재킹(Clickjacking) 방지.
- 설정:
```java
http.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()));
```
3. **X-XSS-Protection**:
- `1; mode=block`: 브라우저의 XSS 필터 활성화(구형 브라우저 지원).
- 설정:
```java
http.headers(headers -> headers.xssProtection());
```
4. **Strict-Transport-Security (HSTS)**:
- HTTPS 강제 적용 및 중간자 공격 방지.
- 설정:
```java
http.headers(headers -> headers.httpStrictTransportSecurity(hsts -> hsts.maxAgeInSeconds(31536000)));
```
5. **Content-Security-Policy**: 위 CSP 섹션 참조.
6. **Referrer-Policy**:
- 참조 정보 제한(예: `no-referrer-when-downgrade`).
- 설정:
```java
http.headers(headers -> headers.referrerPolicy(referrer -> referrer.policy(ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE)));
```
##### 종합 설정 예제
```java
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.contentTypeOptions()
.frameOptions(frame -> frame.deny())
.xssProtection()
.httpStrictTransportSecurity(hsts -> hsts.maxAgeInSeconds(31536000).includeSubDomains(true))
.addHeaderWriter(new StaticHeadersWriter("Referrer-Policy", "strict-origin-when-cross-origin"))
.addHeaderWriter(new StaticHeadersWriter("Content-Security-Policy", "default-src 'self'"))
)
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin();
return http.build();
}
}
```
##### SameSite 쿠키 설정
CSRF 방어를 강화하려면 세션 쿠키에 `SameSite` 속성 추가:
```properties
# application.properties
server.servlet.session.cookie.same-site=strict
```
- `Strict`: 타사 사이트에서 쿠키 전송 차단.
- `Lax`: GET 요청은 허용, POST 등은 차단.
##### 실습 예제
1. CSRF 토큰 없이 POST 요청 시 403 확인.
2. XSS 공격 시도(예: `<script>alert('xss')</script>`) 후 이스케이프 동작 확인.
3. 브라우저 개발자 도구에서 보안 헤더 적용 여부 점검.
---
위 내용은 CSRF, XSS, 보안 헤더의 개념과 스프링 시큐리티에서의 방어 방법을 실습 가능하도록 설명했습니다. 추가적인 예제나 세부 사항이 필요하면 말씀해 주세요!

141
docs/security/11_csrf.md Normal file
View File

@@ -0,0 +1,141 @@
스프링 부트에서 CSRF 설정을 URL 기반으로 세밀하게 제어하는 방법은 주로 `HttpSecurity` 설정을 통해 이루어집니다. 특정 URL 패턴에 대해서만 CSRF 보호를 활성화하거나 비활성화할 수 있습니다.
**핵심 아이디어:**
`HttpSecurity``csrf()` 메서드를 통해 CSRF 설정을 시작하고, `ignoringAntMatchers()`, `requireCsrfProtectionMatcher()` 등의 메서드를 사용하여 특정 URL 패턴을 설정합니다.
**설정 방법:**
`WebSecurityConfigurerAdapter`를 상속받는 설정 클래스 또는 `@Bean`으로 `SecurityFilterChain`을 정의하는 방식에서 `HttpSecurity`를 설정합니다.
**1. 특정 URL 패턴에 대해 CSRF 보호 비활성화:**
```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.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.ignoringRequestMatchers(
new AntPathRequestMatcher("/api/public/**"), // /api/public/으로 시작하는 모든 요청에 대해 CSRF 비활성화
new AntPathRequestMatcher("/h2-console/**") // H2 콘솔 접근 시 CSRF 비활성화 (개발 환경)
// 추가적인 URL 패턴 설정 가능
)
)
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() // 예시: 모든 요청 허용
);
return http.build();
}
}
```
* `.csrf(csrf -> ...)`: CSRF 설정을 시작합니다.
* `.ignoringRequestMatchers(...)`: 주어진 `RequestMatcher`와 일치하는 요청에 대해서는 CSRF 보호를 비활성화합니다.
* `new AntPathRequestMatcher("/api/public/**")`: Ant 스타일의 URL 패턴을 사용하여 `/api/public/`으로 시작하는 모든 경로를 매칭합니다.
* `new AntPathRequestMatcher("/h2-console/**")`: H2 콘솔 경로는 일반적으로 개발 환경에서 사용되므로 CSRF 보호를 비활성화하는 경우가 많습니다.
* 여러 개의 `ignoringRequestMatchers()`를 사용하여 다양한 URL 패턴을 설정할 수 있습니다.
**2. 특정 HTTP 메서드에 대해서만 CSRF 보호 비활성화:**
특정 URL 패턴에 대해 특정 HTTP 메서드(예: GET 요청)에 대해서만 CSRF 보호를 비활성화하고, 데이터를 변경하는 요청(POST, PUT, DELETE)에는 CSRF 보호를 유지할 수 있습니다.
```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.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import java.util.Arrays;
import java.util.List;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
List<RequestMatcher> withoutCsrf = Arrays.asList(
new AntPathRequestMatcher("/api/public/**", "GET"), // /api/public/으로 시작하는 GET 요청에 대해 CSRF 비활성화
new AntPathRequestMatcher("/some/readonly/path", "GET")
// 추가적인 GET 요청 패턴 설정 가능
);
http
.csrf(csrf -> csrf
.ignoringRequestMatchers(withoutCsrf.toArray(new RequestMatcher[0]))
)
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() // 예시: 모든 요청 허용
);
return http.build();
}
}
```
* `new AntPathRequestMatcher("/api/public/**", "GET")`: `/api/public/`으로 시작하고 HTTP 메서드가 `GET`인 요청만 매칭합니다.
**3. 특정 URL 패턴에 대해서만 CSRF 보호 활성화 (기본 설정 외 추가):**
기본적으로 CSRF 보호는 활성화되어 있지만, 특정 URL 패턴에 대해서만 명시적으로 활성화하거나, `ignoringRequestMatchers()`로 제외했던 패턴 중 일부에 대해 다시 활성화할 수 있습니다. (일반적인 사용 사례는 아닐 수 있습니다.)
```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.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import java.util.Arrays;
import java.util.List;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
List<RequestMatcher> requireCsrf = Arrays.asList(
new AntPathRequestMatcher("/admin/**") // /admin/으로 시작하는 요청에 대해 CSRF 활성화 (명시적)
// 추가적인 활성화 패턴 설정 가능
);
http
.csrf(csrf -> csrf
// .ignoringRequestMatchers(...) // 특정 패턴 비활성화 설정이 있을 경우
.requireCsrfProtectionMatcher(request -> {
// 기본적으로 POST, PUT, DELETE 요청에 대해 CSRF 보호를 적용하고,
// requireCsrf 리스트에 있는 패턴에 대해서도 적용
boolean defaultMatch = !request.getMethod().equalsIgnoreCase("GET");
return defaultMatch || requireCsrf.stream().anyMatch(matcher -> matcher.matches(request));
})
)
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() // 예시: 모든 요청 허용
);
return http.build();
}
}
```
* `.requireCsrfProtectionMatcher(RequestMatcher)`: CSRF 보호를 적용할 요청을 결정하는 `RequestMatcher`를 설정합니다.
* 위 예시에서는 기본적으로 GET 요청이 아닌 경우 CSRF 보호를 적용하고, 추가적으로 `/admin/**` 패턴과 일치하는 모든 요청에 대해 CSRF 보호를 적용합니다.
**주의 사항:**
* CSRF 보호를 특정 URL에 대해 비활성화하는 것은 보안 위험을 증가시킬 수 있습니다. 신중하게 결정해야 하며, 정말로 CSRF 보호가 불필요한 경우에만 적용해야 합니다.
* RESTful API를 개발하는 경우, Stateless 특성을 유지하기 위해 CSRF 대신 다른 보안 메커니즘(예: JWT 기반 인증)을 사용하는 것을 고려할 수 있습니다. 이 경우 CSRF를 비활성화할 수 있습니다.
* H2 콘솔과 같은 개발 도구에 대한 CSRF 비활성화는 개발 환경에서 편의를 위해 사용될 수 있지만, 프로덕션 환경에서는 보안을 위해 접근 제어를 설정하는 것이 좋습니다.
**결론:**
스프링 부트에서 URL 기반으로 CSRF 설정을 제어하려면 `HttpSecurity` 설정을 활용하여 `ignoringRequestMatchers()` 또는 `requireCsrfProtectionMatcher()` 메서드를 사용하여 원하는 URL 패턴에 따라 CSRF 보호를 활성화하거나 비활성화할 수 있습니다. 보안상의 영향을 충분히 고려하여 설정을 적용해야 합니다.

86
docs/security/README.md Normal file
View File

@@ -0,0 +1,86 @@
스프링부트의 Security에 관한 책의 목차를 작성해 드리겠습니다. 이 목차는 초보자부터 중급 개발자까지 이해할 수 있도록 기본 개념부터 실무 활용까지 다루며, 실습 예제와 함께 체계적으로 구성했습니다. 아래는 제안하는 목차입니다:
---
### 목차
#### 1장. 스프링 시큐리티 소개
- 1.1 스프링 시큐리티란?
- 1.2 스프링부트와 시큐리티의 통합
- 1.3 보안의 중요성과 기본 개념
- 1.4 이 책의 목표와 구성
#### 2장. 개발 환경 설정
- 2.1 스프링부트 프로젝트 생성
- 2.2 스프링 시큐리티 의존성 추가
- 2.3 기본 설정 확인 및 테스트
- 2.4 개발 도구와 환경 준비
#### 3장. 스프링 시큐리티 기본 동작 이해
- 3.1 인증(Authentication)과 인가(Authorization)
- 3.2 기본 인증 메커니즘
- 3.3 Security Filter Chain의 구조
- 3.4 기본 로그인 페이지와 동작 원리
#### 4장. 사용자 인증 구현
- 4.1 In-Memory 인증 설정
- 4.2 데이터베이스 연동을 통한 사용자 인증
- 4.3 커스텀 UserDetailsService 구현
- 4.4 비밀번호 암호화와 보안
#### 5장. 인가와 권한 관리
- 5.1 역할(Role)과 권한(Authority)의 차이
- 5.2 URL 기반 접근 제어
- 5.3 메서드 수준 보안 설정
- 5.4 @PreAuthorize와 @Secured 활용
#### 6장. 로그인/로그아웃 커스터마이징
- 6.1 커스텀 로그인 페이지 제작
- 6.2 로그인 성공/실패 처리
- 6.3 로그아웃 기능 구현
- 6.4 세션 관리와 타임아웃 설정
#### 7장. OAuth2와 소셜 로그인
- 7.1 OAuth2 기본 개념
- 7.2 스프링 시큐리티와 OAuth2 통합
- 7.3 구글, 깃허브 등 소셜 로그인 구현
- 7.4 커스텀 OAuth2 클라이언트 설정
#### 8장. JWT와 토큰 기반 인증
- 8.1 JWT(Json Web Token) 개요
- 8.2 스프링 시큐리티와 JWT 연동
- 8.3 토큰 발급 및 검증 구현
- 8.4 stateless 인증 설계
#### 9장. 보안 강화 기법
- 9.1 CSRF(Cross-Site Request Forgery) 방어
- 9.2 XSS(Cross-Site Scripting)와 콘텐츠 보안 정책
- 9.3 HTTPS 설정과 SSL/TLS 적용
- 9.4 보안 헤더 추가
#### 10장. 실무에서의 스프링 시큐리티
- 10.1 REST API 보안 설계
- 10.2 프론트엔드와 백엔드 통합
- 10.3 에러 처리와 예외 관리
- 10.4 배포 시 보안 점검 항목
#### 11장. 테스트와 디버깅
- 11.1 스프링 시큐리티 테스트 환경 구축
- 11.2 MockMvc를 활용한 보안 테스트
- 11.3 일반적인 문제 해결과 디버깅 팁
#### 12장. 프로젝트 실습
- 12.1 간단한 웹 애플리케이션 보안 구현
- 12.2 JWT 기반 REST API 구축
- 12.3 소셜 로그인 통합 프로젝트
- 12.4 종합 예제: 회원 관리 시스템
#### 부록
- A. 스프링 시큐리티 설정 참고 자료
- B. 자주 사용하는 보안 관련 라이브러리
- C. 용어 정리
- D. 추가 학습 리소스
---
이 목차는 스프링 시큐리티의 기초부터 실무 적용까지 폭넓게 다루며, 독자가 단계적으로 학습하고 실습할 수 있도록 설계되었습니다. 필요에 따라 특정 주제(예: 클라우드 환경에서의 보안, 마이크로서비스 보안)를 추가하거나 세부 항목을 조정할 수 있습니다. 의견이 있으면 말씀해 주세요!