@@ -0,0 +1,75 @@
+package nextstep.security.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.AuthenticationConverter;
+import nextstep.security.authentication.AuthenticationErrorHandler;
+import nextstep.security.authentication.AuthenticationManager;
+import nextstep.security.authentication.exception.AuthenticationException;
+import nextstep.security.context.SecurityContextHolder;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+
+public class BasicAuthenticationFilter extends OncePerRequestFilter {
+
+    private final AuthenticationManager authenticationManager;
+    private final AuthenticationConverter converter = new AuthenticationConverter();
+
+    private static final String AUTHORIZATION_HEADER = "Authorization";
+    private static final String[] BASIC_AUTH_PATH = new String[]{"/members"};
+
+    public BasicAuthenticationFilter(AuthenticationManager authenticationManager) {
+        this.authenticationManager = Objects.requireNonNull(authenticationManager);
+    }
+
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        boolean isNotGetMethod = !HttpMethod.GET.name().equalsIgnoreCase(request.getMethod());
+        boolean shouldNotFilterURI = Arrays.stream(BASIC_AUTH_PATH).noneMatch(it -> it.equalsIgnoreCase(request.getRequestURI()));
+        return isNotGetMethod || shouldNotFilterURI;
+    }
+
+    @Override
+    protected void doFilterInternal(
+            HttpServletRequest request,
+            HttpServletResponse response,
+            FilterChain filterChain
+    ) throws IOException, ServletException {
+
+        try {
+            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+            if (authentication != null) {
+                filterChain.doFilter(request, response);
+                return;
+            }
+
+            authentication = authenticateByRequestHeader(request);
+
+            SecurityContextHolder.getContext().setAuthentication(authentication);
+
+        } catch (AuthenticationException e) {
+            AuthenticationErrorHandler.handleError(response, e);
+            return;
+        }
+
+        filterChain.doFilter(request, response);
+    }
+
+    private Authentication authenticateByRequestHeader(HttpServletRequest request) {
+        Authentication authentication = converter.convert(request.getHeader(AUTHORIZATION_HEADER));
+
+        Authentication result = authenticationManager.authenticate(authentication);
+        if (!result.isAuthenticated()) {
+            throw new AuthenticationException();
+        }
+
+        return result;
+    }
+}

리뷰: OncePerRequestFilter를 사용하는 이유는 요청 당 한번만 실행하기 위해서입니다.

이 필터에서는 ServeltRequset에 attribute를 설정하는데 hasAlreadyFilteredAttribute 메서드를 통해 요청내 에서 한번 실행했다면 이 필터를 무시합니다.
만약 요청에 속성이 없다면, 실행을 성공적으로 마친 후 요청에 필터이름.FILTERED 속성에 TRUE 값을 설정해줘서 같은 요청내에 다시 실행되지 않게 막아줍니다.

비동기 요청이나 에러처리 부분도 있지만, OncePerRequestFilter를 상속받는 가장 큰 이유는 한번 실행 보장이라고 생각이 드네요.