ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,58 @@
+package nextstep.app.config;
+
+import nextstep.security.authentication.*;
+import nextstep.security.context.HttpSessionSecurityContextRepository;
+import nextstep.security.context.SecurityContextRepository;
+import nextstep.security.domain.MemberDetailService;
+import nextstep.security.filter.*;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.filter.DelegatingFilterProxy;
+
+import java.util.List;
+
+@Configuration
+public class SecurityConfig {
+
+    private final MemberDetailService memberDetailService;
+
+    public SecurityConfig(MemberDetailService memberDetailService) {
+        this.memberDetailService = memberDetailService;
+    }
+
+    @Bean
+    public DelegatingFilterProxy delegatingFilterProxy() {
+        return new DelegatingFilterProxy(filterChainProxy(List.of(securityFilterChain())));
+    }
+
+    @Bean
+    public FilterChainProxy filterChainProxy(List<SecurityFilterChain> securityFilterChains) {
+        return new FilterChainProxy(securityFilterChains);
+    }
+
+    @Bean
+    public SecurityFilterChain securityFilterChain() {
+        return new DefaultSecurityFilterChain(
+                List.of(new ExceptionHandlerFilter(),
+                        new SecurityContextHolderFilter(securityContextRepository()),
+                        new BasicAuthenticationFilter(authenticationManager()),
+                        new LoginAuthenticationFilter(authenticationManager())));
+
+    }
+
+    @Bean
+    public AuthenticationManager authenticationManager() {
+        List<AuthenticationProvider> providers = List.of(daoAuthenticationProvider());
+        return new ProviderManager(providers);
+    }
+
+    @Bean
+    public DaoAuthenticationProvider daoAuthenticationProvider() {
+        return new DaoAuthenticationProvider(memberDetailService, new BasicPasswordMatcher());
+    }
+
+    @Bean
+    public SecurityContextRepository securityContextRepository() {
+        return new HttpSessionSecurityContextRepository();
+    }
+}

리뷰: 필터의 순서는 중요합니다!
SecurityContextHolderFilter는 BasicAuthenticationFilter, LoginAuthenticationFilter보다 먼저 설정되어야 할 것 같아요~
@@ -0,0 +1,77 @@
+package nextstep.security.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.AuthenticationManager;
+import nextstep.security.authentication.UsernamePasswordAuthenticationToken;
+import nextstep.security.context.SecurityContext;
+import nextstep.security.context.SecurityContextHolder;
+import nextstep.security.exception.AuthenticationException;
+import nextstep.security.util.Base64Convertor;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+
+public class BasicAuthenticationFilter extends GenericFilterBean {
+
+    private final AuthenticationManager authenticationManager;
+
+    public BasicAuthenticationFilter(AuthenticationManager authenticationManager) {
+        this.authenticationManager = authenticationManager;
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+
+        if (notTarget(httpRequest)) {
+            chain.doFilter(request, response);
+            return;
+        }
+
+        Authentication resultAuthentication = authenticationManager.authenticate(getAuthenticationFrom(httpRequest));
+        SecurityContextHolder.getContext().setAuthentication(resultAuthentication);
+        chain.doFilter(request, response);
+    }
+
+    private static boolean notTarget(HttpServletRequest httpRequest) {
+        return !httpRequest.getRequestURI().startsWith("/members");
+    }
+
+    private static Authentication getAuthenticationFrom(HttpServletRequest httpRequest) {
+        SecurityContext context = SecurityContextHolder.getContext();
+        Authentication authentication = context.getAuthentication();
+        if (authentication != null) {
+            return authentication;
+        }
+
+        String basicToken = extractToken(httpRequest);
+        String[] usernameAndPassword = splitToken(basicToken);
+
+        String username = usernameAndPassword[0];
+        String password = usernameAndPassword[1];
+        return new UsernamePasswordAuthenticationToken(username, password);
+    }
+
+    private static String extractToken(HttpServletRequest httpRequest) {
+        String authorization = httpRequest.getHeader("Authorization");
+        return authorization.split(" ")[1];
+    }
+
+    private static String[] splitToken(String basicToken) {
+        String decodedString = Base64Convertor.decode(basicToken);
+        String[] usernameAndPassword = decodedString.split(":");
+        validateToken(usernameAndPassword);
+        return usernameAndPassword;
+    }
+
+    private static void validateToken(String[] usernameAndPassword) {
+        if (usernameAndPassword.length != 2) {
+            throw new AuthenticationException();
+        }
+    }
+}

