아래는 "스프링 시큐리티" 책의 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분으로 설정하고, 만료 후 동작 테스트.

---

위 내용은 커스텀 로그인/로그아웃 구현과 세션 관리 방법을 실습 가능하도록 설명했습니다. 추가적인 예제나 설정이 필요하면 말씀해 주세요!