2025-04-08T19:56:24
This commit is contained in:
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분으로 설정하고, 만료 후 동작 테스트.
|
||||
|
||||
---
|
||||
|
||||
위 내용은 커스텀 로그인/로그아웃 구현과 세션 관리 방법을 실습 가능하도록 설명했습니다. 추가적인 예제나 설정이 필요하면 말씀해 주세요!
|
||||
Reference in New Issue
Block a user