diff --git a/keyword/chapter10/image/image1.png b/keyword/chapter10/image/image1.png new file mode 100644 index 0000000..b1dfe9f Binary files /dev/null and b/keyword/chapter10/image/image1.png differ diff --git a/keyword/chapter10/image/image2.png b/keyword/chapter10/image/image2.png new file mode 100644 index 0000000..f715366 Binary files /dev/null and b/keyword/chapter10/image/image2.png differ diff --git a/keyword/chapter10/image/image3.png b/keyword/chapter10/image/image3.png new file mode 100644 index 0000000..e101612 Binary files /dev/null and b/keyword/chapter10/image/image3.png differ diff --git a/keyword/chapter10/keyword_access_refresh.md b/keyword/chapter10/keyword_access_refresh.md new file mode 100644 index 0000000..6475bf2 --- /dev/null +++ b/keyword/chapter10/keyword_access_refresh.md @@ -0,0 +1,40 @@ +# Access Token & Refresh Token + +토큰 기반 인증은 서버가 인증 상태를 저장하지 않고, 요청 하나에 인증 정보가 모두 담겨 전달되는 Stateless 방식이다. 이로 인해 토큰이 탈취될 경우, 토큰 자체에 포함된 사용자 ID나 권한 정보를 통해 공격자가 해당 사용자로 가장할 수 있다는 치명적인 문제가 존재한다. + +> 즉, 유효한 토큰만 가지고 있다면 서버 입장에서는 정상 사용자와 공격자를 구분할 수 없다. +> + +반면 세션 기반 인증에서는 클라이언트와 서버 간에 오고 가는 값이 단순한 `sessionId`이기 때문에, 요청·응답 과정에서 사용자 정보가 직접 노출되지 않는다. + +실제 인증 정보와 권한 정보는 모두 서버 내부의 `HttpSession`에 저장되어 있으며, 클라이언트는 이를 식별하기 위한 키만을 전달한다. + +### 세션 ID 강탈 당하면 해당 사용자 인 척 하면 되지 않나? +--- + +세션 ID가 탈취될 경우 공격자가 해당 사용자로 가장할 수 있다는 점에서 세션 역시 보안 위협이 존재한다. + +그러나 세션 ID 자체에는 사용자 정보가 포함되어 있지 않으며, 실제 인증 상태는 서버에서 관리되기 때문에 서버 차원에서 해당 세션을 즉시 무효화하거나 강제 로그아웃 처리하는 것이 가능하다. + +반면 토큰은 한 번 발급되어 클라이언트에 전달되는 순간 서버의 관리 영역을 벗어나며, 탈취 사실을 인지하더라도 만료 전까지 이를 즉시 차단하기 어렵다는 한계가 있다. + +### 너무 위험한거 아닌가? +--- + +만약 JWT 토큰이 해커에게 탈취된다면, 해커는 해당 토큰의 실제 소유자인 것처럼 서비스를 이용할 수 있다. + +이러한 위험을 줄이기 위해 JWT에는 일반적으로 **유효 기간(expiration time)** 을 설정하여, 한 번의 탈취로 장기간 악용되는 상황을 방지한다. 그러나 단순히 토큰의 유효 기간을 짧게 설정하면, 사용자는 인증을 반복적으로 수행해야 하는 불편함을 겪게 된다. + +이러한 문제를 해결하기 위해 보통 JWT를 두 개로 분리하여 사용하는 방식, 즉 `AccessToken`과 `RefreshToken` 구조가 활용된다. + +![Untitled](./image/image3.png) + +> `AccessToken`과 `RefreshToken`의 가장 큰 차이는 **유효 기간**이다. +> + +`AccessToken`은 상대적으로 짧은 유효 기간을 가지며, 일반적인 API 요청 시 사용된다. 반면 `RefreshToken`은 더 긴 유효 기간을 가지며, `AccessToken`이 만료되었을 때 이를 재발급받는 용도로만 사용된다. 이로 인해 `RefreshToken`은 사용 빈도가 낮아 상대적으로 노출 위험이 적다. 그럼에도 불구하고 `RefreshToken` 역시 탈취될 가능성을 완전히 배제할 수는 없다. + +> 이를 보완하기 위해 OAuth에서는 **Refresh Token Rotation**이라는 전략을 제시한다. +> + +Refresh Token Rotation은 `AccessToken`을 재발급할 때마다 `RefreshToken` 또한 새로 발급하고, 기존의 `RefreshToken`을 즉시 무효화하는 방식이다. 이 방식에서는 설령 공격자가 이전 `RefreshToken`을 탈취하더라도, 이미 무효화된 토큰이기 때문에 더 이상 사용할 수 없게 된다. \ No newline at end of file diff --git a/keyword/chapter10/keyword_authentication_authorization.md b/keyword/chapter10/keyword_authentication_authorization.md new file mode 100644 index 0000000..4045a92 --- /dev/null +++ b/keyword/chapter10/keyword_authentication_authorization.md @@ -0,0 +1,70 @@ +# Authentication & Authorization + +## 1. 인증(Authentication)이란? + +인증은 쉽게 말해 **“이 사용자가 누구인지 신원을 확인하는 과정”**입니다. + +시스템이 “당신은 정말 당신이 주장하는 그 사람이 맞습니까?”라고 묻고, 사용자가 이에 대한 증거(비밀번호, 생체 정보 등)를 제시하는 단계입니다. + +> 건물에 들어가기 위해 입구에서 **신분증**을 보여주는 것과 같습니다. +> + +로그인을 시도했을 때, 입력한 정보가 DB에 있는지 확인합니다. + +**일치하면** 유저 정보를 반환하거나, 다음 통신에서 나를 증명할 수 있는 **토큰** 혹은 **세션**을 부여합니다. + +**일치하지 않으면:** 접근을 차단하며 보통 **401 Unauthorized** 에러를 반환합니다. + +## 2. 인가(Authorization)란? + +인가는 인증을 마친 사용자가 **“어떤 권한을 가지고 있으며, 무엇을 할 수 있는지 판단하는 과정”**입니다. + +단순히 로그인에 성공했다고 해서 모든 기능을 쓸 수 있는 것은 아니기에, 특정 요청을 수행할 자격이 있는지 검증하는 단계가 반드시 필요합니다. + +> 건물에는 들어왔지만(인증), +> + +> **‘관리자 전용 구역’**에는 관리자 카드키가 있는 사람만 들어갈 수 있는 것과 같습니다. +> + +유저가 "내 블로그 설정 변경" 요청을 보냈을 때, 시스템은 두 가지를 확인합니다. + +1. 이 유저가 로그인된 상태인가? (**인증 확인**) +2. 이 블로그의 소유주가 현재 로그인한 유저가 맞는가? (**권한 확인 = 인가**) + +권한이 없다면 접근을 거부하며 **403 Forbidden** 에러를 반환합니다. + + + +## 3. Spring Security에서 인증과 인가의 흐름 + +![image.png](./image/image2.png) + +사용자가 로그인을 시도하고, 이후 권한이 필요한 페이지에 접근하는 전 과정을 5단계로 요약할 수 있습니다. + +### 1단계: 인증 요청 (Authentication) + +- 사용자가 아이디와 비밀번호를 입력하여 로그인을 시도합니다. +- **UsernamePasswordAuthenticationFilter**가 이 요청을 가로채서 `Authentication` 객체(미인증 상태)를 생성합니다. + +### 2단계: 신원 검증 (AuthenticationManager & Provider) + +- 필터는 **AuthenticationManager**에게 검증을 맡깁니다. +- 매니저는 **AuthenticationProvider**를 통해 `UserDetailsService`를 호출하고, DB에 저장된 유저 정보(`UserDetails`)와 입력된 비밀번호를 비교합니다. +- 비밀번호가 일치하면 '인증이 완료된' `Authentication` 객체를 반환합니다. + +### 3단계: 인증 정보 보관 (SecurityContextHolder) + +- 인증에 성공하면, 위에서 만든 '인증 완료 객체'를 **SecurityContextHolder** 내부의 **SecurityContext**에 저장합니다. +- 이로써 시스템은 "현재 접속 중인 이 유저는 '홍길동'이며 'USER' 권한을 가졌다"는 사실을 기억하게 됩니다. + +### 4단계: 권한 확인 (Authorization / FilterSecurityInterceptor) + +- 로그인한 유저가 권한이 필요한 페이지(예: `/admin`)에 접근합니다. +- 필터 체인의 가장 마지막 단계에 있는 **FilterSecurityInterceptor**가 작동합니다. +- 이 필터는 **SecurityContextHolder**에서 유저 정보를 꺼내와 "이 유저가 ADMIN 권한이 있는가?"를 확인합니다. + +### 5단계: 최종 응답 (Success or Failure) + +- **인가 성공:** 유저가 권한을 가지고 있다면 컨트롤러로 요청을 전달하여 페이지를 보여줍니다. +- **인가 실패:** 권한이 없다면 **ExceptionTranslationFilter**가 에러를 가로채서 `403 Forbidden` 응답을 보내거나 적절한 예외 처리를 수행합니다. \ No newline at end of file diff --git a/keyword/chapter10/keyword_session_token.md b/keyword/chapter10/keyword_session_token.md new file mode 100644 index 0000000..9446d44 --- /dev/null +++ b/keyword/chapter10/keyword_session_token.md @@ -0,0 +1,87 @@ +# Session & Token +Spring Security의 공통 인증 흐름에서는 인증이 완료되면 사용자 정보를 포함한 Authentication 객체를 생성하고, 이를 SecurityContext에 저장한다. 세션 기반 인증과 토큰 기반 인증의 핵심적인 차이는 이 `SecurityContext`를 **어떻게, 그리고 얼마나 유지하느냐**에 있다. + +![image.png](./image/image2.png) + +세션 기반 인증은 `SecurityContext`를 서버 내부의 `HttpSession`에 저장함으로써 다음 요청에서도 인증 상태를 재사용하는 Stateful 방식인 반면, + +토큰 기반 인증은 요청 처리 과정에서만 `SecurityContext`를 생성하고 응답 이후에는 이를 제거하여 서버가 인증 상태를 저장하지 않는 Stateless 방식으로 동작한다. + + +## **1. 세션 기반 인증에서의 SecurityContext** + +### **로그인 직후** + +``` +AuthenticationProvider 인증 성공 + ↓ +Authentication 생성 + ↓ +SecurityContext에 저장 + ↓ +SecurityContext를 HttpSession에 저장 +``` + +### 다음 요청에서는? + +``` +요청 + JSESSIONID + ↓ +SecurityContextPersistenceFilter + ↓ +세션에서 SecurityContext 꺼냄 +``` + +**SecurityContext가 “세션에 보관”됨** + +## **2. JWT(토큰) 기반 인증에서의 SecurityContext** + +### 로그인 직후 + +``` +AuthenticationProvider 인증 성공 + ↓ +Authentication 생성 + ↓ +SecurityContext에 잠깐 저장 (요청 동안) + ↓ +User 정보로 JWT 생성 + ↓ +응답 후 SecurityContext 제거 +``` + +**세션에 저장 안 함** + +### 다음 요청에서는? + +``` +요청 + Authorization: Bearer JWT + ↓ +JWT Filter + ↓ +토큰 검증 + ↓ +Authentication 재생성 + ↓ +SecurityContext에 다시 저장 (요청 동안만) +``` + +요청 끝나면 **SecurityContext는 사라짐** + + +## 그래서 정리하자면 + +> 일반적으로 세션과 토큰의 차이는 상태를 서버에 저장하느냐에 있다. +> +> +> Spring Security 관점에서 세션 기반 인증은 인증 완료 후 `SecurityContext`를 `HttpSession`에 저장함으로써 서버가 사용자의 인증 상태를 기억하는 **Stateful 방식**이다. +> +> 반면 토큰 기반 인증은 로그인 시 토큰을 발급하는 시점에만 서버가 개입하며, 이후 요청부터는 클라이언트가 전달한 토큰 자체가 인증과 인가를 대체하여 동작하는 **Stateless 방식**이다. +> + +| 구분 | 세션 | 토큰(JWT) | +| --- | --- | --- | +| 인증 상태 저장 | 서버 | 클라이언트 | +| 서버 상태 | 있음 (Stateful) | 없음 (Stateless) | +| 요청 간 의존 | 있음 | 없음 | +| 확장성 | 낮음 | 높음 | \ No newline at end of file diff --git a/keyword/chapter10/keyword_spring_security.md b/keyword/chapter10/keyword_spring_security.md new file mode 100644 index 0000000..1b7d914 --- /dev/null +++ b/keyword/chapter10/keyword_spring_security.md @@ -0,0 +1,137 @@ +# Spring Security +**Spring Security**는 스프링 기반 애플리케이션의 **인증(Authentication), 인가(Authorization) 및 일반적인 공격에 대한 보호**를 담당하는 강력하고 커스터마이징 가능한 보안 프레임워크입니다. + +쉽게 말해, 내 애플리케이션에 **"누가 들어올 수 있는지"** 를 확인하고, **“그 사람이 무엇을 할 수 있는지"** 를 통제하며, **"외부의 나쁜 공격"** 으로부터 서비스를 지키는 '보안 종합 세트'라고 이해하면 됩니다. + + + +## 1. Spring Security의 3가지 핵심 역할 + +1. **인증 (Authentication)** + - "이 사용자가 누구인가?"를 확인하는 과정입니다. + - 아이디/비밀번호 로그인, 소셜 로그인(OAuth2), 생체 인증 등이 이에 해당합니다. +2. **인가 (Authorization)** + - "인증된 사용자가 특정 리소스에 접근할 권한이 있는가?"를 결정합니다. + - 예: 일반 유저는 '게시글 읽기'만 가능하고, 관리자는 '게시글 삭제'까지 가능한 권한 제어입니다. +3. **보안 공격 방어** + - CSRF(사이트 간 요청 위조), 세션 고정(Session Fixation) 공격 등 웹 취약점으로부터 애플리케이션을 기본적으로 보호합니다. + + +## 2. Spring Security의 핵심 동작 원리: 필터 체인(Filter Chain) + +Spring Security는 애플리케이션의 서블릿(Servlet) 앞에 **여러 개의 필터(Filter)**를 층층이 쌓아놓은 형태로 동작합니다. 사용자의 요청이 컨트롤러에 도달하기 전에 이 '필터 터널'을 무사히 통과해야 합니다. + +![image.png](./image/image1.png) + +Spring Security는 요청을 처리할 때 `SecurityFilterChain`(필터 체인)을 순서대로 통과시키는 구조이며, `SecurityConfig`에서 설정을 변경하면 이 체인에 포함되는 필터의 **활성화 여부**나 **추가되는 필터**, 그리고 **필터의 배치 순서**가 달라진다. + +### **SecurityConfig Example** + +```java +public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(requests -> requests + .requestMatchers(allowUris).permitAll() + .requestMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() + ) + .formLogin(AbstractHttpConfigurer::disable) + .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) + .csrf(AbstractHttpConfigurer::disable) + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .permitAll() + ) + .exceptionHandling(exception -> exception.authenticationEntryPoint(authenticationEntryPoint())) + + ; + + return http.build(); + } +``` + +| **순서** | **필터 이름 (Filter Name)** | **역할 및 동작 설명** | **비고** | +| --- | --- | --- | --- | +| **1** | **`SecurityContextPersistenceFilter`** | **[복원]** 기존 세션이 있다면 인증 정보를 꺼내와서 `SecurityContextHolder`에 채워줌. | 모든 요청의 시작 | +| **2** | **`LogoutFilter`** | **[로그아웃]** 설정하신 `/logout` 경로로 요청이 오면 세션을 무효화하고 로그아웃 처리. | 코드 내 `.logout()` 설정 | +| **3** | **`JwtAuthFilter`** | **[커스텀 인증]** 헤더의 JWT를 검사해서 유효하면 유저 정보를 `SecurityContextHolder`에 저장. | **직접 추가한 필터** | +| **4** | `UsernamePasswordAuthenticationFilter` | **[기존 인증]** 원래는 폼 로그인을 처리하지만, `JwtAuthFilter` 뒤에 위치하며 사실상 하는 일 없음. | `.formLogin(disable)` | +| **5** | **`AnonymousAuthenticationFilter`** | **[익명 권한]** 앞선 필터들에서 인증이 안 됐다면 '익명 사용자' 권한을 부여함. | 필터 체인 기본 구성 | +| **6** | **`ExceptionTranslationFilter`** | **[예외 처리]** 뒤쪽 필터에서 터지는 인증/인가 에러를 잡아서 응답을 처리함. | `.exceptionHandling()` | +| **7** | **`FilterSecurityInterceptor`** | **[인가/최종]** 설정하신 `.requestMatchers()` 규칙에 따라 실제 접근 권한을 최종 판단. | **보안의 마지막 관문** | + +결국 **Spring Security**는 거대한 **'필터의 사슬(Filter Chain)**'이고, 그 안에서 일어나는 **인증(Authentication)** 과 **인가(Authorization)** 도 특정 필터가 자기 차례가 왔을 때 **조건(요청 경로, 토큰 유무, 권한 등)에 따라 실행**하는 하나의 '작업'일 뿐이다. + + +## 3. Spring Security의 기본 보안 방어 구조 + +Spring Security는 웹 애플리케이션에서 빈번하게 발생하는 주요 보안 공격들을 **기본 설정**으로 방어해 주며, + +개발자는 `SecurityConfig`를 통해 필터의 활성화 여부를 설정하여 이러한 보안 기능을 선택적으로 켜거나 끌 수 있다. + +> 이러한 보안 필터들은 대부분 **인증 필터보다 앞쪽**에서 동작한다. +> + +이는 사용자의 신원을 확인하기 전에, **요청 자체가 안전한지 먼저 검증**해야 하기 때문이다. + +Spring Security가 필터 단계에서 방어하는 대표적인 공격 유형은 다음과 같다. + + + +### CSRF (Cross-Site Request Forgery, 사이트 간 요청 위조) + +--- + +사용자가 의도하지 않았음에도, 공격자가 심어놓은 요청을 통해 비밀번호 변경과 같은 민감한 작업이 수행되는 공격이다. + +**방어 필터 :** `CsrfFilter` + +**동작 방식** + +서버는 클라이언트에 임의의 CSRF 토큰을 발급하고, 상태 변경 요청 시 해당 토큰이 함께 전달되는지를 검증하여 요청의 정당성을 확인한다. + + +JWT 기반의 REST API에서는 서버 세션을 사용하지 않기 때문에 CSRF 공격 가능성이 낮아, 성능 및 구조 단순화를 위해 `.csrf().disable()` 처리하는 경우가 많다. + +### XSS (Cross-Site Scripting) 완화 + +--- + +게시글이나 입력 폼 등에 악성 스크립트를 삽입하여, 다른 사용자의 쿠키 탈취나 세션 하이재킹을 시도하는 공격이다. + +**방어 필터 :** `HeaderWriterFilter` + +**동작 방식** + +Spring Security는 XSS 공격을 직접 차단하기보다는, 브라우저가 악성 스크립트를 실행하기 어렵도록 다음과 같은 **보안 헤더를 응답에 자동으로 추가**하여 공격을 완화한다. + +- `Content-Security-Policy` +- `X-XSS-Protection` +- `X-Content-Type-Options` + +### Clickjacking (클릭재킹) + +--- + +정상 웹 페이지를 투명한 iframe으로 감싸고, 사용자가 의도하지 않은 버튼을 클릭하도록 유도하는 공격이다. + +**방어 필터 :** `HeaderWriterFilter` + +**동작 방식** + +응답 헤더에 `X-Frame-Options: DENY` 또는 `SAMEORIGIN`을 설정하여해당 페이지가 다른 사이트의 `