From 1dd4a357734067c5b98cff22fc6315aeade2f447 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:25:17 +0900 Subject: [PATCH 01/20] =?UTF-8?q?feat:=20Authenticated=20=EC=9D=B8?= =?UTF-8?q?=EA=B0=80=20=ED=97=88=EC=9A=A9=20=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=EC=A7=80=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AuthenticatedAuthorizationManager.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/nextstep/security/authorization/manager/AuthenticatedAuthorizationManager.java diff --git a/src/main/java/nextstep/security/authorization/manager/AuthenticatedAuthorizationManager.java b/src/main/java/nextstep/security/authorization/manager/AuthenticatedAuthorizationManager.java new file mode 100644 index 0000000..f2c7053 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/AuthenticatedAuthorizationManager.java @@ -0,0 +1,19 @@ +package nextstep.security.authorization.manager; + +import nextstep.security.authentication.Authentication; + +import static nextstep.security.authorization.manager.AuthorizationDecision.GRANTED; +import static nextstep.security.authorization.manager.AuthorizationDecision.NOT_GRANTED; + +public class AuthenticatedAuthorizationManager implements AuthorizationManager { + @Override + public AuthorizationResult authorize(Authentication authentication, T target) { + if(authentication != null && + authentication.isAuthenticated() + ) { + return GRANTED; + } + + return NOT_GRANTED; + } +} From cbaadb0dfc77717fbe06d0de4c7c1c4bac9e0d99 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:27:40 +0900 Subject: [PATCH 02/20] =?UTF-8?q?feat:=20AuthorityAuthorizationManager=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AuthorityAuthorizationManager.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/main/java/nextstep/security/authorization/manager/AuthorityAuthorizationManager.java diff --git a/src/main/java/nextstep/security/authorization/manager/AuthorityAuthorizationManager.java b/src/main/java/nextstep/security/authorization/manager/AuthorityAuthorizationManager.java new file mode 100644 index 0000000..0e3444f --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/AuthorityAuthorizationManager.java @@ -0,0 +1,29 @@ +package nextstep.security.authorization.manager; + +import nextstep.security.authentication.Authentication; + +import java.util.Set; + +public class AuthorityAuthorizationManager implements AuthorizationManager { + private final Set authorities; + + public AuthorityAuthorizationManager(String... authorities) { + this.authorities = Set.of(authorities); + } + + @Override + public AuthorizationResult authorize(Authentication authentication, T target) { + return AuthorizationDecision.from(isGranted(authentication)); + } + + private boolean isGranted(Authentication authentication) { + return authentication != null + && authentication.isAuthenticated() + && anyMatch(authentication); + } + + private boolean anyMatch(Authentication authentication) { + return authentication.getAuthorities().stream() + .anyMatch(authorities::contains); + } +} From 0106339d2e4370c1a3ce8c8a56cadf5a5d66d238 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:27:59 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20=EC=9D=B8=EA=B0=80=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EC=A0=95=EC=9D=98=20enum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manager/AuthorizationDecision.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/nextstep/security/authorization/manager/AuthorizationDecision.java diff --git a/src/main/java/nextstep/security/authorization/manager/AuthorizationDecision.java b/src/main/java/nextstep/security/authorization/manager/AuthorizationDecision.java new file mode 100644 index 0000000..06edbf8 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/AuthorizationDecision.java @@ -0,0 +1,20 @@ +package nextstep.security.authorization.manager; + +public enum AuthorizationDecision implements AuthorizationResult { + GRANTED(true), + NOT_GRANTED(false); + + private final boolean granted; + + AuthorizationDecision(final boolean granted) { + this.granted = granted; + } + + public boolean isGranted() { + return this.granted; + } + + public static AuthorizationResult from(boolean granted) { + return granted ? GRANTED : NOT_GRANTED; + } +} From 9ff668af7ef5e0af8f59347549ee00d784d4b6e8 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:28:19 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat;=20AuthenticationManger=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../authorization/manager/AuthorizationManager.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/nextstep/security/authorization/manager/AuthorizationManager.java diff --git a/src/main/java/nextstep/security/authorization/manager/AuthorizationManager.java b/src/main/java/nextstep/security/authorization/manager/AuthorizationManager.java new file mode 100644 index 0000000..51a5922 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/AuthorizationManager.java @@ -0,0 +1,8 @@ +package nextstep.security.authorization.manager; + +import nextstep.security.authentication.Authentication; + +@FunctionalInterface +public interface AuthorizationManager { + AuthorizationResult authorize(Authentication authentication, T target); +} From 814f82328e60bf13b989409225d013cef6f72194 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:28:44 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat:=20=EB=AC=B4=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=ED=97=88=EC=9A=A9=20=EB=B0=8F=20=ED=97=88=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=98=B5=EC=85=98=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manager/DenyAllAuthorizationManager.java | 12 ++++++++++++ .../manager/PermitAllAuthorizationManager.java | 12 ++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 src/main/java/nextstep/security/authorization/manager/DenyAllAuthorizationManager.java create mode 100644 src/main/java/nextstep/security/authorization/manager/PermitAllAuthorizationManager.java diff --git a/src/main/java/nextstep/security/authorization/manager/DenyAllAuthorizationManager.java b/src/main/java/nextstep/security/authorization/manager/DenyAllAuthorizationManager.java new file mode 100644 index 0000000..a611aa2 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/DenyAllAuthorizationManager.java @@ -0,0 +1,12 @@ +package nextstep.security.authorization.manager; + +import nextstep.security.authentication.Authentication; + +import static nextstep.security.authorization.manager.AuthorizationDecision.NOT_GRANTED; + +public class DenyAllAuthorizationManager implements AuthorizationManager { + @Override + public AuthorizationResult authorize(Authentication authentication, T target) { + return NOT_GRANTED; + } +} diff --git a/src/main/java/nextstep/security/authorization/manager/PermitAllAuthorizationManager.java b/src/main/java/nextstep/security/authorization/manager/PermitAllAuthorizationManager.java new file mode 100644 index 0000000..4df5d5d --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/PermitAllAuthorizationManager.java @@ -0,0 +1,12 @@ +package nextstep.security.authorization.manager; + +import nextstep.security.authentication.Authentication; + +import static nextstep.security.authorization.manager.AuthorizationDecision.GRANTED; + +public class PermitAllAuthorizationManager implements AuthorizationManager { + @Override + public AuthorizationResult authorize(Authentication authentication, T target) { + return GRANTED; + } +} From ac8a7939c688d0c7d3f0ee69444625f140916c87 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:29:00 +0900 Subject: [PATCH 06/20] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/authorization/manager/AuthorizationResult.java | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/main/java/nextstep/security/authorization/manager/AuthorizationResult.java diff --git a/src/main/java/nextstep/security/authorization/manager/AuthorizationResult.java b/src/main/java/nextstep/security/authorization/manager/AuthorizationResult.java new file mode 100644 index 0000000..987157f --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/AuthorizationResult.java @@ -0,0 +1,5 @@ +package nextstep.security.authorization.manager; + +public interface AuthorizationResult { + boolean isGranted(); +} From d682f6d5dbea9afd98222b1b0c9c83fb94a44d54 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:30:16 +0900 Subject: [PATCH 07/20] =?UTF-8?q?feat:=20=EA=B0=81=20url=20=EB=B3=84=20?= =?UTF-8?q?=EC=9D=B8=EA=B0=80=20=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/app/SecurityConfig.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 1683058..12d04aa 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -1,13 +1,16 @@ 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.AuthorizationFilter; import nextstep.security.authorization.CheckAuthenticationFilter; import nextstep.security.authorization.SecuredAspect; import nextstep.security.authorization.SecuredMethodInterceptor; +import nextstep.security.authorization.manager.*; import nextstep.security.config.DefaultSecurityFilterChain; import nextstep.security.config.DelegatingFilterProxy; import nextstep.security.config.FilterChainProxy; @@ -22,6 +25,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 +53,20 @@ public FilterChainProxy filterChainProxy(List securityFilte public SecuredMethodInterceptor securedMethodInterceptor() { return new SecuredMethodInterceptor(); } -// @Bean -// public SecuredAspect securedAspect() { -// return new SecuredAspect(); -// } @Bean public SecurityFilterChain securityFilterChain() { + final AuthorizationManager authorizationManager = new RequestAuthorizationManager(List.of( + createMvcMatcher(GET, "/members", new AuthorityAuthorizationManager<>("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) ) ); } From 9f0f2fa09b2796894db8184e011975f5841315cf Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:33:54 +0900 Subject: [PATCH 08/20] =?UTF-8?q?feat:=20member=20me=20url=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/ui/MemberController.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index 823cf7e..12e8c83 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -2,12 +2,16 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationException; import nextstep.security.authorization.Secured; +import nextstep.security.context.SecurityContextHolder; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.Optional; @RestController public class MemberController { @@ -30,4 +34,17 @@ public ResponseEntity> search() { List members = memberRepository.findAll(); return ResponseEntity.ok(members); } + + @Secured("USER") + @GetMapping("/members/me") + public ResponseEntity me() { + final Authentication authentication = SecurityContextHolder + .getContext().getAuthentication(); + + Member member = memberRepository.findByEmail( + authentication.getPrincipal().toString() + ).orElseThrow(AuthenticationException::new); + + return ResponseEntity.ok(member); + } } From 445bab9d942873ff98dcc7de46010a85973e0f50 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:34:32 +0900 Subject: [PATCH 09/20] =?UTF-8?q?feat:=20=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EB=B3=84=20=EA=B6=8C=ED=95=9C=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SecuredMethodInterceptor.java | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/main/java/nextstep/security/authorization/SecuredMethodInterceptor.java b/src/main/java/nextstep/security/authorization/SecuredMethodInterceptor.java index 8ee7409..76d14bc 100644 --- a/src/main/java/nextstep/security/authorization/SecuredMethodInterceptor.java +++ b/src/main/java/nextstep/security/authorization/SecuredMethodInterceptor.java @@ -2,6 +2,7 @@ import nextstep.security.authentication.Authentication; import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authorization.manager.SecuredAuthorizationManager; import nextstep.security.context.SecurityContextHolder; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; @@ -11,32 +12,42 @@ import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; -import java.lang.reflect.Method; - public class SecuredMethodInterceptor implements MethodInterceptor, PointcutAdvisor, AopInfrastructureBean { + private final SecuredAuthorizationManager securedAuthorizationManager; + private final Pointcut pointcut; public SecuredMethodInterceptor() { this.pointcut = new AnnotationMatchingPointcut(null, Secured.class); + this.securedAuthorizationManager = new SecuredAuthorizationManager(); } @Override public Object invoke(MethodInvocation invocation) throws Throwable { - Method method = invocation.getMethod(); - if (method.isAnnotationPresent(Secured.class)) { - Secured secured = method.getAnnotation(Secured.class); - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null) { - throw new AuthenticationException(); - } - if (!authentication.getAuthorities().contains(secured.value())) { - throw new ForbiddenException(); - } + if (!invocation.getMethod().isAnnotationPresent(Secured.class)) { + return invocation.proceed(); } + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + checkAuthenticated(authentication); + checkAuthorize(authentication, invocation); + return invocation.proceed(); } + private void checkAuthenticated(Authentication authentication) { + if (authentication == null || !authentication.isAuthenticated()) { + throw new AuthenticationException(); + } + } + + private void checkAuthorize(Authentication authentication, MethodInvocation invocation) { + if (!securedAuthorizationManager.authorize(authentication, invocation).isGranted()) { + throw new ForbiddenException(); + } + } + @Override public Pointcut getPointcut() { return pointcut; From f048ae3832389af03b624223b8a5b7e2a8cee845 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:35:30 +0900 Subject: [PATCH 10/20] =?UTF-8?q?feat:=20request=20AuthorizationManger=20e?= =?UTF-8?q?ntry=20=EC=B2=B4=ED=81=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manager/RequestAuthorizationManager.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/main/java/nextstep/security/authorization/manager/RequestAuthorizationManager.java diff --git a/src/main/java/nextstep/security/authorization/manager/RequestAuthorizationManager.java b/src/main/java/nextstep/security/authorization/manager/RequestAuthorizationManager.java new file mode 100644 index 0000000..53cfa0a --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/RequestAuthorizationManager.java @@ -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 { + private final List>> entries; + private final RequestMatcherEntry> defaultEntry; + + public RequestAuthorizationManager( + List>> entries, + RequestMatcherEntry> defaultEntry + ) { + this.entries = entries; + this.defaultEntry = defaultEntry; + } + + @Override + public AuthorizationResult authorize(Authentication authentication, HttpServletRequest target) { + if (noneMatch(target)) { + return AuthorizationDecision.from( + check(authentication, target, defaultEntry) + ); + } + return AuthorizationDecision.from( + 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> matcherEntry + ) { + return !matcherEntry.requestMatcher().matches(request) + || matcherEntry.entry().authorize(authentication, request).isGranted(); + } +} From 686f74fa0961cd7a740236446118eaaa1d4acde7 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:35:49 +0900 Subject: [PATCH 11/20] =?UTF-8?q?feat:=20RequestMatcher=20Entry=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../matcher/RequestMatcherEntry.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/main/java/nextstep/security/authorization/matcher/RequestMatcherEntry.java diff --git a/src/main/java/nextstep/security/authorization/matcher/RequestMatcherEntry.java b/src/main/java/nextstep/security/authorization/matcher/RequestMatcherEntry.java new file mode 100644 index 0000000..ab14e3f --- /dev/null +++ b/src/main/java/nextstep/security/authorization/matcher/RequestMatcherEntry.java @@ -0,0 +1,29 @@ +package nextstep.security.authorization.matcher; + +import jakarta.servlet.http.HttpServletRequest; +import nextstep.security.authorization.manager.AuthorizationManager; +import org.springframework.http.HttpMethod; + +public record RequestMatcherEntry( + RequestMatcher requestMatcher, + T entry +) { + public static RequestMatcherEntry> createMvcMatcher( + HttpMethod method, String pattern, + AuthorizationManager authorizationManager + ) { + return new RequestMatcherEntry<>( + new MvcRequestMatcher(method, pattern), + authorizationManager + ); + } + + public static RequestMatcherEntry> createDefaultMatcher( + AuthorizationManager authorizationManager + ) { + return new RequestMatcherEntry<>( + new AnyRequestMatcher(), + authorizationManager + ); + } +} From 2c694df7123c4a82ded76e227913449d6b2496f9 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:36:02 +0900 Subject: [PATCH 12/20] =?UTF-8?q?feat:=20Matcher=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/authorization/matcher/RequestMatcher.java | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/main/java/nextstep/security/authorization/matcher/RequestMatcher.java diff --git a/src/main/java/nextstep/security/authorization/matcher/RequestMatcher.java b/src/main/java/nextstep/security/authorization/matcher/RequestMatcher.java new file mode 100644 index 0000000..3878f78 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/matcher/RequestMatcher.java @@ -0,0 +1,7 @@ +package nextstep.security.authorization.matcher; + +import jakarta.servlet.http.HttpServletRequest; + +public interface RequestMatcher { + boolean matches(HttpServletRequest request); +} From a5e876844959180ab605f3cdd720974eebe8ae1b Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:36:16 +0900 Subject: [PATCH 13/20] =?UTF-8?q?feat:=20Mvc=20interface=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../matcher/MvcRequestMatcher.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/main/java/nextstep/security/authorization/matcher/MvcRequestMatcher.java diff --git a/src/main/java/nextstep/security/authorization/matcher/MvcRequestMatcher.java b/src/main/java/nextstep/security/authorization/matcher/MvcRequestMatcher.java new file mode 100644 index 0000000..e8266c4 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/matcher/MvcRequestMatcher.java @@ -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(); + } +} From 0a0cdf27afdf176643b2c0355a005b3d44d4fbec Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:36:41 +0900 Subject: [PATCH 14/20] =?UTF-8?q?feat:=20=EB=AA=A8=EB=93=A0=EA=B0=92?= =?UTF-8?q?=EC=9D=B4=20true=EC=9D=B8=20matcher=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../authorization/matcher/AnyRequestMatcher.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/nextstep/security/authorization/matcher/AnyRequestMatcher.java diff --git a/src/main/java/nextstep/security/authorization/matcher/AnyRequestMatcher.java b/src/main/java/nextstep/security/authorization/matcher/AnyRequestMatcher.java new file mode 100644 index 0000000..3a8c640 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/matcher/AnyRequestMatcher.java @@ -0,0 +1,11 @@ +package nextstep.security.authorization.matcher; + +import jakarta.servlet.http.HttpServletRequest; + +public class AnyRequestMatcher implements RequestMatcher { + + @Override + public boolean matches(HttpServletRequest request) { + return true; + } +} From b2297e9ff89023e1a2034b97411e2ab80dca6b6d Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:37:08 +0900 Subject: [PATCH 15/20] =?UTF-8?q?feat:=20secured=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=EC=97=90=EC=84=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EC=9A=A9=EB=8F=84=20manger=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manager/SecuredAuthorizationManager.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/java/nextstep/security/authorization/manager/SecuredAuthorizationManager.java diff --git a/src/main/java/nextstep/security/authorization/manager/SecuredAuthorizationManager.java b/src/main/java/nextstep/security/authorization/manager/SecuredAuthorizationManager.java new file mode 100644 index 0000000..06c91d2 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/SecuredAuthorizationManager.java @@ -0,0 +1,25 @@ +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 { + + @Override + public AuthorizationResult authorize(Authentication authentication, MethodInvocation target) { + return AuthorizationDecision.from(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()); + } +} From 78a77a06e3edb685d535f22f6fa85b0a90a8d3e5 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:37:28 +0900 Subject: [PATCH 16/20] =?UTF-8?q?feat:=20authenticationFilter=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../authorization/AuthorizationFilter.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/main/java/nextstep/security/authorization/AuthorizationFilter.java diff --git a/src/main/java/nextstep/security/authorization/AuthorizationFilter.java b/src/main/java/nextstep/security/authorization/AuthorizationFilter.java new file mode 100644 index 0000000..091eb77 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AuthorizationFilter.java @@ -0,0 +1,37 @@ +package nextstep.security.authorization; + +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.AuthenticationException; +import nextstep.security.authorization.manager.AuthorizationManager; +import nextstep.security.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class AuthorizationFilter extends OncePerRequestFilter { + private final AuthorizationManager authorizationManager; + + public AuthorizationFilter(AuthorizationManager authorizationManager) { + this.authorizationManager = authorizationManager; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + throw new AuthenticationException(); + } + if (!authorizationManager.authorize(authentication, request).isGranted()) { + throw new ForbiddenException(); + } + filterChain.doFilter(request, response); + } +} From b73d13679d403afa5dad13146bba7e1915d8ff28 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:39:01 +0900 Subject: [PATCH 17/20] =?UTF-8?q?feat:=20filter=20Proxy=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/config/FilterChainProxy.java | 22 +++++++++--- .../java/nextstep/fixture/MemberFixture.java | 36 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 src/test/java/nextstep/fixture/MemberFixture.java diff --git a/src/main/java/nextstep/security/config/FilterChainProxy.java b/src/main/java/nextstep/security/config/FilterChainProxy.java index e40cea2..410bc65 100644 --- a/src/main/java/nextstep/security/config/FilterChainProxy.java +++ b/src/main/java/nextstep/security/config/FilterChainProxy.java @@ -1,5 +1,8 @@ package nextstep.security.config; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authorization.ForbiddenException; import org.springframework.web.filter.GenericFilterBean; import jakarta.servlet.*; @@ -7,6 +10,8 @@ import java.io.IOException; import java.util.List; +import static jakarta.servlet.http.HttpServletResponse.*; + public class FilterChainProxy extends GenericFilterBean { private final List filterChains; @@ -16,10 +21,19 @@ public FilterChainProxy(List filterChains) { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - List filters = getFilters((HttpServletRequest) request); - - VirtualFilterChain virtualFilterChain = new VirtualFilterChain(chain, filters); - virtualFilterChain.doFilter(request, response); + final HttpServletRequest httpRequest = (HttpServletRequest) request; + final HttpServletResponse httpResponse = (HttpServletResponse) response; + try { + new VirtualFilterChain( + chain, getFilters(httpRequest) + ).doFilter(request, response); + } catch (AuthenticationException e) { + httpResponse.setStatus(SC_UNAUTHORIZED); + } catch (ForbiddenException e) { + httpResponse.setStatus(SC_FORBIDDEN); + } catch (Exception e) { + httpResponse.setStatus(SC_INTERNAL_SERVER_ERROR); + } } private List getFilters(HttpServletRequest request) { diff --git a/src/test/java/nextstep/fixture/MemberFixture.java b/src/test/java/nextstep/fixture/MemberFixture.java new file mode 100644 index 0000000..2a0e6b3 --- /dev/null +++ b/src/test/java/nextstep/fixture/MemberFixture.java @@ -0,0 +1,36 @@ +package nextstep.fixture; + +import nextstep.app.domain.Member; + +import java.util.Set; + +public enum MemberFixture { + TEST_ADMIN_MEMBER(new Member( + "a@a.com", "password", + "a", "", + Set.of("USER", "ADMIN") + )), + TEST_USER_MEMBER(new Member( + "b@b.com", "password", + "b", "", + Set.of("USER") + )); + + private final Member member; + + MemberFixture(Member member) { + this.member = member; + } + + public Member getMember() { + return member; + } + + public String getEmail() { + return member.getEmail(); + } + + public String getPassword() { + return member.getPassword(); + } +} From db26ae5e3a79a5f62dbcb3b13a6acf772d711c56 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:46:02 +0900 Subject: [PATCH 18/20] =?UTF-8?q?fix:=20Servelt,=20IO=20Exception=20?= =?UTF-8?q?=EB=8D=98=EC=A7=80=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BasicAuthenticationFilter.java | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java index 406116f..4cf3809 100644 --- a/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java +++ b/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java @@ -1,6 +1,7 @@ package nextstep.security.authentication; import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import nextstep.security.context.SecurityContext; @@ -10,6 +11,7 @@ import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; @@ -26,23 +28,23 @@ public BasicAuthenticationFilter(UserDetailsService userDetailsService) { } @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { - try { - Authentication authentication = convert(request); - if (authentication == null) { - filterChain.doFilter(request, response); - return; - } - - Authentication authResult = this.authenticationManager.authenticate(authentication); - SecurityContext context = SecurityContextHolder.createEmptyContext(); - context.setAuthentication(authResult); - SecurityContextHolder.setContext(context); - + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + Authentication authentication = convert(request); + if (authentication == null) { filterChain.doFilter(request, response); - } catch (Exception e) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; } + + Authentication authResult = this.authenticationManager.authenticate(authentication); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authResult); + SecurityContextHolder.setContext(context); + + filterChain.doFilter(request, response); } private Authentication convert(HttpServletRequest request) { From cd6fa2f46e860b4308a1114984d417d24dc684a3 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:49:10 +0900 Subject: [PATCH 19/20] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...AuthenticatedAuthorizationManagerTest.java | 50 +++++++++++++++ .../AuthorityAuthorizationManagerTest.java | 62 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 src/test/java/nextstep/security/authorization/manager/AuthenticatedAuthorizationManagerTest.java create mode 100644 src/test/java/nextstep/security/authorization/manager/AuthorityAuthorizationManagerTest.java diff --git a/src/test/java/nextstep/security/authorization/manager/AuthenticatedAuthorizationManagerTest.java b/src/test/java/nextstep/security/authorization/manager/AuthenticatedAuthorizationManagerTest.java new file mode 100644 index 0000000..b4588c0 --- /dev/null +++ b/src/test/java/nextstep/security/authorization/manager/AuthenticatedAuthorizationManagerTest.java @@ -0,0 +1,50 @@ +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; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AuthenticatedAuthorizationManagerTest { + private final AuthorizationManager manager = new AuthenticatedAuthorizationManager<>(); + + @DisplayName("유저가 인증되지 않으면 인가되지 않은 상태이다.") + @Test + void notAuthenticated() { + assertAll( + () -> assertThat(manager.authorize(null, null)).isEqualTo(NOT_GRANTED), + () -> assertThat(manager.authorize(unauthenticatedUser(), null)).isEqualTo(NOT_GRANTED) + ); + } + + @DisplayName("유저가 인증되면 인가된 상태이다.") + @Test + void authenticated() { + assertThat(manager.authorize(authenticatedUser(), null)).isEqualTo(GRANTED); + } + + private Authentication authenticatedUser() { + return createAuthentication(true); + } + + private Authentication unauthenticatedUser() { + return createAuthentication(false); + } + + private Authentication createAuthentication(boolean isAuthenticated) { + Authentication authentication = mock(Authentication.class); + when(authentication.isAuthenticated()).thenReturn(isAuthenticated); + when(authentication.getAuthorities()).thenReturn(Set.of()); + when(authentication.getCredentials()).thenReturn("PASSWORD"); + when(authentication.getPrincipal()).thenReturn("USERNAME"); + return authentication; + } +} diff --git a/src/test/java/nextstep/security/authorization/manager/AuthorityAuthorizationManagerTest.java b/src/test/java/nextstep/security/authorization/manager/AuthorityAuthorizationManagerTest.java new file mode 100644 index 0000000..7a43837 --- /dev/null +++ b/src/test/java/nextstep/security/authorization/manager/AuthorityAuthorizationManagerTest.java @@ -0,0 +1,62 @@ +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; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AuthorityAuthorizationManagerTest { + private final AuthorizationManager manager = new AuthorityAuthorizationManager<>( + "ADMIN", "USER" + ); + + @DisplayName("인증되지 않는 유저는 인가를 받지 않는다.") + @Test + void notAuthenticated() { + assertAll( + () -> assertThat(manager.authorize(null, null)).isEqualTo(NOT_GRANTED), + () -> assertThat(manager.authorize(unauthenticatedUser(), null)).isEqualTo(NOT_GRANTED) + ); + } + + @DisplayName("어드민 유저는 인가을 받는다.") + @Test + void adminWithAuthority() { + AuthorizationResult admin = manager.authorize(authenticatedUserWithAuthorities("ADMIN"), null); + + assertThat(admin).isEqualTo(GRANTED); + } + + @DisplayName("일반 유저는 인가를 받는다.") + @Test + void userWithAuthority() { + AuthorizationResult user = manager.authorize(authenticatedUserWithAuthorities("USER"), null); + + assertThat(user).isEqualTo(GRANTED); + } + + private Authentication authenticatedUserWithAuthorities(String... authorities) { + return createAuthentication(true, authorities); + } + + private Authentication unauthenticatedUser() { + return createAuthentication(false); + } + + private Authentication createAuthentication(boolean isAuthenticated, String... authorities) { + Authentication authentication = mock(Authentication.class); + when(authentication.isAuthenticated()).thenReturn(isAuthenticated); + when(authentication.getAuthorities()).thenReturn(Set.of(authorities)); + when(authentication.getCredentials()).thenReturn("PASSWORD"); + when(authentication.getPrincipal()).thenReturn("USERNAME"); + return authentication; + } +} From 713f5eb88baa92c5f48b74b6a9dd81e97c7e7a27 Mon Sep 17 00:00:00 2001 From: evan Date: Sun, 16 Feb 2025 09:52:39 +0900 Subject: [PATCH 20/20] =?UTF-8?q?docs:=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 61f417c..25a7a98 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ # spring-security-authorization + +# 🚀 1단계 - AuthorizationManager를 활용 + +- [x] AuthorizationManager를 활용하여 인가 과정 추상화 + - [x] SecuredAuthorizationManager 구현 + - [x] RequestAuthorizationManager 구현 + - [x] SecuredAuthorizationManager 구현 + +# 🚀 2단계 - 요청별 권한 검증 정보 분리 + +- [x] 요청별 권한 검증 정보를 별도의 객체로 분리하여 관리 + - [x] RequestMatcher 작성 + - [x] AnyRequestMatcher 구현 + - [x] MvcRequestMatcher 구현 + - [x] RequestMatcherEntry 작성 +