ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,60 @@
+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.AuthenticationManager;
+import nextstep.security.authentication.UsernamePasswordAuthenticationToken;
+import nextstep.security.context.SecurityContextHolder;
+import nextstep.security.util.Base64Convertor;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+public class BasicAuthenticationFilter extends OncePerRequestFilter {
+    private static final String BASIC_PREFIX = "basic ";
+
+    private final AuthenticationManager authenticationManager;
+
+    public BasicAuthenticationFilter(AuthenticationManager authenticationManager) {
+        this.authenticationManager = authenticationManager;
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+        final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
+        if (isNotBasicAuth(authorization)) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+        try {
+            String[] usernameAndPassword = extractUsernameAndPassword(authorization);
+            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(usernameAndPassword[0], usernameAndPassword[1]);
+            Authentication authenticate = authenticationManager.authenticate(authenticationToken);
+
+            SecurityContextHolder.getContext().setAuthentication(authenticate);
+
+            filterChain.doFilter(request, response);
+        } catch (Exception e) {
+            SecurityContextHolder.clearContext();
+            response.setStatus(HttpStatus.UNAUTHORIZED.value());
+        }
+    }
+
+    private String[] extractUsernameAndPassword(String authorization) {
+        String base64Credentials = authorization.substring(BASIC_PREFIX.length());
+        String decodedString = Base64Convertor.decode(base64Credentials);
+        return decodedString.split(":");
+    }
+
+    private boolean isNotBasicAuth(String authorization) {
+        if (authorization == null) {
+            return true;
+        }
+        return !authorization.toLowerCase().startsWith(BASIC_PREFIX);
+    }
+}

리뷰: if 문에 메서드를 true 하게 바꾸려다가 리펙토링 부분만하고 커밋만 찍어버렷네요 ㅎㅎ...

[0455587](https://github.com/next-step/spring-security-authentication/pull/32/commits/0455587ca34b85135e20207244a250667422db0b) 반영하였습니다
@@ -0,0 +1,39 @@
+package nextstep.security.context;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+
+public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
+    private static final String DEFAULT_SESSION_KEY = "SPRING_SECURITY_CONTEXT";
+
+    private final String sessionKey;
+
+    public HttpSessionSecurityContextRepository() {
+        this(DEFAULT_SESSION_KEY);
+    }
+
+    public HttpSessionSecurityContextRepository(String sessionKey) {
+        if (sessionKey == null) {
+            sessionKey = DEFAULT_SESSION_KEY;
+        }
+        this.sessionKey = sessionKey;
+    }
+
+    @Override
+    public SecurityContext loadContext(HttpServletRequest request) {
+        HttpSession session = request.getSession(false);
+        if (session == null) {
+            return SecurityContextHolder.createEmptyContext();
+        }
+        return (SecurityContext) session.getAttribute(sessionKey);
+    }
+
+    @Override
+    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
+        HttpSession session = request.getSession();
+        session.setAttribute(sessionKey, context);
+
+        SecurityContextHolder.setContext(context);
+    }
+}

리뷰: saveContext()에서는 세션 저장 역할만 수행하는 것이 더 적절하지 않을까 생각되는데
어떻게 생각하시나요? 😊
@@ -0,0 +1,46 @@
+package nextstep.security.authentication;
+
+import nextstep.security.UserDetails;
+import nextstep.security.UserDetailsService;
+
+import java.util.Objects;
+
+public class DaoAuthenticationProvider implements AuthenticationProvider {
+    private final UserDetailsService userDetailsService;
+
+    public DaoAuthenticationProvider(UserDetailsService userDetailsService) {
+        this.userDetailsService = userDetailsService;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+        check(authentication);
+
+        final String username = authentication.getPrincipal().toString();
+        final String password = authentication.getCredentials().toString();
+        final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
+
+        if (Objects.equals(password, userDetails.getPassword())) {
+            return UsernamePasswordAuthenticationToken.ofAuthenticated(username, password);
+        }
+
+        throw new AuthenticationException();
+    }
+
+    private void check(Authentication authentication) {
+        if (authentication == null) {
+            throw new AuthenticationException();
+        }
+        if (authentication.getPrincipal() == null) {
+            throw new AuthenticationException();
+        }
+        if (authentication.getCredentials() == null) {
+            throw new AuthenticationException();
+        }
+    }
+
+    @Override
+    public boolean supports(Class<?> authentication) {
+        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
+    }
+}

