ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,8 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+    AuthorizationResult authorize(Authentication authentication, T target);
+}

리뷰: 오오 좋은 글 감사합니다. 저도 AuthorizationDecision 를 보자마자 확장성이 없는 Value Objet 같다는 느낌을 받았는데,
AuthorizationResult 인터페이스가 선언하게 된 배경에 대해서 알려주셔서 감사합니다.

기왕 deprecated 된거 AuthorizationManager.check 와 AuthorizationDecision 을 제외하고,
AuthorizationResult 인터페이스와 AuthorizationManager.authorize 만을 활용하도록 리팩토링하겠습니다.
@@ -0,0 +1,58 @@
+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 AuthorizationResult authorize(Authentication authentication, HttpServletRequest target) {
+        if (noneMatch(target)) {
+            return AuthorizationDecision.of(
+                    check(authentication, target, defaultEntry)
+            );
+        }
+        return AuthorizationDecision.of(
+                allMatch(authentication, target)
+        );
+    }
+
+    private boolean noneMatch(HttpServletRequest request) {
+        for (var entry : entries) {
+            if (entry.requestMatcher().matches(request)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean allMatch(Authentication authentication, HttpServletRequest request) {
+        for (var entry : entries) {
+            if (!check(authentication, request, entry)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean check(
+            Authentication authentication, HttpServletRequest request,
+            RequestMatcherEntry<AuthorizationManager<HttpServletRequest>> matcherEntry
+    ) {
+        return !matcherEntry.requestMatcher().matches(request)
+                || matcherEntry.entry().authorize(authentication, request).isGranted();
+    }
+}

리뷰: 앗 그렇군요! [edd41c3](https://github.com/next-step/spring-security-authorization/pull/9/commits/edd41c3f6e1f5e45357936ebfd818fdc2f3690df) 커밋에서 stream 문을 사용하지 않도록 변경했습니다.
@@ -0,0 +1,36 @@
+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) {
+        if (regex == null || regex.isBlank()) {
+            return null;
+        }
+        return 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();
+    }
+}

리뷰: [e23654e](https://github.com/next-step/spring-security-authorization/pull/9/commits/e23654eee8c18779f69bc3fd9099f82e1ac94bcd) 커밋에서 삼항연산자 대신 if 문을 사용하도록 모두 변경하였습니다!
@@ -0,0 +1,34 @@
+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 AuthorizationResult authorize(Authentication authentication, MethodInvocation target) {
+        return AuthorizationDecision.of(hasAuthority(authentication, target.getMethod()));
+    }
+
+    private boolean hasAuthority(Authentication authentication, Method method) {
+        if (!method.isAnnotationPresent(Secured.class)) {
+            return false;
+        }
+        return authentication != null
+                && authentication.isAuthenticated()
+                && authentication.getAuthorities()
+                .contains(method.getAnnotation(Secured.class).value());
+    }
+
+    private static final class SingletonHolder {
+        private static final SecuredAuthorizationManager INSTANCE = new SecuredAuthorizationManager();
+    }
+}

리뷰: 실제 상황을 상상해보니 AuthorizationDecision 이 true 인 경우, 인가되었을 경우 실행되는 horizontal 한 로직이 실행될 위험이 있을 것 같습니다.

저는 AuthorizationDecision is false == Access Denied 이렇게 생각을 해서,
Secured 어노테이션이 없다면 해당 메소드의 access 가 항상 가능해야하기 때문에 true 값을 가진 AuthorizationDecision 을 반환해야겠다고 생각했던 것 같습니다.

하지만 "Authorization" 과 "Access" 의 개념은 분명 다르고,
이건 AuthorizationDecision 이기 때문에 진영님의 말씀이 더 적절한 것 같습니다.

특히 Security Filter 의 경우 인가되었을 경우 추가적인 필터 로직이 실행될지도 모르는데,
AuthorizationDecision 가 false 를 반환하고, 인가되지 않았더라도 Access 가 가능하도록 구조를 짜는게 훨씬 안전할 것 같습니다.

안전성을 고려할때 진영님의 의견에 강하게 동의합니다.
@@ -0,0 +1,34 @@
+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 AuthorizationResult authorize(Authentication authentication, MethodInvocation target) {
+        return AuthorizationDecision.of(hasAuthority(authentication, target.getMethod()));
+    }
+
+    private boolean hasAuthority(Authentication authentication, Method method) {
+        if (!method.isAnnotationPresent(Secured.class)) {
+            return false;
+        }
+        return authentication != null
+                && authentication.isAuthenticated()
+                && authentication.getAuthorities()
+                .contains(method.getAnnotation(Secured.class).value());
+    }
+
+    private static final class SingletonHolder {
+        private static final SecuredAuthorizationManager INSTANCE = new SecuredAuthorizationManager();
+    }
+}

리뷰: [e4211fa](https://github.com/next-step/spring-security-authorization/pull/9/commits/e4211faef878dd75678d496460e22bf306eb6d9f) 커밋에서 Secured 어노테이션이 없을 경우, 인가 값이 false 가 되도록 수정하였습니다.
@@ -0,0 +1,8 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+    AuthorizationResult authorize(Authentication authentication, T target);
+}

리뷰: [ad3fc21](https://github.com/next-step/spring-security-authorization/pull/9/commits/ad3fc21a7af01dada44670d862d6ae3eabe89bfd) 커밋에서, `AuthorizationDecision check()` 대신 `AuthorizationResult authorize()` 를 사용하도록 모두 변경했습니다.
@@ -0,0 +1,8 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+    AuthorizationResult authorize(Authentication authentication, T target);
+}

리뷰: verify() 주석을 보면 "throws AccessDeniedException if access is not granted" 라고 적혀있습니다.

AuthorizationResult 를 반환하는 authorize() 메서드와 다르게 verify() 는 예외를 던짐으로써,
verify() 실패시 모든 로직의 수행을 강제로 중단하게 하는 효과를 가지고 있다고 생각합니다.

authorize() 의 경우, 인가를 받지 못할수도 있는 상황을 가정하고, true/false 에 대한 조건문 분기를 통해,
추가적인 로직 수행이 가능하다고 생각합니다.
가령 Secured 어노테이션이 없는 메서드의 경우 authorize 의 인가값이 false 라 할지라도,
정상적인 200 OK Response 를 반환할 수 있습니다.

하지만 verify() 를 통해 인가를 실패한 경우는 Spring Security 의 예외 핸들러에 따라 항상 access denied 403 response 가 반환됩니다. (사실 Spring Security 도입하면서 잘못 설정했을 때 많이 당해봤습니다. ㅋㅋㅋㅋㅋ)

이런 강제적인 로직 수행 중단이 엄격하다고 느껴질 수도 있지만,
보안이 중요한 리소스에 대해서 access 를 확실하게 차단해준다는 점에서 무척 안전하다고 느껴지기도 하네요.

저는 verify() 를 활용한다면 로직의 수행보다도, API 를 통해 조회 혹은 조작할 수 있는 "**리소스**"가 보안상 얼마나 중요한지를 생각해볼것 같습니다.
만약 해당 리소스가 현금으로 바꿀 수 있는 가치있는 리소스라면, 저는 authorize() 보다도 verify() 를 사용하려 할 것 같습니다.
불편하더라도 동작 및 정보조회가 실패하면 고객 클레임 선에서 끝나지만,
덜 불편하겠다고 현금성 있는 리소스가 털리게 되면 고객은 법적으로 고소를 할테니까요. ㅋㅋ
@@ -0,0 +1,8 @@
+package nextstep.security.authorization.manager;
+
+import nextstep.security.authentication.Authentication;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+    AuthorizationResult authorize(Authentication authentication, T target);
+}

리뷰: 저도 안정적인 코드를 만들어준다는 점에서 외부에 예외에 대한 처리를 넘겨주는 것보다는 발생할 여지가 있는 곳에서 처리하는 것이 맞다고는 생각하는데요.

제 개인적인 생각을 말씀드리면 `verify`가 잘못 설계되었다고 생각해요.

우선 interface를 사용하는 입장에서 핵심인 `throws`를 명시하지 않았기 때문에 정확하게 의도 전달을 할 수 없는 메소드가 되어있어 아직도 불완전하다는 것이 첫번째 이유이구요.

두번째는 예외를 처리하고 말기에는 다양한 로직이 있을 수 있다입니다. 저희는 간단한 부분만을 구현하기 때문에 `AuthorizationManager`를 호출하고 나서의 구조가 간단하지만, 실제 spring security 내에서는 반환된 `AuthorizationResult`를 가지고 event 발행을 하여 뒷단에서 실패에 대한 처리를 진행하기도 해요. (복잡한 뒷단 처리도 있지만 간단하게는 인가 실패에 대한 로그를 남기기도 합니다.)
물론 try-catch로 예외를 잡아서 처리할 수도 있는 부분이기는 하지만 예외는 발생 후의 비즈니스 로직을 기대하는 것이 아니라 실패를 기대하기때문에 이런 로직은 예외를 터트리기보다는 인가 실패에 대한 정보를 남기는 것이 더 적절하겠죠.

예시를 로그로 들기는 했지만 예외는 예상치 못한 상태를 처리할때 사용하는 것인데 인가 실패는 예외적인 상황이 아닌 정상적인 흐름이 될 수 있다가 포인트가 될 것 같아요.
@@ -0,0 +1,22 @@
+package nextstep.security.authorization.manager;
+
+public enum AuthorizationDecision implements AuthorizationResult {
+    GRANTED(true), NOT_GRANTED(false);
+
+    private final boolean granted;
+
+    AuthorizationDecision(boolean granted) {
+        this.granted = granted;
+    }
+
+    public static AuthorizationDecision of(boolean granted) {
+        if (granted) {
+            return GRANTED;
+        }
+        return NOT_GRANTED;
+    }
+
+    public boolean isGranted() {
+        return this.granted;
+    }
+}

리뷰: 추가적으로 확장될 여지가 있기 때문에 enum이 아닌 interface(AuthorizationResult) 혹은 클래스(AuthorizationDecision)이 더 적절할 수 있다는 이야기였어요 :)

말씀해주신대로 권한을 가진 객체도 있습니다.
https://github.com/spring-projects/spring-security/blob/main/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationDecision.java
@@ -0,0 +1,34 @@
+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 AuthorizationResult authorize(Authentication authentication, MethodInvocation target) {
+        return AuthorizationDecision.of(hasAuthority(authentication, target.getMethod()));
+    }
+
+    private boolean hasAuthority(Authentication authentication, Method method) {
+        if (!method.isAnnotationPresent(Secured.class)) {
+            return false;
+        }
+        return authentication != null
+                && authentication.isAuthenticated()
+                && authentication.getAuthorities()
+                .contains(method.getAnnotation(Secured.class).value());
+    }
+
+    private static final class SingletonHolder {
+        private static final SecuredAuthorizationManager INSTANCE = new SecuredAuthorizationManager();
+    }
+}

