ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,45 @@
+package nextstep.security.config;
+
+import nextstep.security.ProviderNotFoundException;
+
+import java.util.Collections;
+import java.util.List;
+
+public class ProviderManager implements AuthenticationManager {
+
+    private List<AuthenticationProvider> providers = Collections.emptyList();
+
+    public ProviderManager(List<AuthenticationProvider> providers) {
+        this.providers = providers;
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) {
+        Class<? extends Authentication> authenticationClassToTest = authentication.getClass();
+        Authentication result = null;
+        for (AuthenticationProvider provider : this.providers) {
+            if (!provider.supports(authenticationClassToTest)) {
+                continue;
+            }
+            result = provider.authenticate(authentication);
+            if (result != null) { // 인증된 객제가 있다면
+                copyDetails(authentication, result);
+                result.setAuthenticated(true); // todo 책임 확인 한번 필요
+                break;
+            }
+        }
+        if (result == null) {
+            throw new ProviderNotFoundException("지원하는 provider가 없습니다!");
+        }
+        return result;
+    }
+
+    // 인증된 토큰에 detail 값이 없다면 detail 넣어주기
+    private void copyDetails(Authentication source, Authentication dest) {
+        if ((dest instanceof AbstractAuthenticationToken token) && (dest.getDetails() == null)) {
+            token.setDetails(source.getDetails());
+        }
+    }
+
+}
+

리뷰: 여기서 `ProviderManager` 는 지원하는 `AuthenticationProvider`를 순회하는 역할을 한다고 생각했습니다.
인증 로직 자체는 각 `AuthenticationProvider`에서 진행을 한다고 생각했어요. (마치 FilerChainProxy와 SecurityFilterChain 과 비슷한 관계같다고도 생각했습니다.)

`AuthenticationProvider` 는 인증된다면 `Authentication` 이라는 토큰객체에 사용자 정보를 담아 넘겨주는 역할을 하고요.
그런데 여기서는 result라는 `Authentication` 변수를 `ProviderManager`에서 선언하고, null인지 아닌지에 따라 로직을 다르게 태우기 때문에  `ProviderManager`에 있는게 더 적절해 보인다고 판단했습니다.

혹시 재연님은 다르게 생각하신다면 이유가 궁금합니다!
@@ -0,0 +1,49 @@
+package nextstep.security.interceptor;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import nextstep.security.AuthenticationException;
+import nextstep.security.UserDetailService;
+import nextstep.security.UserDetails;
+import nextstep.security.util.Base64Convertor;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+public class BasicAuthInterceptor implements HandlerInterceptor {
+
+    private final UserDetailService userDetailService;
+
+    public BasicAuthInterceptor(UserDetailService userDetailService) {
+        this.userDetailService = userDetailService;
+    }
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        try {
+            String authorizationHeader = request.getHeader("Authorization");
+            String authType = authorizationHeader.split(" ")[0];
+            String credentials = authorizationHeader.split(" ")[1];
+            String decodedString = Base64Convertor.decode(credentials);
+            checkAuthType(authType);
+
+            String[] usernameAndPassword = decodedString.split(":");
+            String username = usernameAndPassword[0];
+            String password = usernameAndPassword[1];
+
+            UserDetails userDetail = userDetailService.getUserByUsername(username);
+            if (!userDetail.getPassword().equals(password)) {
+                throw new AuthenticationException();
+            }
+            return true;
+        } catch (Exception e) {
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+            return false;
+        }
+    }
+
+    private void checkAuthType(String authType) {
+        if (!authType.equalsIgnoreCase(HttpServletRequest.BASIC_AUTH)) {
+            throw new AuthenticationException();
+        }
+    }
+
+}

리뷰: 이건 미션진행하다 남긴 유물들인데 지우지않고 학습용으로 남겨놓았습니다!
처음에 interceptor의 로직 > filter로 변환  과정에서 생겼던 것들입니다:)
@@ -9,6 +9,7 @@
     "react": "^19.0.0",
     "react-dom": "^19.0.0",
     "react-scripts": "5.0.1",
