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 가 먼저 실행이 되어야 요청이 시작하고 끝날 때 SecurityContext를 적절하게 처리할 수 있겠네요. 제가 이해한 부분이 맞을까요?

정확히 이해해주셨네요 👍
@@ -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();
+    }
+}

리뷰: HttpSessionSecurityContextRepository를 매번 생성하지 않고, Spring Bean으로 등록하여 재사용해보면 어떨까요?
@@ -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에서 수행을 안 해도 될 것 같은데 제가 이해한 내용이 맞을까요?

맞습니다 👍 
추가로 AuthenticationException 이외에 다른 예외는 어떤게 발생할 수 있고, 어떻게 처리하면 좋을지 고민해보는 것도 학습에 도움이 될 것 같아요!
@@ -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의 역할이 HttpSessionSecurityContextRepository로 넘어갔으니, 해당 상수도 HttpSessionSecurityContextRepository로 넘어가면 역할이 명확해 질 것 같아요!
@@ -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");
+        }
+    }
+}

리뷰: > 이유를 잘 모르겠네요.. ㅠㅠ
근데 수정하다가 든 생각인데, 각 필터에서 securityContextRepository.saveContext 를 수행하고 있으니까 SecurityContextHolderFilter에서는 saveContext 수행을 안 해도 되지 않나요?

반대로 접근할 수도 있을 것 같아요! 지금의 구조에선 각 필터에서 `saveContext`를 호출해줘야 하는데, SecurityContextHolderFilter에서 담당하게 된다면 각 필터에선 SecurityContext에 대한 신경을 안쓰도록 만들 수 있습니다~
@@ -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);
+        }
+    }
+}

리뷰: 로그인 시에 입력값이 누락됐을 경우의 예외처리를 추가했고,
ExceptionHandlerFilter 에서의 에러 응답값 생성 로직을 추가했습니다. 그리고 예상 못한 예외는 500 에러 처리하도록 구현했습니다..!
@@ -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");
+        }
+    }
+}

리뷰: SecurityContextHolderFilter의 역할에 대해 다시 생각해보게 되었습니다. 감사합니다!
@@ -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();
+        }
+    }
+}

리뷰: SecurityContextHolderFilter만 SecurityContextRepository에 대한 의존성을 가지도록 수정했습니다!
@@ -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);
+        }
+    }
+}

리뷰: ```suggestion
        try {
            chain.doFilter(request, response);
        } catch (AuthenticationException e) {
        } catch (IllegalArgumentException e) {
        } catch (Exception e) {
        }
```

instanceof 대신 위와 같이 작성하는게 더 명확할 것 같네요!
@@ -0,0 +1,40 @@
+package nextstep.security.context;
+
+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 java.io.IOException;
+
+public class SecurityContextHolderFilter implements Filter {
+
+    private final SecurityContextRepository securityContextRepository;
+
+    public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) {
+        this.securityContextRepository = securityContextRepository;
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
+        throws IOException, ServletException {
+
+        try {
+            SecurityContext securityContext = securityContextRepository.loadContext((HttpServletRequest) servletRequest);
+
+            if (securityContext != null) {
+                SecurityContextHolder.setContext(securityContext);
+            }
+
+            filterChain.doFilter(servletRequest, servletResponse);
+
+            SecurityContext afterSecurityContext = SecurityContextHolder.getContext();
+            securityContextRepository.saveContext(afterSecurityContext, (HttpServletRequest) servletRequest,
+                (HttpServletResponse) servletResponse);
+        } finally {
+            SecurityContextHolder.clearContext();
+        }
+    }
+}

리뷰: `SecurityContextHolderFilter`는 가장 먼저 SecurityContext를 관리하며, 마지막으로 SecurityContext를 관리해주는 역할을 해요. 위에서 예외가 발생하면 `SecurityContext.clearContext`가 실행이 어려워보여요.