리뷰: 한가지 더 고민해보시면 좋을만한 부분은 `SecuredAuthorizationManager`와 `AuthorityAuthorizationManager`의 관계인데요. 

따지고보면 `@Secured`에 대한 처리를 제외하고는 `AuthorityAuthorizationManager`와 로직이 동일해보이지 않으신가요? 실제고 `AuthorizationManager`들은 서로의 구현체에 대해서 신경쓰지 않고 본인만의 것만 구현하는 것이 아닌 필요한 구현내용들은 서로를 의존하며 구성되어있어요.

즉 `@Secured`에서 인가될 권한을 꺼내오는 것까지가 `SecuredAuthorizationManager`의 순수역할, 그리고 그걸 인가처리하는건 `AuthorityAuthorizationManager`가 되는거죠.

실제 구현체는 사실 다양한 권한체계를 고려하고 있기 때문에 `AuthorityAuthorizationManager`가 아닌 `AuthoritiesAuthorizationManager`를 사용하고 있기는 하지만 `AuthorityAuthorizationManager`를 `SecuredAuthorizationManager`안에 넣어보는 것도 좋은 인사이트를 얻으실 수 있을 것 같아요.

https://github.com/spring-projects/spring-security/blob/main/core/src/main/java/org/springframework/security/authorization/method/SecuredAuthorizationManager.java
@@ -0,0 +1,22 @@
+package nextstep.security.authorization.manager;
+
+public enum AuthorizationDecision implements AuthorizationResult {
+    GRANTED(true), NOT_GRANTED(false);
+
+    private final boolean granted;
+
+    AuthorizationDecision(boolean granted) {
+        this.granted = granted;
+    }
+
+    public static AuthorizationDecision of(boolean granted) {
+        if (granted) {
+            return GRANTED;
+        }
+        return NOT_GRANTED;
+    }
+
+    public boolean isGranted() {
+        return this.granted;
+    }
+}

