아래는 "스프링 시큐리티" 책의 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 인증 설계를 실습 가능하도록 설명했습니다. 추가적인 예제나 세부 사항이 필요하면 말씀해 주세요!