문제 상황
RestAuthenticationFilter는 REST API 기반 인증을 처리하기 위한 커스텀 필터로, RESTful 방식에서 세션 기반 인증과 달리, 주로 토큰 기반 인증이나 비동기 요청을 처리하는 데 사용된다. Spring Security에서 제공하는 기본적인 UsernamePasswordAuthenticationFilter와 유사하지만, REST API에 맞게 커스터마이징 되어 동작하는 것이 핵심이다.
RestAuthenticationFilter
1. Form 기반 인증과 차이점
- 기본적으로 Spring Security는 UsernamePasswordAuthenticationFilter를 사용하여 username과 password로 로그인 폼에서 입력한 데이터를 인증한다. 이 과정에서 세션을 사용하여 인증 상태를 유지한다.
- 반면에 RestAuthenticationFilter는 주로 비동기 인증 또는 토큰 기반 인증을 위해 설계되며, RESTful API의 특성상 세션을 사용하지 않고 요청마다 인증 정보를 전달하는 방식을 따른다.
- 로그인 요청이 REST API 요청으로 들어오면, RestAuthenticationFilter는 요청 본문에서 JSON 데이터를 파싱하거나 HTTP 헤더에서 토큰을 추출하여 인증을 진행할 수 있다.
2. 인증 처리
RestAuthenticationFilter는 기본적으로 요청에서 인증 정보를 추출하고, 이를 바탕으로 AuthenticationManager를 통해 인증을 시도한다. 일반적으로, JSON 데이터를 파싱하거나 HTTP 헤더에서 인증 토큰을 추출하는 방식으로 인증을 진행한다.
인증 과정은 다음과 같은 절차를 따른다:
- 요청에서 인증 정보 추출: 클라이언트가 POST 요청으로 전송한 JSON 데이터나 헤더에서 사용자 이름 및 비밀번호, 또는 토큰을 추출한다.
- AuthenticationManager에 인증 위임: 추출한 정보를 바탕으로 인증을 시도하며, 이때 AuthenticationManager에 인증을 위임한다.
- 성공/실패 처리: 인증이 성공하면 인증된 사용자 정보를 반환하고, 실패하면 예외를 던진다. REST API에서는 주로 HTTP 상태 코드를 통해 성공(200 OK) 또는 실패(401 Unauthorized)를 나타낸다.
3. Spring Security와의 통합
RestAuthenticationFilter는 일반적으로 Spring Security의 인증 흐름에 맞춰 커스텀 구현된다. 특히 RESTful API에서는 CSRF 보호가 필요 없기 때문에, CSRF 비활성화와 함께 동작하는 경우가 많다. 또한, 세션 사용을 비활성화하여 매 요청마다 인증을 처리하는 무상태(stateless) 방식을 유지한다.
4. JSON 데이터 처리
RestAuthenticationFilter는 요청 본문에서 JSON 데이터를 읽어들이고, 이를 파싱하여 사용자 이름과 비밀번호를 추출할 수 있다. 이는 클라이언트가 HTML 폼 대신 AJAX 요청 또는 JSON 요청으로 로그인할 때 유용하다.
코드 설명
public class RestAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private final ObjectMapper objectMapper = new ObjectMapper();
// JSON 데이터를 파싱하기 위해 Jackson의 ObjectMapper를 사용.
public RestAuthenticationFilter() {
super(new AntPathRequestMatcher("/api/login", "POST"));
// 이 필터는 "/api/login" 경로로 들어오는 POST 요청에 대해 작동하도록 설정.
// AntPathRequestMatcher는 특정 경로와 HTTP 메서드와 매칭하여 필터를 적용하는 역할.
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
// POST 메서드인지 확인하고, AJAX 요청인지 확인 (비동기 요청이어야 한다는 조건을 검증).
if(!HttpMethod.POST.name().equals(request.getMethod()) || !WebUtil.isAjax(request)){
throw new IllegalArgumentException("Authentication method is not supported");
// 요청이 POST 메서드가 아니거나, AJAX 요청이 아니면 예외를 던져 인증이 지원되지 않음을 알림.
}
// 요청 본문을 JSON 형식으로 읽어서 AccountDto 객체로 변환 (사용자의 입력 데이터를 파싱).
AccountDto accountDto = objectMapper.readValue(request.getReader(), AccountDto.class);
// 사용자 이름이 없거나 비밀번호가 없는 경우 인증 오류 발생. (필수 값 검증)
if(StringUtils.hasText(accountDto.getUsername()) || !StringUtils.hasText(accountDto.getPassword())){
throw new AuthenticationServiceException("Username or Password is not provided");
// 사용자 이름이나 비밀번호가 제공되지 않으면 예외를 던짐.
}
// 인증 토큰 생성. 입력받은 사용자 이름과 비밀번호를 기반으로 RestAuthenticationToken 객체 생성.
RestAuthenticationToken authenticationToken = new RestAuthenticationToken(accountDto.getUsername(), accountDto.getPassword());
// AuthenticationManager를 통해 인증을 위임. 토큰을 전달하여 실제 인증 로직 수행.
return getAuthenticationManager().authenticate(authenticationToken);
}
}