+    "styled-components": "^6.1.15",
     "web-vitals": "^2.1.0"
   },
   "scripts": {

리뷰: 이미 main에 추가된 코드는 rebase를 통해 해결하고 반영해봅시다~
@@ -0,0 +1,47 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authentication.AuthenticationException;
+import nextstep.security.authorization.AuthorizationDecision;
+import nextstep.security.authorization.ForbiddenException;
+import nextstep.security.authorization.Secured;
+import org.aopalliance.intercept.MethodInvocation;
+import org.springframework.core.annotation.AnnotationUtils;
+
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Set;
+
+public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+
+    private final AuthorizationManager<Collection<String>> authorizationManager;
+
+    public SecuredAuthorizationManager() {
+        this.authorizationManager = new AuthoritiesAuthorizationManager();
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, MethodInvocation invocation) {
+        Method method = invocation.getMethod();
+
+        if (authentication == null) {
+            throw new ForbiddenException();
+        }
+
+        if (method.isAnnotationPresent(Secured.class)) {
+            Secured secured = getSecured(method);
+            return this.authorizationManager.check(authentication, Set.of(secured.value()));
+        }
+
+        return AuthorizationDecision.ACCESS_DENIED;
+    }
+
+    private Secured getSecured(Method method) {
+        Secured secured = AnnotationUtils.findAnnotation(method, Secured.class);
+        if (secured == null) {
+            throw new IllegalStateException();
+        }
+
+        return secured;
+    }
+}

리뷰: ```suggestion
        if (authentication == null) {
            return AuthorizationDecision.ACCESS_DENIED;
        }
```

권한에 따른 예외 처리는 Interceptor의 역할로 넘겨주면 어떨까요?
@@ -0,0 +1,28 @@
+package nextstep.security.authorization.manager;
+
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.AuthorizationDecision;
+import nextstep.security.authorization.access.RequestMatcherEntry;
+
+import java.util.List;
+
+public class RequestMatcherDelegatingAuthorizationManager implements AuthorizationManager<HttpServletRequest> {
+
+    private final List<RequestMatcherEntry<AuthorizationManager<HttpServletRequest>>> mappings;
+
+    public RequestMatcherDelegatingAuthorizationManager(List<RequestMatcherEntry<AuthorizationManager<HttpServletRequest>>> mappings) {
+        this.mappings = mappings;
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, HttpServletRequest httpServletRequest) {
+        for (RequestMatcherEntry<AuthorizationManager<HttpServletRequest>> entry : mappings) {
+            if (entry.getMatcher().matches(httpServletRequest)) {
+                return entry.getManager().check(authentication, httpServletRequest);
+            }
+        }
+
+        return AuthorizationDecision.ACCESS_DENIED;
+    }
+}

리뷰: AuthorizationManager에 대한 추상화를 잘 해주셨네요 👍
@@ -1,13 +1,19 @@
 package nextstep.app;
 
+import jakarta.servlet.http.HttpServletRequest;
 import nextstep.app.domain.Member;
 import nextstep.app.domain.MemberRepository;
 import nextstep.security.authentication.AuthenticationException;
 import nextstep.security.authentication.BasicAuthenticationFilter;
 import nextstep.security.authentication.UsernamePasswordAuthenticationFilter;
-import nextstep.security.authorization.CheckAuthenticationFilter;
-import nextstep.security.authorization.SecuredAspect;
+import nextstep.security.authorization.AuthorizationFilter;
 import nextstep.security.authorization.SecuredMethodInterceptor;
+import nextstep.security.authorization.manager.AuthenticatedAuthorizationManager;
+import nextstep.security.authorization.manager.AuthorizationManager;
+import nextstep.security.authorization.manager.DenyAllAuthorizationManager;
+import nextstep.security.authorization.manager.HasAuthorityAuthorizationManager;
+import nextstep.security.authorization.manager.PermitAllAuthorizationManager;
+import nextstep.security.authorization.manager.RequestAuthorizationManager;
 import nextstep.security.config.DefaultSecurityFilterChain;
 import nextstep.security.config.DelegatingFilterProxy;
 import nextstep.security.config.FilterChainProxy;
@@ -22,6 +28,10 @@
 import java.util.List;
 import java.util.Set;
 