리뷰: 오오! AuthorityAuthorizationDecision 를 읽으니 무엇을 말씀하신건지 단번에 이해가 되네요!
공유 감사합니다!! 😄
@@ -0,0 +1,24 @@
+package nextstep.security.authorization;
+
+import jakarta.servlet.http.HttpServletRequest;
+import nextstep.security.authentication.Authentication;
+import nextstep.security.matcher.RequestMatcherEntry;
+
+import java.util.List;
+
+public class RequestMatcherDelegatingAuthorizationManager implements AuthorizationManager<HttpServletRequest> {
+    private final List<RequestMatcherEntry<AuthorizationManager>> mappings;
+
+    public RequestMatcherDelegatingAuthorizationManager(List<RequestMatcherEntry<AuthorizationManager>> mappings) {
+        this.mappings = mappings;
+    }
+
+    @Override
+    public AuthorizationDecision check(Authentication authentication, HttpServletRequest request) {
+        return mappings.stream()
+                .filter((it) -> it.getRequestMatcher().matches(request))
+                .map((it) -> it.getEntry().check(authentication, request))
+                .findFirst()
+                .orElse(AuthorizationDecision.deny());
+    }
+}

리뷰: AuthorizationManager<T>는 제네릭을 통해 해당 AuthorizationManager에서 인가를 위해 활용할 정보를 정의해서 check 시 사용합니다.
AuthorizationManager<RequestAuthorizationContext>에서 RequestAuthorizationContext 코드를 살펴보면 request와 variable이 있습니다.
참고로 말씀드리면 variable은 hasVariable에서 사용하는 값입니다