리뷰: 맞아요! 👍 
 isAssignableFrom()을 사용하면 UsernamePasswordAuthenticationToken을 확장한 커스텀 인증 토큰도 지원할 수 있기 때문에, 확장성을 고려할 때 더 유연한 방식이 될 수 있습니다. 😄
@@ -0,0 +1,39 @@
+package nextstep.security.context;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+
+public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
+    private static final String DEFAULT_SESSION_KEY = "SPRING_SECURITY_CONTEXT";
+
+    private final String sessionKey;
+
+    public HttpSessionSecurityContextRepository() {
+        this(DEFAULT_SESSION_KEY);
+    }
+
+    public HttpSessionSecurityContextRepository(String sessionKey) {
+        if (sessionKey == null) {
+            sessionKey = DEFAULT_SESSION_KEY;
+        }
+        this.sessionKey = sessionKey;
+    }
+
+    @Override
+    public SecurityContext loadContext(HttpServletRequest request) {
+        HttpSession session = request.getSession(false);
+        if (session == null) {
+            return SecurityContextHolder.createEmptyContext();
+        }
+        return (SecurityContext) session.getAttribute(sessionKey);
+    }
+
+    @Override
+    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
+        HttpSession session = request.getSession();
+        session.setAttribute(sessionKey, context);
+
+        SecurityContextHolder.setContext(context);
+    }
+}

리뷰: 단순하게 생각했을때 레포지토리에서 저장하는게 깔끔하지 않을까 고민이 되었었는데

코멘트 남겨주신 내용을 보니

새로운 레포지토리 구현체에서는 SecurityContextHolder를 계속 챙겨줘야되는 부담이 생길꺼 같아요!  필터쪽에 이관을 해야할꺼 같아요
@@ -0,0 +1,69 @@
+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.AuthenticationManager;
+import nextstep.security.core.context.SecurityContext;
+import nextstep.security.core.context.SecurityContextHolder;
+import nextstep.security.exception.AuthenticationException;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+public abstract class AbstractAuthProcessingFilter extends OncePerRequestFilter {
+    private final AuthenticationManager authenticationManager;
+
+    protected AbstractAuthProcessingFilter(final AuthenticationManager authenticationManager) {
+        this.authenticationManager = authenticationManager;
+    }
+
+    @Override
+    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
+        if (!match(request)) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        if (isAlreadyAuthenticated()) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        Authentication authRequest = makeAuthentication(request);
+
+        Authentication authentication = authenticate(authRequest);
+
+        saveAuthentication(request, response, authentication);
+
+        successAuthentication(request, response, filterChain);
+    }
+
+    private static boolean isAlreadyAuthenticated() {
+        return SecurityContextHolder.getContext().getAuthentication() != null;
+    }
+
+    private Authentication authenticate(final Authentication authRequest) {
+        Authentication authentication = this.authenticationManager.authenticate(authRequest);
+        if (!authentication.isAuthenticated()) {
+            throw new AuthenticationException();
+        }
+
+        return authentication;
+    }
+
+    abstract boolean match(final HttpServletRequest request);
+
+    private void saveAuthentication(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) {
+        SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
+        securityContext.setAuthentication(authentication);
+        SecurityContextHolder.setContext(securityContext);
+    }
+
+    abstract Authentication makeAuthentication(final HttpServletRequest request);
+
+    protected abstract void successAuthentication(final HttpServletRequest request, final HttpServletResponse response,
+                                                  final FilterChain filterChain) throws ServletException, IOException;
+}