리뷰: 각 필터에서 SecurityContext를 사용할 때는 SecurityContextHolder를 통해 사용하면 어떨까요?
@@ -0,0 +1,56 @@
+package nextstep.security.filter;
+
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.exception.FilterChainNotFoundException;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+import java.util.List;
+
+public class FilterChainProxy extends GenericFilterBean {
+
+    private final List<SecurityFilterChain> filterChains;
+
+    public FilterChainProxy(List<SecurityFilterChain> filterChains) {
+        this.filterChains = filterChains;
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
+        List<Filter> filters = getFilters(httpRequest);
+        VirtualFilterChain virtualFilterChain = new VirtualFilterChain(filters, chain);
+        virtualFilterChain.doFilter(servletRequest, servletResponse);
+    }
+
+    private List<Filter> getFilters(HttpServletRequest httpRequest) {
+        return filterChains.stream()
+                .filter(filterChain -> filterChain.matches(httpRequest))
+                .findFirst()
+                .map(SecurityFilterChain::filters)
+                .orElseThrow(FilterChainNotFoundException::new);
+    }
+
+    public static class VirtualFilterChain implements FilterChain {
+
+        private final List<Filter> filters;
+        private final FilterChain originalChain;
+        private int currentPosition = 0;
+
+        public VirtualFilterChain(List<Filter> filters, FilterChain originalChain) {
+            this.filters = filters;
+            this.originalChain = originalChain;
+        }
+
+        @Override
+        public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
+            if (currentPosition >= filters.size()) {
+                originalChain.doFilter(request, response);
+            } else {
+                Filter filter = filters.get(currentPosition++);
+                filter.doFilter(request, response, this);
+            }
+        }
+    }
+}

리뷰: 지금은 모든 체인의 필터를 실행하고 있는데요. 지금 프로젝트에선 하나의 필터 체인만 사용하고 있지만, 여러 필터 체인을 사용할 수 경우 문제가 생길 수 있을 것 같은데 어떻게 생각하시나요?
@@ -0,0 +1,67 @@
+package nextstep.security.filter;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.exception.AuthenticationException;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+public class ExceptionHandlerFilter extends OncePerRequestFilter {
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException {
+        try {
+            chain.doFilter(request, response);
+        } catch (Exception e) {
+            createErrorResponse(e, response);
+        }
+    }
+
+    private void createErrorResponse(Throwable e, HttpServletResponse response) throws IOException {
+        if (e instanceof Exception) {
+            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
+        }
+
+        if (e instanceof AuthenticationException) {
+            response.setStatus(HttpStatus.UNAUTHORIZED.value());
+        }
+
+        if (e instanceof IllegalArgumentException) {
+            response.setStatus(HttpStatus.BAD_REQUEST.value());
+        }
+
+        ErrorResponse errorResponse = new ErrorResponse(response.getStatus(), e.getMessage());
+        String errorResponseJson = errorResponse.toJson();
+        response.getWriter().write(errorResponseJson);
+    }
+
+    public static class ErrorResponse {
+
+        private static final ObjectMapper objectMapper = new ObjectMapper();
+
+        private final Integer code;
+        private final String message;
+
+        public ErrorResponse(Integer code, String message) {
+            this.code = code;
+            this.message = message;
+        }
+
+        public Integer getCode() {
+            return code;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+
+        public String toJson() throws JsonProcessingException {
+            return objectMapper.writeValueAsString(this);
+        }
+    }
+}

리뷰: > 예외 발생 시 clearContext는 ExceptionHandlerFilter에서 수행하도록 구현했고, 요청 완료 후 clearContext는 SecurityContextHolderFilter에서 수행하도록 구현했습니다. 이게 맞는 방식인지 궁금합니다.

Filter의 역할을 잘 나눠주셨어요 👍 

조금 더 코드를 발전시켜 본다면 예외 상황에 따른 예외 처리가 추가되고, SecurityContextHolder와 관련된 모든 역할은 SecurityContextHolderFilter에서 담당하도록 해보면 좋을 것 같습니다!
@@ -0,0 +1,56 @@
+package nextstep.security.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.AuthenticationManager;
+import nextstep.security.authentication.UsernamePasswordAuthenticationToken;
+import nextstep.security.context.SecurityContextHolder;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+import java.util.Map;
+
+public class LoginAuthenticationFilter extends GenericFilterBean {
+
+    private final AuthenticationManager authenticationManager;
+
+    public LoginAuthenticationFilter(AuthenticationManager authenticationManager) {
+        this.authenticationManager = authenticationManager;
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+
+        if (notTarget(httpRequest)) {
+            chain.doFilter(request, response);
+            return;
+        }
+
+        Authentication authentication = authenticationManager.authenticate(getAuthentication(httpRequest));
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+        chain.doFilter(request, response);
+    }
+
+    private static boolean notTarget(HttpServletRequest httpRequest) {
+        return !httpRequest.getRequestURI().startsWith("/login");
+    }
+
+    private static UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest httpRequest) {
+        Map<String, String[]> parameterMap = httpRequest.getParameterMap();
+        validateParameter(parameterMap);
+        String username = parameterMap.get("username")[0];
+        String password = parameterMap.get("password")[0];
+        return new UsernamePasswordAuthenticationToken(username, password);
+    }
+
+    private static void validateParameter(Map<String, String[]> parameterMap) {
+        if (parameterMap.get("username") == null || parameterMap.get("password") == null) {
+            throw new IllegalArgumentException("Missing required parameter");
+        }
+    }
+}