+import static nextstep.security.authorization.matcher.RequestMatcherEntry.createDefaultMatcher;
+import static nextstep.security.authorization.matcher.RequestMatcherEntry.createMvcMatcher;
+import static org.springframework.http.HttpMethod.GET;
+
 @EnableAspectJAutoProxy
 @Configuration
 public class SecurityConfig {
@@ -46,19 +56,20 @@ public FilterChainProxy filterChainProxy(List<SecurityFilterChain> securityFilte
     public SecuredMethodInterceptor securedMethodInterceptor() {
         return new SecuredMethodInterceptor();
     }
-//    @Bean
-//    public SecuredAspect securedAspect() {
-//        return new SecuredAspect();
-//    }
 
     @Bean
     public SecurityFilterChain securityFilterChain() {
+        final AuthorizationManager<HttpServletRequest> authorizationManager = new RequestAuthorizationManager(List.of(
+                createMvcMatcher(GET, "/members", new HasAuthorityAuthorizationManager<>("ADMIN")),
+                createMvcMatcher(GET, "/members/me", new AuthenticatedAuthorizationManager<>()),
+                createMvcMatcher(GET, "/search", new PermitAllAuthorizationManager<>())
+        ), createDefaultMatcher(new DenyAllAuthorizationManager<>()));
         return new DefaultSecurityFilterChain(
                 List.of(
                         new SecurityContextHolderFilter(),
                         new UsernamePasswordAuthenticationFilter(userDetailsService()),
                         new BasicAuthenticationFilter(userDetailsService()),
-                        new CheckAuthenticationFilter()
+                        new AuthorizationFilter(authorizationManager)
                 )
         );
     }

리뷰: config에 잘 추가해주셨네요 👍 

자바 컨벤션 상 클래스 네이밍은 명사를 활용하기 때문에 `Has`보다는 조금 더 적절한 네이밍이 들어가면 좋을 것 같아요.
https://www.oracle.com/java/technologies/javase/codeconventions-namingconventions.html
@@ -0,0 +1,8 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+    AuthorizationDecision check(Authentication authentication, T target);
+}

리뷰: 최근 spring security 버전에서는 `check`는 사실 deprecated되었는데요. 일반적으로는 잘 아시다시피 대부분의 오픈소스는 결국 추상화를 바라보고 진행하려하며 `AuthorizationDecision`이 spring security 내에서는 클래스 구현체로 되어있고, 버전이 올라가는 과정에서 이부분 또한 추상화되었어요.

최근 버전에서는 그래서 구현체를 반환하는 `AuthorizationDecision`을 사용하기보다는 이를 사용하는 default 메소드인 `authorize` 사용을 권장하고 있습니다.

https://github.com/franticticktick/spring-security/blob/main/core/src/main/java/org/springframework/security/authorization/AuthorizationManager.java

https://github.com/spring-projects/spring-security/pull/14712
https://github.com/spring-projects/spring-security/pull/14846
@@ -0,0 +1,8 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+    AuthorizationDecision check(Authentication authentication, T target);
+}

리뷰: 선택 요구사항에는 `verify`도 있는데요. 정완님은 `verfiy`는 어떤 상황에서 사용하면 좋을만하다고 생각하시나요?
@@ -0,0 +1,19 @@
+package nextstep.security.authorization.manager;
+
+public enum AuthorizationDecision {
+    GRANTED(true), NOT_GRANTED(false);
+
+    private final boolean granted;
+
+    AuthorizationDecision(boolean granted) {
+        this.granted = granted;
+    }
+
+    public static AuthorizationDecision of(boolean granted) {
+        return granted ? GRANTED : NOT_GRANTED;
+    }
+
+    public boolean isGranted() {
+        return this.granted;
+    }
+}

리뷰: `AuthorizationManager.check`를 이어서 이야기하면, enum을 사용하기에 적절한지 고려해보면 좋을 것 같아요.

물론 현재는 granted 조건이 true와 false밖에 없는 상황이긴 하지만 확장성을 고려해본다면 interface로 구성하는 것이 맞지 않을까요? 권한이 true인지, false인지 담아둘 수도 있지만 어떤 권한인지도 보유할 상황이 존재하지 않을까요?
@@ -0,0 +1,20 @@
+package nextstep.security.authorization.matcher;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+public class AnyRequestMatcher implements RequestMatcher {
+    private AnyRequestMatcher() {}
+
+    public static AnyRequestMatcher getInstance() {
+        return SingletonHolder.INSTANCE;
+    }
+
+    @Override
+    public boolean matches(HttpServletRequest request) {
+        return true;
+    }
+
+    private final static class SingletonHolder {
+        private static final AnyRequestMatcher INSTANCE = new AnyRequestMatcher();
+    }
+}

