2025-04-08T19:56:24
This commit is contained in:
38
docs/security/01_인증과 인가.md
Normal file
38
docs/security/01_인증과 인가.md
Normal 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 등 더 강력한 인증 방식을 사용하게 될 것입니다.
|
||||
|
||||
이 장에서는 기본 인증 메커니즘을 기반으로 스프링 시큐리티의 동작을 이해하고, 이후 장에서 이를 커스터마이징하거나 확장하는 방법을 배워보겠습니다.
|
||||
|
||||
---
|
||||
|
||||
위 내용은 독자가 인증과 인가의 개념을 명확히 이해하고, 스프링 시큐리티의 기본 인증 방식을 쉽게 파악할 수 있도록 작성되었습니다. 추가로 다루고 싶은 세부 사항이나 예제가 있다면 말씀해 주세요!
|
||||
58
docs/security/02_Security Filter Chain의 구조.md
Normal file
58
docs/security/02_Security Filter Chain의 구조.md
Normal 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의 구조와 기본 로그인 페이지의 동작을 체계적으로 설명하며, 스프링 시큐리티의 내부 동작을 이해하는 데 초점을 맞췄습니다. 추가적인 예제 코드나 다이어그램이 필요하다면 말씀해 주세요!
|
||||
227
docs/security/03_UserDetailsService 구현.md
Normal file
227
docs/security/03_UserDetailsService 구현.md
Normal 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 등)을 지원할 수 있습니다.
|
||||
- **비밀번호 정책**: 강력한 비밀번호를 요구하거나 주기적 변경을 유도하세요.
|
||||
- **암호화 키 관리**: 외부 설정 파일이나 환경 변수를 통해 암호화 키를 관리하세요.
|
||||
|
||||
---
|
||||
|
||||
위 내용은 인증 방식의 기본부터 실무 적용까지 다루며, 비밀번호 암호화로 보안을 강화하는 방법을 설명했습니다. 코드 예제나 특정 부분에 대한 추가 설명이 필요하면 말씀해 주세요!
|
||||
111
docs/security/04_역할과 권한.md
Normal file
111
docs/security/04_역할과 권한.md
Normal 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 기반 접근 제어의 설정과 동작을 실습 가능하도록 설명했습니다. 추가적인 코드 예제나 세부 사항이 필요하면 말씀해 주세요!
|
||||
136
docs/security/05_메서드 수준 보안.md
Normal file
136
docs/security/05_메서드 수준 보안.md
Normal 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` 어노테이션의 활용 방법을 체계적으로 설명했습니다. 추가 예제나 특정 상황에 대한 설명이 필요하면 말씀해 주세요!
|
||||
209
docs/security/06_커스텀 로그인 페이지.md
Normal file
209
docs/security/06_커스텀 로그인 페이지.md
Normal 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분으로 설정하고, 만료 후 동작 테스트.
|
||||
|
||||
---
|
||||
|
||||
위 내용은 커스텀 로그인/로그아웃 구현과 세션 관리 방법을 실습 가능하도록 설명했습니다. 추가적인 예제나 설정이 필요하면 말씀해 주세요!
|
||||
260
docs/security/07_소셜 로그인.md
Normal file
260
docs/security/07_소셜 로그인.md
Normal 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
254
docs/security/08_JWT.md
Normal 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 인증 설계를 실습 가능하도록 설명했습니다. 추가적인 예제나 세부 사항이 필요하면 말씀해 주세요!
|
||||
203
docs/security/09_보안 강화.md
Normal file
203
docs/security/09_보안 강화.md
Normal 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
141
docs/security/11_csrf.md
Normal 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
86
docs/security/README.md
Normal 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. 추가 학습 리소스
|
||||
|
||||
---
|
||||
|
||||
이 목차는 스프링 시큐리티의 기초부터 실무 적용까지 폭넓게 다루며, 독자가 단계적으로 학습하고 실습할 수 있도록 설계되었습니다. 필요에 따라 특정 주제(예: 클라우드 환경에서의 보안, 마이크로서비스 보안)를 추가하거나 세부 항목을 조정할 수 있습니다. 의견이 있으면 말씀해 주세요!
|
||||
Reference in New Issue
Block a user