문제 상황
Spring Security에서 REST API와 비동기 인증은 주로 JWT(JSON Web Token) 같은 토큰 기반 인증을 사용하여 세션을 유지하지 않는 방식으로 동작한다. 비동기 인증은 웹 페이지의 리프레시 없이 데이터를 송수신할 수 있는 환경, 즉 주로 AJAX 호출이나 REST API 호출에서 자주 사용된다.
Rest 인증 보안
세션 기반 인증에서 비동기 방식
세션 기반 인증에서 비동기 방식으로 동작하는 인증은 주로 세션 쿠키를 통해 이루어진다. 웹 애플리케이션에서 로그인 후 서버가 클라이언트에게 세션 ID를 포함한 쿠키를 전달하고, 클라이언트는 이후 비동기 요청에서 이 쿠키를 계속해서 포함시켜 서버에 인증을 요청한다.
1. 세션 기반 인증 흐름
- 로그인 요청: 클라이언트는 로그인 폼을 통해 사용자 이름과 비밀번호를 서버로 POST 요청으로 보낸다.
- 세션 생성 및 세션 ID 발급: 서버는 Spring Security를 통해 인증을 진행하고, 인증에 성공하면 서버 측에서 세션을 생성하고 클라이언트에게 세션 ID가 포함된 쿠키를 발급한다. 이 쿠키는 JSESSIONID 같은 이름을 가질 수 있다.
- 비동기 요청에서 쿠키 사용: 이후 클라이언트는 AJAX 호출이나 다른 비동기 방식으로 REST API 요청을 할 때, 브라우저는 자동으로 이 세션 ID를 쿠키에 포함해 요청한다. 서버는 세션 ID를 통해 사용자가 인증된 상태인지 확인하고 요청을 처리한다.
- 서버에서 세션 상태 유지: 서버는 클라이언트의 세션 상태를 메모리나 데이터베이스에 저장하고, 세션 ID를 이용해 사용자를 추적한다. 각 요청 시마다 세션 정보를 참조하여 사용자의 인증 상태를 확인하고 API 요청을 처리한다.
2. Spring Security에서 세션 기반 인증 설정
Spring Security의 기본 설정은 세션 기반 인증이다. 특별한 설정이 없으면 인증 후 자동으로 세션을 생성하고 이를 관리한다.
3. 비동기 요청에서의 쿠키 처리
비동기 요청 시 클라이언트는 세션 ID를 자동으로 서버에 전송할 수 있는데, 이는 브라우저의 기본 쿠키 처리 방식 때문이다. 예를 들어, 다음과 같은 AJAX 요청을 생각해볼 수 있다.
4. 세션 기반 인증의 장점과 단점
- 장점:
- 서버에서 세션을 관리하기 때문에 JWT와 달리 클라이언트에서 토큰을 별도로 저장할 필요가 없다.
- 서버에서 세션 정보를 관리하므로, 세션 만료나 사용자 로그아웃 등의 작업이 비교적 간단하다.
- 단점:
- 스케일링 문제: 세션 기반 인증에서는 서버가 모든 세션 정보를 유지해야 하므로 서버가 여러 대로 확장되면 세션 정보를 공유하는 것이 복잡해진다. 이를 해결하기 위해 Redis 같은 중앙 세션 저장소를 사용할 수 있다.
- 비동기 통신에서의 제약: AJAX 요청 등 비동기 방식에서도 세션을 사용하려면 쿠키 기반으로 동작하므로, CORS나 쿠키 설정 등 추가적인 보안 조치가 필요하다.
5. CSRF 보호와 비동기 요청
세션 기반 인증에서는 CSRF(Cross-Site Request Forgery) 공격에 취약할 수 있다. 특히 비동기 요청을 사용할 때 CSRF 공격에 대비하기 위해 CSRF 토큰을 사용해야 한다. Spring Security는 기본적으로 CSRF 보호를 제공하며, 비동기 요청을 보낼 때 클라이언트가 CSRF 토큰을 함께 전송해야 한다.
코드 설명
Rest인증은 클라이언트에서 JS로 CSRF값을 직접 전달해줘야 하고 먼저 비활성화 했다.
@Bean
@Order(1) // SecurityFilterChain의 우선순위를 설정. 숫자가 낮을수록 높은 우선순위로 적용된다.
public SecurityFilterChain restSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("api/login") // 특정 경로(api/login)에만 이 Security 설정을 적용.
.authorizeHttpRequests(auth -> auth
// 정적 리소스 경로에 대해 모든 사용자에게 접근을 허용. (css, 이미지, js 파일 등)
.requestMatchers("/css/**", "/images/**", "/js/**", "/favicon.*", "/*/icon-*").permitAll()
// 그 외의 모든 요청에 대해서도 접근을 허용.
.anyRequest().permitAll())
// CSRF 보호 기능을 비활성화. 일반적으로 REST API에서는 CSRF 보호가 필요하지 않기 때문에 비활성화.
.csrf(AbstractHttpConfigurer::disable)
;
return http.build(); // 설정을 마친 SecurityFilterChain 객체를 반환.
}