리뷰: 필터에서는 마지막에 doFilter()를 호출해야 정상적인 요청 흐름을 유지할 수 있습니다~
@@ -0,0 +1,69 @@
+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.AuthenticationManager;
+import nextstep.security.core.context.SecurityContext;
+import nextstep.security.core.context.SecurityContextHolder;
+import nextstep.security.exception.AuthenticationException;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+public abstract class AbstractAuthProcessingFilter extends OncePerRequestFilter {
+    private final AuthenticationManager authenticationManager;
+
+    protected AbstractAuthProcessingFilter(final AuthenticationManager authenticationManager) {
+        this.authenticationManager = authenticationManager;
+    }
+
+    @Override
+    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
+        if (!match(request)) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        if (isAlreadyAuthenticated()) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        Authentication authRequest = makeAuthentication(request);
+
+        Authentication authentication = authenticate(authRequest);
+
+        saveAuthentication(request, response, authentication);
+
+        successAuthentication(request, response, filterChain);
+    }
+
+    private static boolean isAlreadyAuthenticated() {
+        return SecurityContextHolder.getContext().getAuthentication() != null;
+    }
+
+    private Authentication authenticate(final Authentication authRequest) {
+        Authentication authentication = this.authenticationManager.authenticate(authRequest);
+        if (!authentication.isAuthenticated()) {
+            throw new AuthenticationException();
+        }
+
+        return authentication;
+    }
+
+    abstract boolean match(final HttpServletRequest request);
+
+    private void saveAuthentication(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) {
+        SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
+        securityContext.setAuthentication(authentication);
+        SecurityContextHolder.setContext(securityContext);
+    }
+
+    abstract Authentication makeAuthentication(final HttpServletRequest request);
+
+    protected abstract void successAuthentication(final HttpServletRequest request, final HttpServletResponse response,
+                                                  final FilterChain filterChain) throws ServletException, IOException;
+}

리뷰: 기존의 힌트 코드를 꼭 그대로 사용하지 않으셔도 되고, 문제가 있다면 수정하셔도 됩니다 ㅎㅎ 

Filter의 구조는 디자인패턴 중 하나인 연쇄 책임 패턴의 형태를 가지고 있는데요.
Filter는 다른 필터에 대한 정보를 모르는 상태에서 자신의 역할만 수행하고, 문제가 없다면 반드시 다음 필터로 위임하는 것이 정해진 규칙을 가지고 있습니다.
만약 구현 방식에 따라 doFilter() 호출 여부가 달라진다면, 설계를 다시 고민해볼 필요가 있을 것 같습니다!
@@ -0,0 +1,29 @@
+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.core.context.SecurityContextHolder;
+import nextstep.security.exception.AuthenticationException;
+import nextstep.security.exception.ForbiddenException;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+public class GlobalExceptionFilter extends OncePerRequestFilter {
+    @Override
+    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
+        try {
+            filterChain.doFilter(request, response);
+        } catch (AuthenticationException e) {
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        } catch (ForbiddenException e) {
+            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+        } catch (Exception e) {
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+        } finally {
+            SecurityContextHolder.clearContext();
+        }
+    }
+}

리뷰: 예외를 공통으로 처리할 수 있어서 좋네요 👍
@@ -0,0 +1,29 @@
+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.core.context.SecurityContextHolder;
+import nextstep.security.exception.AuthenticationException;
+import nextstep.security.exception.ForbiddenException;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+public class GlobalExceptionFilter extends OncePerRequestFilter {
+    @Override
+    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
+        try {
+            filterChain.doFilter(request, response);
+        } catch (AuthenticationException e) {
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+        } catch (ForbiddenException e) {
+            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+        } catch (Exception e) {
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+        } finally {
+            SecurityContextHolder.clearContext();
+        }
+    }
+}

리뷰: 문제는 없지만, SecurityContextHolder의 초기화 역할은 SecurityContextHolder를 담당하는 Filter에서 하는 규칙을 가져가면 다른 Filter에서는 SecurityContextHolder에 대한 신경을 안써도 될 것 같아요~
@@ -1,25 +1,13 @@
-import logo from './logo.svg';
-import './App.css';
+import ReviewForm from "./Components/DetailPage/ReviewForm";
 
 function App() {
   return (
     <div className="App">
       <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.js</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
       </header>
+      <ReviewForm />
     </div>
   );
 }
 
-export default App;
+export default App;
\ No newline at end of file