```java
/**
	* Specify that a path variable in URL to be compared.
	*
	* <p>
	* For example, <pre>
	* requestMatchers("/user/{username}").hasVariable("username").equalTo(Authentication::getName)
	* </pre>
	* @param variable the variable in URL template to compare.
	* @return {@link AuthorizedUrlVariable} for further customization.
	* @since 6.3
	*/
public AuthorizedUrlVariable hasVariable(String variable) {
	return new AuthorizedUrlVariable(variable);
}
```
@@ -1,34 +1,30 @@
 package nextstep.security.authorization;
 
-import nextstep.security.authentication.Authentication;
-import nextstep.security.context.SecurityContextHolder;
-import org.springframework.web.filter.OncePerRequestFilter;
-
 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.context.SecurityContextHolder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
 import java.io.IOException;
-import java.util.Set;
 
 public class CheckAuthenticationFilter extends OncePerRequestFilter {
-    private static final String DEFAULT_REQUEST_URI = "/members";
+
+    private final RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager;
+
+    public CheckAuthenticationFilter(RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager) {
+        this.requestMatcherDelegatingAuthorizationManager = requestMatcherDelegatingAuthorizationManager;
+    }
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
-        if (!DEFAULT_REQUEST_URI.equals(request.getRequestURI())) {
-            filterChain.doFilter(request, response);
-            return;
-        }
-
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-        if (authentication == null || !authentication.isAuthenticated()) {
-            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-            return;
-        }
 
-        Set<String> authorities = authentication.getAuthorities();
-        if (!authorities.contains("ADMIN")) {
+        AuthorizationDecision authorizationDecision = requestMatcherDelegatingAuthorizationManager.check(authentication, request);
+
+        if (authorizationDecision.isDeny()) {
             response.setStatus(HttpServletResponse.SC_FORBIDDEN);
             return;
         }

리뷰: AuthorizationFilter 필터의 역할이군요!
OncePerRequestFilter를 상속받은 이유는 무엇인가요?
@@ -1,34 +1,30 @@
 package nextstep.security.authorization;
 
-import nextstep.security.authentication.Authentication;
-import nextstep.security.context.SecurityContextHolder;
-import org.springframework.web.filter.OncePerRequestFilter;
-
 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.context.SecurityContextHolder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
 import java.io.IOException;
-import java.util.Set;
 
 public class CheckAuthenticationFilter extends OncePerRequestFilter {
-    private static final String DEFAULT_REQUEST_URI = "/members";
+
+    private final RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager;
+
+    public CheckAuthenticationFilter(RequestMatcherDelegatingAuthorizationManager requestMatcherDelegatingAuthorizationManager) {
+        this.requestMatcherDelegatingAuthorizationManager = requestMatcherDelegatingAuthorizationManager;
+    }
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
-        if (!DEFAULT_REQUEST_URI.equals(request.getRequestURI())) {
-            filterChain.doFilter(request, response);
-            return;
-        }
-
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-        if (authentication == null || !authentication.isAuthenticated()) {
-            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-            return;
-        }
 
-        Set<String> authorities = authentication.getAuthorities();
-        if (!authorities.contains("ADMIN")) {
+        AuthorizationDecision authorizationDecision = requestMatcherDelegatingAuthorizationManager.check(authentication, request);
+
+        if (authorizationDecision.isDeny()) {
             response.setStatus(HttpServletResponse.SC_FORBIDDEN);
             return;
         }

리뷰: RequestMatcherDelegatingAuthorizationManager로 직접 의존하는 것과 인터페이스를 의존하는 것의 차이는 무엇일까요?
@@ -0,0 +1,56 @@
+package nextstep.security.authorization;
+
+import nextstep.security.authentication.Authentication;
+import nextstep.security.fixture.TestAuthentication;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class HasAuthorityAuthorizationManagerTest {
+    private static final String ADMIN_ROLE = "ADMIN";
+
+    @DisplayName("Authenticate가 비어 있으면 인가를 거부한다")
+    @Test
+    void unAuthorized() {
+        HasAuthorityAuthorizationManager authenticatedAuthorizationManager = new HasAuthorityAuthorizationManager(ADMIN_ROLE);
+
+        AuthorizationDecision check = authenticatedAuthorizationManager.check(null, new MockHttpServletRequest());
+
+        assertThat(check.isDeny()).isTrue();
+    }
+
+    @DisplayName("인증되어 있지 않으면 인가를 거부한다")
+    @Test
+    void unAuthenticate() {
+        HasAuthorityAuthorizationManager authenticatedAuthorizationManager = new HasAuthorityAuthorizationManager(ADMIN_ROLE);
+        Authentication authentication = TestAuthentication.unAuthenticated();
+
+        AuthorizationDecision check = authenticatedAuthorizationManager.check(authentication, new MockHttpServletRequest());
+
+        assertThat(check.isDeny()).isTrue();
+    }
+
+    @DisplayName("매칭된 권한이 없으면 인가를 거부한다")
+    @Test
+    void authenticate() {
+        HasAuthorityAuthorizationManager authenticatedAuthorizationManager = new HasAuthorityAuthorizationManager(ADMIN_ROLE);
+        Authentication authentication = TestAuthentication.user();
+
+        AuthorizationDecision check = authenticatedAuthorizationManager.check(authentication, new MockHttpServletRequest());
+
+        assertThat(check.isDeny()).isTrue();
+    }
+
+    @DisplayName("매칭된 권한이 있으면 인증을 허용한다")
+    @Test
+    void admin_authenticate() {
+        HasAuthorityAuthorizationManager authenticatedAuthorizationManager = new HasAuthorityAuthorizationManager(ADMIN_ROLE);
+        Authentication authentication = TestAuthentication.admin();
+
+        AuthorizationDecision check = authenticatedAuthorizationManager.check(authentication, new MockHttpServletRequest());
+
+        assertThat(check.isDeny()).isFalse();
+    }
+}

리뷰: 적절한 테스트 케이스 지정 및 테스트 작성
@@ -26,13 +26,14 @@ public Object invoke(MethodInvocation invocation) throws Throwable {
         Method method = invocation.getMethod();
         if (method.isAnnotationPresent(Secured.class)) {
             Secured secured = method.getAnnotation(Secured.class);
+            HasAuthorityAuthorizationManager hasAuthorityAuthorizationManager = new HasAuthorityAuthorizationManager(secured.value());
             Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+
             if (authentication == null) {
                 throw new AuthenticationException();
             }
-            if (!authentication.getAuthorities().contains(secured.value())) {
-                throw new ForbiddenException();
-            }
+
+            hasAuthorityAuthorizationManager.verify(authentication, method);
         }
         return invocation.proceed();
     }

리뷰: 역할별 권한 검증 책임 위임 👍
@@ -0,0 +1,15 @@
+package nextstep.security.authorization;
+
+import nextstep.security.authentication.Authentication;
+
+@FunctionalInterface
+public interface AuthorizationManager<T> {
+
+    AuthorizationDecision check(Authentication authentication, T object);
+
+    default void verify(Authentication authentication, T object) throws ForbiddenException{
+        if (check(authentication, object).isDeny()) {
+            throw new ForbiddenException();
+        }
+    }
+}

리뷰: ForbiddenException 발생 시 HTTP 응답 코드는 어떻게 응답되도록 되어있나요?