리뷰: 싱글톤으로 잘 구성해주셨네요 👍
@@ -0,0 +1,35 @@
+package nextstep.security.authorization.matcher;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpMethod;
+
+import java.util.regex.Pattern;
+
+public class MvcRequestMatcher implements RequestMatcher {
+    private final HttpMethod method;
+    private final Pattern pattern;
+
+    public MvcRequestMatcher(HttpMethod method, String pattern) {
+        this.method = method;
+        this.pattern = compile(pattern);
+    }
+
+    private Pattern compile(String regex) {
+        return regex == null || regex.isBlank()
+                ? null
+                : Pattern.compile(regex);
+    }
+
+    @Override
+    public boolean matches(HttpServletRequest request) {
+        return matchMethod(request) && matchPattern(request);
+    }
+
+    private boolean matchMethod(HttpServletRequest request) {
+        return method == null || method.name().equals(request.getMethod());
+    }
+
+    private boolean matchPattern(HttpServletRequest request) {
+        return pattern == null || pattern.matcher(request.getRequestURI()).matches();
+    }
+}

리뷰: ```suggestion
        if (regex == null || regex.isBlank()) {
            return null;
        }
        return Pattern.compile(regex);
```
개인적으로는 삼항연산자보다는 if문으로 early return 해주는 것이 한줄 한줄을 읽을 때 컨텍스트를 외워두지 않아도 되어서 읽기 편한 코드로 만들어준다고 생각합니다 :)
@@ -0,0 +1,32 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.Secured;
+import org.aopalliance.intercept.MethodInvocation;
+
+import java.lang.reflect.Method;
+
+public class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
+    private SecuredAuthorizationManager() {}
+
+    public static SecuredAuthorizationManager getInstance() {
+        return SingletonHolder.INSTANCE;
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, MethodInvocation target) {
+        return AuthorizationDecision.of(hasAuthority(authentication, target.getMethod()));
+    }
+
+    private boolean hasAuthority(Authentication authentication, Method method) {
+        return authentication != null
+                && authentication.isAuthenticated()
+                && method.isAnnotationPresent(Secured.class)
+                && authentication.getAuthorities()
+                .contains(method.getAnnotation(Secured.class).value());
+    }
+
+    private static final class SingletonHolder {
+        private static final SecuredAuthorizationManager INSTANCE = new SecuredAuthorizationManager();
+    }
+}

리뷰: `@Secured` 어노테이션이 없는 메소드를 타는 경우에는 인가 값이 true인 `AuthorizationDecision`을 반환하는 것이 맞을지 고민해보면 좋을 것 같아요.

`AuthorizationDecision`이 true인 객체를 반환한다는 것은 "인가"되었다는 것인데 `@Secured`어노테이션이 없는 것은 인가의 프로세스가 필요없지 않나요?
@@ -0,0 +1,48 @@
+package nextstep.security.authorization.manager;
+
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.authorization.matcher.RequestMatcherEntry;
+
+import java.util.List;
+
+public class RequestAuthorizationManager implements AuthorizationManager<HttpServletRequest> {
+    private final List<RequestMatcherEntry<AuthorizationManager<HttpServletRequest>>> entries;
+    private final RequestMatcherEntry<AuthorizationManager<HttpServletRequest>> defaultEntry;
+
+    public RequestAuthorizationManager(
+            List<RequestMatcherEntry<AuthorizationManager<HttpServletRequest>>> entries,
+            RequestMatcherEntry<AuthorizationManager<HttpServletRequest>> defaultEntry
+    ) {
+        this.entries = entries;
+        this.defaultEntry = defaultEntry;
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, HttpServletRequest target) {
+        final boolean isGranted = noneMatch(target)
+                ? check(authentication, target, defaultEntry)
+                : allMatch(authentication, target);
+        return AuthorizationDecision.of(isGranted);
+    }
+
+    private boolean noneMatch(HttpServletRequest request) {
+        return entries.stream().noneMatch(
+                entry -> entry.requestMatcher().matches(request)
+        );
+    }
+
+    private boolean allMatch(Authentication authentication, HttpServletRequest request) {
+        return entries.stream().allMatch(
+                entry -> check(authentication, request, entry)
+        );
+    }
+
+    private boolean check(
+            Authentication authentication, HttpServletRequest request,
+            RequestMatcherEntry<AuthorizationManager<HttpServletRequest>> matcherEntry
+    ) {
+        return !matcherEntry.requestMatcher().matches(request)
+                || matcherEntry.entry().check(authentication, request).isGranted();
+    }
+}

리뷰: spring security에서는 성능의 문제로 stream문을 지양합니다.

