@@ -0,0 +1,85 @@
+package nextstep.authentication.filter;
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.authentication.Authentication;
+import nextstep.authentication.AuthenticationManager;
+import nextstep.authentication.UsernamePasswordAuthenticationToken;
+import nextstep.authentication.context.SecurityContextHolder;
+import nextstep.authentication.exception.AuthenticationException;
+import nextstep.authentication.util.Base64Convertor;
+import nextstep.authentication.util.matcher.MvcRequestMatcher;
+import org.springframework.http.HttpMethod;
+
+import java.io.IOException;
+
+public class BasicAuthenticationFilter implements Filter {
+
+    private static final MvcRequestMatcher DEFAULT_REQUEST_MATCHER = new MvcRequestMatcher(HttpMethod.GET, "/members");
+
+    private final AuthenticationManager authenticationManager;
+
+    public BasicAuthenticationFilter(AuthenticationManager authenticationManager) {
+        this.authenticationManager = authenticationManager;
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+        if (!DEFAULT_REQUEST_MATCHER.matches((HttpServletRequest) servletRequest)) {
+            filterChain.doFilter(servletRequest, servletResponse);
+            return;
+        }
+
+        HttpServletRequest request = (HttpServletRequest) servletRequest;
+        HttpServletResponse response = (HttpServletResponse) servletResponse;
+
+        try {
+
+            Authentication authenticationResult = attemptAuthentication(request, response);
+            if (authenticationResult == null) {
+                filterChain.doFilter(servletRequest, servletResponse);
+                return;
+            }
+
+            SecurityContextHolder.getContext().setAuthentication(authenticationResult);
+
+        } catch (Exception e) {
+            SecurityContextHolder.clearContext();
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+
+            return;
+        }
+
+        filterChain.doFilter(servletRequest, servletResponse);
+    }
+
+    private Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
+
+        UsernamePasswordAuthenticationToken authRequest;
+
+        var securityContext = SecurityContextHolder.getContext();
+
+        if (securityContext.getAuthentication() == null) {
+
+            String authorization = request.getHeader("Authorization");
+            String credentials = authorization.split(" ")[1];
+            String decodedString = Base64Convertor.decode(credentials);
+            String[] usernameAndPassword = decodedString.split(":");
+
+            String username = usernameAndPassword[0];
+            String password = usernameAndPassword[1];
+
+            authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
+
+        } else {
+            authRequest = (UsernamePasswordAuthenticationToken) securityContext.getAuthentication();
+        }
+
+        return authenticationManager.authenticate(authRequest);
+    }
+}

리뷰: `attempAuthentication`하는 동작은 request에서 헤더를 보고 토큰을 읽어 `authenticationManager.authenticate`를 실행하는 과정인데요. 이 과정에서 `SecurityContext`는 사실 불필요한 동작입니다.

현재 필터에서의 동작은
1. 헤더에 원하는 값을 뽑아내어 토큰을 생성한다.
2. 토큰을 가지고 `AuthenticationManager.authenticate`를 실행한다.
3. 성공하면 SecurityContext에 인증 값을 넣는다.
4. 실패하면 초기화하고 401을 반환한다.

입니다. 즉, `SecurityContext`를 조작하는 행위는 모든 인증 과정이 끝나고 성공과 실패에 따른 행위이지 그 중간에 `SecurityContext`를 건들진 않아요.