리뷰: 경로 ./Components/DetailPage/ReviewForm 요거 아닌가요  ?!
@@ -0,0 +1,57 @@
+import { ReviewSectionName, Button, ReviewFormArea, Conditions, IfReward, Checkbox, Number, DropDown, ReviewContent } from "./ReviewForm.style";
+import { useState } from "react";
+
+const ReviewForm = () => {
+    const [received, setReceived] = useState(false);
+    const [year, setYear] = useState("");
+    const [semester, setSemester] = useState("");
+    const [review, setReview] = useState("");
+
+    const Submission = () => {
+        console.log({ received, year, semester, review });
+    };
+
+    return (
+        <div>
+            <ReviewSectionName>리뷰쓰기</ReviewSectionName>
+            <ReviewFormArea>
+                <Conditions>
+                    <IfReward>
+                        <Checkbox
+                            type="checkbox"
+                            checked={received}
+                            onChange={() => setReceived(!received)}
+                        />
+                        수혜여부
+                    </IfReward>
+
+                    <Number
+                        type="number"
+                        value={year}
+                        onChange={(e) => setYear(e.target.value)}
+                        placeholder="신청 연도"
+                    />
+
+                    <DropDown value={semester} onChange={(e) => setSemester(e.target.value)}>
+                        <option value="">신청 학기</option>
+                        <option value="1학기">1학기</option>
+                        <option value="2학기">2학기</option>
+                    </DropDown>
+                </Conditions>
+
+                <ReviewContent>
+                    <textarea
+                        value={review}
+                        onChange={(e) => setReview(e.target.value)}
+                        placeHolder="장학금 신청 과정이 어떠했나요? &#13;&#10;입금 금액 및 시기, 선발 기준, 팁 등 장학금에 관한 정보와 경험을 자유롭게 남겨주세요."
+                    />
+                </ReviewContent>
+
+                <Button onClick={Submission}>리뷰 작성하기</Button>
+
+            </ReviewFormArea>
+        </div>
+    );
+};
+
+export default ReviewForm;
\ No newline at end of file

리뷰: 함수 이름 파스칼케이스로 통일하셨나요? 🙂 (just 궁금)
@@ -0,0 +1,57 @@
+import { ReviewSectionName, Button, ReviewFormArea, Conditions, IfReward, Checkbox, Number, DropDown, ReviewContent } from "./ReviewForm.style";
+import { useState } from "react";
+
+const ReviewForm = () => {
+    const [received, setReceived] = useState(false);
+    const [year, setYear] = useState("");
+    const [semester, setSemester] = useState("");
+    const [review, setReview] = useState("");
+
+    const Submission = () => {
+        console.log({ received, year, semester, review });
+    };
+
+    return (
+        <div>
+            <ReviewSectionName>리뷰쓰기</ReviewSectionName>
+            <ReviewFormArea>
+                <Conditions>
+                    <IfReward>
+                        <Checkbox
+                            type="checkbox"
+                            checked={received}
+                            onChange={() => setReceived(!received)}
+                        />
+                        수혜여부
+                    </IfReward>
+
+                    <Number
+                        type="number"
+                        value={year}
+                        onChange={(e) => setYear(e.target.value)}
+                        placeholder="신청 연도"
+                    />
+
+                    <DropDown value={semester} onChange={(e) => setSemester(e.target.value)}>
+                        <option value="">신청 학기</option>
+                        <option value="1학기">1학기</option>
+                        <option value="2학기">2학기</option>
+                    </DropDown>
+                </Conditions>
+
+                <ReviewContent>
+                    <textarea
+                        value={review}
+                        onChange={(e) => setReview(e.target.value)}
+                        placeHolder="장학금 신청 과정이 어떠했나요? &#13;&#10;입금 금액 및 시기, 선발 기준, 팁 등 장학금에 관한 정보와 경험을 자유롭게 남겨주세요."
+                    />
+                </ReviewContent>
+
+                <Button onClick={Submission}>리뷰 작성하기</Button>
+
+            </ReviewFormArea>
+        </div>
+    );
+};
+
+export default ReviewForm;
\ No newline at end of file

리뷰: 함수명은 보편적으로 사용하는 카멜케이스로 사용해보는 것은 어떨까요~?<br/>
추가로 `handleSubmit`과 같이 handle prefix로 수정해봐도 좋을 것 같습니다~

cf. [리액트 공식문서](https://ko.react.dev/learn/adding-interactivity)
<img width="396" alt="image" src="https://github.com/user-attachments/assets/efda3a60-12ff-4957-9e17-32377de6ce05" />
@@ -1,25 +1,13 @@
-import logo from './logo.svg';
-import './App.css';
+import ReviewForm from "./Components/DetailPage/ReviewForm";
 
 function App() {
   return (
     <div className="App">
       <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.js</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
       </header>
+      <ReviewForm />
     </div>
   );
 }
 
-export default App;
+export default App;
\ No newline at end of file

리뷰: 맞아요! 저런 오류가 있었네요.. 나중에 DetailPage 폴더를 추가하고 저 부분을 수정 안 한 것 같습니다 덕분에 알게 됐어요 감사합니다!!