리뷰: SPRING_SECURITY_CONTEXT_KEY를 기반으로 처리하는 역할은 SecurityContextRepository에게 넘겨주면 좋겠네요!
@@ -0,0 +1,39 @@
+package nextstep.security.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.security.context.SecurityContext;
+import nextstep.security.context.SecurityContextHolder;
+import nextstep.security.context.SecurityContextRepository;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+
+public class SecurityContextHolderFilter extends GenericFilterBean {
+
+    private final SecurityContextRepository securityContextRepository;
+
+    public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) {
+        this.securityContextRepository = securityContextRepository;
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+        SecurityContext securityContext = securityContextRepository.loadContext(httpRequest);
+        SecurityContextHolder.setContext(securityContext);
+
+        try {
+            chain.doFilter(httpRequest, response);
+        } finally {
+            securityContextRepository.saveContext(securityContext, httpRequest, httpResponse);
+            SecurityContextHolder.clearContext();
+        }
+    }
+}

리뷰: > 4단계 SecurityContextHolderFilter 구현 중 clearContext를 SecurityContextHolderFilter에서 수행하는 게 맞다고 생각했는데, clearContext를 수행하면 테스트가 실패합니다. 일단 제출을 위해 주석처리했는데, 테스트가 실패하는 이유가 궁금합니다.

```suggestion
            securityContextRepository.saveContext(securityContext, httpRequest, httpResponse);
            SecurityContextHolder.clearContext();
```

위와 같이 수정하면 잘 동작할 것 같은데 왜 그럴지 고민해보아요 :)
@@ -0,0 +1,58 @@
+package nextstep.app.config;
+
+import nextstep.security.authentication.*;
+import nextstep.security.context.HttpSessionSecurityContextRepository;
+import nextstep.security.context.SecurityContextRepository;
+import nextstep.security.domain.MemberDetailService;
+import nextstep.security.filter.*;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.filter.DelegatingFilterProxy;
+
+import java.util.List;
+
+@Configuration
+public class SecurityConfig {
+
+    private final MemberDetailService memberDetailService;
+
+    public SecurityConfig(MemberDetailService memberDetailService) {
+        this.memberDetailService = memberDetailService;
+    }
+
+    @Bean
+    public DelegatingFilterProxy delegatingFilterProxy() {
+        return new DelegatingFilterProxy(filterChainProxy(List.of(securityFilterChain())));
+    }
+
+    @Bean
+    public FilterChainProxy filterChainProxy(List<SecurityFilterChain> securityFilterChains) {
+        return new FilterChainProxy(securityFilterChains);
+    }
+
+    @Bean
+    public SecurityFilterChain securityFilterChain() {
+        return new DefaultSecurityFilterChain(
+                List.of(new ExceptionHandlerFilter(),
+                        new SecurityContextHolderFilter(securityContextRepository()),
+                        new BasicAuthenticationFilter(authenticationManager()),
+                        new LoginAuthenticationFilter(authenticationManager())));
+
+    }
+
+    @Bean
+    public AuthenticationManager authenticationManager() {
+        List<AuthenticationProvider> providers = List.of(daoAuthenticationProvider());
+        return new ProviderManager(providers);
+    }
+
+    @Bean
+    public DaoAuthenticationProvider daoAuthenticationProvider() {
+        return new DaoAuthenticationProvider(memberDetailService, new BasicPasswordMatcher());
+    }
+
+    @Bean
+    public SecurityContextRepository securityContextRepository() {
+        return new HttpSessionSecurityContextRepository();
+    }
+}