https://github.com/spring-projects/spring-security/issues/7154
@@ -0,0 +1,36 @@
+package nextstep;
+
+import nextstep.app.domain.Member;
+
+import java.util.Base64;
+import java.util.Set;
+
+public final class Fixture {
+    public static final Member TEST_ADMIN_MEMBER = new Member(
+            "a@a.com", "password",
+            "a", "",
+            Set.of("USER", "ADMIN")
+    );
+    public static final Member TEST_USER_MEMBER = new Member(
+            "b@b.com", "password",
+            "b", "",
+            Set.of("USER")
+    );
+    public static final String TEST_ADMIN_TOKEN = createToken(TEST_ADMIN_MEMBER);
+    public static final String TEST_USER_TOKEN = createToken(TEST_USER_MEMBER);
+
+    private Fixture() {}
+
+    private static String createToken(final Member member) {
+        return createToken(member.getEmail(), member.getPassword());
+    }
+
+    public static String createToken(
+            final String principal,
+            final String credential
+    ) {
+        return "Basic " + Base64.getEncoder().encodeToString(
+                (principal + ":" + credential).getBytes()
+        );
+    }
+}

리뷰: `Fixture` 👍 

(반영하지 않으셔도 됩니다) 저는 특정 도메인 객체의 Fixture 를 활용해야하는 경우가 있으면 enum으로 생성하여 활용하는 편이긴합니다. enum에서 제공하는 메소드를 활용할 필요가 있을 때가 간혹 있더라구요.
@@ -0,0 +1,79 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+import static nextstep.security.authorization.manager.AuthorizationDecision.GRANTED;
+import static nextstep.security.authorization.manager.AuthorizationDecision.NOT_GRANTED;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+class HasAuthorityAuthorizationManagerTest {
+    private final AuthorizationManager<String> manager = new HasAuthorityAuthorizationManager<>(
+            "ADMIN", "USER"
+    );
+
+    @DisplayName("유저가 인증되지 않았다면 인가받지 못한다.")
+    @Test
+    void notAuthenticated() {
+        assertAll(
+                () -> assertThat(manager.check(
+                        null, null
+                )).isEqualTo(NOT_GRANTED),
+                () -> assertThat(manager.check(
+                        createAuthentication(false), null
+                )).isEqualTo(NOT_GRANTED)
+        );
+    }
+
+    @DisplayName("인증된 유저가 Authority 를 가졌다면 인가받는다.")
+    @Test
+    void noAuthority() {
+        assertAll(
+                () -> assertThat(manager.check(
+                        createAuthentication(true, "ADMIN"), null)
+                ).isEqualTo(GRANTED),
+                () -> assertThat(manager.check(
+                        createAuthentication(true, "USER"), null)
+                ).isEqualTo(GRANTED)
+        );
+    }
+
+    @DisplayName("인증된 유저가 Authority 를 가지지 못했다면 인가받지 못한다.")
+    @Test
+    void hasAuthority() {
+        assertThat(manager.check(
+                createAuthentication(true, "ANONYMOUS"), null)
+        ).isEqualTo(NOT_GRANTED);
+    }
+
+    private Authentication createAuthentication(
+            boolean isAuthenticated,
+            String... authorities
+    ) {
+        return new Authentication() {
+            @Override
+            public Set<String> getAuthorities() {
+                return Set.of(authorities);
+            }
+
+            @Override
+            public Object getCredentials() {
+                return "PASSWORD";
+            }
+
+            @Override
+            public Object getPrincipal() {
+                return "USERNAME";
+            }
+
+            @Override
+            public boolean isAuthenticated() {
+                return isAuthenticated;
+            }
+        };
+    }
+}

리뷰: 인증된 유저에 대한 테스트를 진행할 때 "ADMIN"과 "USER"는 결국 다른 테스트 케이스에 해당해요.  "ADMIN"에 대한 결과나 "USER"에 대한 결과가 다를 수 있는데, 이에 대한 피드백은 하나의 테스트에서 몰아서 진행하기보다는 각 케이스에 맞게 테스트에 대한 피드백을 받아야 어떤 케이스를 보아야할지 명확하게 파악이 될 수 있습니다.

한 테스트에 한 케이스만 넣으라가 명확한 정답은 아니지만 어떤 피드백을 줄수 있냐의 관점에서는 각각의 다른 케이스도 서로 다른 테스트에 해당한다고 볼 수 있을 것 같아요 :) 

https://martinfowler.com/bliki/SpecificationByExample.html
@@ -7,6 +7,7 @@ export interface ReviewInfo {
   userprofile?: string;
   date: Date;
   isMyReview?: boolean;
+  isMyWritten?: boolean;
   eventId?: number;
   eventType?: string;
 }

리뷰: 내가 작성한 리뷰 탭에서 샤용할 때는 <Review reviewInfo={{...data, isMyWritten: true}}> 이렇게 isMyWritten를 true로 추가해주시면 됩니다.