리뷰: SecurityContextHolderFilter 가 먼저 실행이 되어야 요청이 시작하고 끝날 때 SecurityContext를 적절하게 처리할 수 있겠네요. 제가 이해한 부분이 맞을까요?
@@ -0,0 +1,39 @@
+package nextstep.security.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.security.context.SecurityContext;
+import nextstep.security.context.SecurityContextHolder;
+import nextstep.security.context.SecurityContextRepository;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+
+public class SecurityContextHolderFilter extends GenericFilterBean {
+
+    private final SecurityContextRepository securityContextRepository;
+
+    public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) {
+        this.securityContextRepository = securityContextRepository;
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+        SecurityContext securityContext = securityContextRepository.loadContext(httpRequest);
+        SecurityContextHolder.setContext(securityContext);
+
+        try {
+            chain.doFilter(httpRequest, response);
+        } finally {
+            securityContextRepository.saveContext(securityContext, httpRequest, httpResponse);
+            SecurityContextHolder.clearContext();
+        }
+    }
+}

리뷰: 이유를 잘 모르겠네요.. ㅠㅠ 
근데 수정하다가 든 생각인데, 각 필터에서 `securityContextRepository.saveContext` 를 수행하고 있으니까 SecurityContextHolderFilter에서는 saveContext 수행을 안 해도 되지 않나요?
@@ -0,0 +1,56 @@
+package nextstep.security.filter;
+
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.exception.FilterChainNotFoundException;
+import org.springframework.web.filter.GenericFilterBean;
+
+import java.io.IOException;
+import java.util.List;
+
+public class FilterChainProxy extends GenericFilterBean {
+
+    private final List<SecurityFilterChain> filterChains;
+
+    public FilterChainProxy(List<SecurityFilterChain> filterChains) {
+        this.filterChains = filterChains;
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
+        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
+        List<Filter> filters = getFilters(httpRequest);
+        VirtualFilterChain virtualFilterChain = new VirtualFilterChain(filters, chain);
+        virtualFilterChain.doFilter(servletRequest, servletResponse);
+    }
+
+    private List<Filter> getFilters(HttpServletRequest httpRequest) {
+        return filterChains.stream()
+                .filter(filterChain -> filterChain.matches(httpRequest))
+                .findFirst()
+                .map(SecurityFilterChain::filters)
+                .orElseThrow(FilterChainNotFoundException::new);
+    }
+
+    public static class VirtualFilterChain implements FilterChain {
+
+        private final List<Filter> filters;
+        private final FilterChain originalChain;
+        private int currentPosition = 0;
+
+        public VirtualFilterChain(List<Filter> filters, FilterChain originalChain) {
+            this.filters = filters;
+            this.originalChain = originalChain;
+        }
+
+        @Override
+        public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
+            if (currentPosition >= filters.size()) {
+                originalChain.doFilter(request, response);
+            } else {
+                Filter filter = filters.get(currentPosition++);
+                filter.doFilter(request, response, this);
+            }
+        }
+    }
+}

리뷰: 하나의 필터 체인만 사용하는 게 보장되는 것이 좋을 것 같아요.
이 요청이 filterChain에 맞는 요청인지 확인하는 메서드가 필요할 것 같습니다.
@@ -0,0 +1,67 @@
+package nextstep.security.filter;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.exception.AuthenticationException;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+public class ExceptionHandlerFilter extends OncePerRequestFilter {
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException {
+        try {
+            chain.doFilter(request, response);
+        } catch (Exception e) {
+            createErrorResponse(e, response);
+        }
+    }
+
+    private void createErrorResponse(Throwable e, HttpServletResponse response) throws IOException {
+        if (e instanceof Exception) {
+            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
+        }
+
+        if (e instanceof AuthenticationException) {
+            response.setStatus(HttpStatus.UNAUTHORIZED.value());
+        }
+
+        if (e instanceof IllegalArgumentException) {
+            response.setStatus(HttpStatus.BAD_REQUEST.value());
+        }
+
+        ErrorResponse errorResponse = new ErrorResponse(response.getStatus(), e.getMessage());
+        String errorResponseJson = errorResponse.toJson();
+        response.getWriter().write(errorResponseJson);
+    }
+
+    public static class ErrorResponse {
+
+        private static final ObjectMapper objectMapper = new ObjectMapper();
+
+        private final Integer code;
+        private final String message;
+
+        public ErrorResponse(Integer code, String message) {
+            this.code = code;
+            this.message = message;
+        }
+
+        public Integer getCode() {
+            return code;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+
+        public String toJson() throws JsonProcessingException {
+            return objectMapper.writeValueAsString(this);
+        }
+    }
+}

리뷰: 필터 순서를 변경했고 (SecurityContextHolderFilter -> Basic -> Login) 
SecurityContextHolderFilter 의 finally 절에서 clearContext를 수행하니까 굳이 ExceptionHandlerFilter에서 수행을 안 해도 될 것 같은데 제가 이해한 내용이 맞을까요?