From 22f077426297ad83018bc24aa038ae65802aea6d Mon Sep 17 00:00:00 2001 From: boorownie Date: Fri, 24 Jan 2025 17:18:23 +0900 Subject: [PATCH 01/10] practice - /members/me --- .../nextstep/app/ui/MemberController.java | 17 +++++++++++++ src/test/java/nextstep/app/BasicAuthTest.java | 24 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index 823cf7e..79c905b 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -2,7 +2,10 @@ 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; @@ -30,4 +33,18 @@ public ResponseEntity> search() { List members = memberRepository.findAll(); return ResponseEntity.ok(members); } + + @GetMapping("/members/me") + public ResponseEntity me() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + throw new AuthenticationException(); + } + + String email = authentication.getPrincipal().toString(); + Member member = memberRepository.findByEmail(email) + .orElseThrow(RuntimeException::new); + + return ResponseEntity.ok(member); + } } diff --git a/src/test/java/nextstep/app/BasicAuthTest.java b/src/test/java/nextstep/app/BasicAuthTest.java index ac117be..2dea9bb 100644 --- a/src/test/java/nextstep/app/BasicAuthTest.java +++ b/src/test/java/nextstep/app/BasicAuthTest.java @@ -89,4 +89,28 @@ void request_fail_invalid_password() throws Exception { response.andExpect(status().isUnauthorized()); } + + @DisplayName("인증된 사용자는 자신의 정보를 조회할 수 있다.") + @Test + void request_success_members_me() throws Exception { + String token = Base64.getEncoder().encodeToString((TEST_ADMIN_MEMBER.getEmail() + ":" + TEST_ADMIN_MEMBER.getPassword()).getBytes()); + + ResultActions response = mockMvc.perform(get("/members/me") + .header("Authorization", "Basic " + token) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ); + + response.andExpect(status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.email").value(TEST_ADMIN_MEMBER.getEmail())); + } + + @DisplayName("인증되지 않은 사용자는 자신의 정보를 조회할 수 없다.") + @Test + void request_fail_members_me() throws Exception { + ResultActions response = mockMvc.perform(get("/members/me") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ); + + response.andExpect(status().isUnauthorized()); + } } From d299c59c86324ebf5d99ae6e25d28d6eba575dde Mon Sep 17 00:00:00 2001 From: boorownie Date: Fri, 24 Jan 2025 17:19:30 +0900 Subject: [PATCH 02/10] practice - authorization filter --- .../java/nextstep/app/SecurityConfig.java | 5 +- .../BasicAuthenticationFilter.java | 7 ++- .../authorization/AuthorizationFilter.java | 52 +++++++++++++++++++ .../CheckAuthenticationFilter.java | 38 -------------- 4 files changed, 60 insertions(+), 42 deletions(-) create mode 100644 src/main/java/nextstep/security/authorization/AuthorizationFilter.java delete mode 100644 src/main/java/nextstep/security/authorization/CheckAuthenticationFilter.java diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 1683058..e2bfc3f 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -5,8 +5,7 @@ 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.config.DefaultSecurityFilterChain; import nextstep.security.config.DelegatingFilterProxy; @@ -58,7 +57,7 @@ public SecurityFilterChain securityFilterChain() { new SecurityContextHolderFilter(), new UsernamePasswordAuthenticationFilter(userDetailsService()), new BasicAuthenticationFilter(userDetailsService()), - new CheckAuthenticationFilter() + new AuthorizationFilter() ) ); } diff --git a/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java index 406116f..05cc1ad 100644 --- a/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java +++ b/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java @@ -3,6 +3,7 @@ import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.authorization.ForbiddenException; import nextstep.security.context.SecurityContext; import nextstep.security.context.SecurityContextHolder; import nextstep.security.userdetails.UserDetailsService; @@ -40,8 +41,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse SecurityContextHolder.setContext(context); filterChain.doFilter(request, response); - } catch (Exception e) { + } 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); } } 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..b1e100b --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AuthorizationFilter.java @@ -0,0 +1,52 @@ +package nextstep.security.authorization; + +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationException; +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 java.io.IOException; + +public class AuthorizationFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + boolean isGranted = checkAuthorization(authentication, request); + + if (!isGranted) { + throw new ForbiddenException(); + } + + filterChain.doFilter(request, response); + } + + private boolean checkAuthorization(Authentication authentication, HttpServletRequest httpRequest) { + if (httpRequest.getRequestURI().equals("/members")) { + if (authentication == null) { + throw new AuthenticationException(); + } + + return authentication.getAuthorities().stream() + .anyMatch(authority -> authority.equals("ADMIN")); + } + + if (httpRequest.getRequestURI().equals("/members/me")) { + if (authentication == null) { + throw new AuthenticationException(); + } + + return authentication.isAuthenticated(); + } + + if (httpRequest.getRequestURI().equals("/search")) { + return true; + } + + return false; + } +} diff --git a/src/main/java/nextstep/security/authorization/CheckAuthenticationFilter.java b/src/main/java/nextstep/security/authorization/CheckAuthenticationFilter.java deleted file mode 100644 index 02327ff..0000000 --- a/src/main/java/nextstep/security/authorization/CheckAuthenticationFilter.java +++ /dev/null @@ -1,38 +0,0 @@ -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 java.io.IOException; -import java.util.Set; - -public class CheckAuthenticationFilter extends OncePerRequestFilter { - private static final String DEFAULT_REQUEST_URI = "/members"; - - @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 authorities = authentication.getAuthorities(); - if (!authorities.contains("ADMIN")) { - response.setStatus(HttpServletResponse.SC_FORBIDDEN); - return; - } - - filterChain.doFilter(request, response); - } -} From 6da9eb19fab5e2464f7a176aa13284038875c41b Mon Sep 17 00:00:00 2001 From: yeongunheo Date: Wed, 12 Feb 2025 22:07:19 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20AuthorizationManager=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../authorization/AuthorizationDecision.java | 14 ++++++++++++++ .../authorization/AuthorizationManager.java | 8 ++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/main/java/nextstep/security/authorization/AuthorizationDecision.java create mode 100644 src/main/java/nextstep/security/authorization/AuthorizationManager.java diff --git a/src/main/java/nextstep/security/authorization/AuthorizationDecision.java b/src/main/java/nextstep/security/authorization/AuthorizationDecision.java new file mode 100644 index 0000000..0af2f21 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AuthorizationDecision.java @@ -0,0 +1,14 @@ +package nextstep.security.authorization; + +public class AuthorizationDecision { + + private final boolean granted; + + public AuthorizationDecision(boolean granted) { + this.granted = granted; + } + + public boolean isGranted() { + return granted; + } +} diff --git a/src/main/java/nextstep/security/authorization/AuthorizationManager.java b/src/main/java/nextstep/security/authorization/AuthorizationManager.java new file mode 100644 index 0000000..42b8610 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AuthorizationManager.java @@ -0,0 +1,8 @@ +package nextstep.security.authorization; + +import nextstep.security.authentication.Authentication; + +@FunctionalInterface +public interface AuthorizationManager { + AuthorizationDecision check(Authentication authentication, T object); +} From 838c0fc13272b79e5b87c4b1a0463f42232c76f2 Mon Sep 17 00:00:00 2001 From: yeongunheo Date: Wed, 12 Feb 2025 22:08:11 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20RequestMatcherDelegatingAuthoriza?= =?UTF-8?q?tionManager=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RequestAuthorizationContext.java | 29 ++++++++++++ ...MatcherDelegatingAuthorizationManager.java | 33 +++++++++++++ .../security/util/RequestMatcher.java | 46 +++++++++++++++++++ .../security/util/RequestMatcherEntry.java | 20 ++++++++ 4 files changed, 128 insertions(+) create mode 100644 src/main/java/nextstep/security/authorization/RequestAuthorizationContext.java create mode 100644 src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java create mode 100644 src/main/java/nextstep/security/util/RequestMatcher.java create mode 100644 src/main/java/nextstep/security/util/RequestMatcherEntry.java diff --git a/src/main/java/nextstep/security/authorization/RequestAuthorizationContext.java b/src/main/java/nextstep/security/authorization/RequestAuthorizationContext.java new file mode 100644 index 0000000..355964f --- /dev/null +++ b/src/main/java/nextstep/security/authorization/RequestAuthorizationContext.java @@ -0,0 +1,29 @@ +package nextstep.security.authorization; + +import jakarta.servlet.http.HttpServletRequest; + +import java.util.Collections; +import java.util.Map; + +public class RequestAuthorizationContext { + + private final HttpServletRequest request; + private final Map variables; + + public RequestAuthorizationContext(HttpServletRequest request) { + this(request, Collections.emptyMap()); + } + + public RequestAuthorizationContext(HttpServletRequest request, Map variables) { + this.request = request; + this.variables = variables; + } + + public HttpServletRequest getRequest() { + return this.request; + } + + public Map getVariables() { + return this.variables; + } +} diff --git a/src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java b/src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java new file mode 100644 index 0000000..c0f740a --- /dev/null +++ b/src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java @@ -0,0 +1,33 @@ +package nextstep.security.authorization; + +import jakarta.servlet.http.HttpServletRequest; +import nextstep.security.authentication.Authentication; +import nextstep.security.util.RequestMatcher; +import nextstep.security.util.RequestMatcherEntry; + +import java.util.List; + +public class RequestMatcherDelegatingAuthorizationManager implements AuthorizationManager { + + private final List>> mappings; + + public RequestMatcherDelegatingAuthorizationManager(List>> mappings) { + this.mappings = mappings; + } + + @Override + public AuthorizationDecision check(Authentication authentication, HttpServletRequest request) { + + for (RequestMatcherEntry> mapping : this.mappings) { + RequestMatcher matcher = mapping.getRequestMatcher(); + RequestMatcher.MatchResult matchResult = matcher.matcher(request); + if (matchResult.isMatch()) { + AuthorizationManager manager = mapping.getEntry(); + + return manager.check(authentication, new RequestAuthorizationContext(request, matchResult.getVariables())); + } + } + + return new AuthorizationDecision(false); + } +} diff --git a/src/main/java/nextstep/security/util/RequestMatcher.java b/src/main/java/nextstep/security/util/RequestMatcher.java new file mode 100644 index 0000000..17145d7 --- /dev/null +++ b/src/main/java/nextstep/security/util/RequestMatcher.java @@ -0,0 +1,46 @@ +package nextstep.security.util; + +import jakarta.servlet.http.HttpServletRequest; + +import java.util.Collections; +import java.util.Map; + +public interface RequestMatcher { + + boolean matches(HttpServletRequest request); + + default MatchResult matcher(HttpServletRequest request) { + boolean match = this.matches(request); + return new MatchResult(match, Collections.emptyMap()); + } + + class MatchResult { + private final boolean match; + private final Map variables; + + MatchResult(boolean match, Map variables) { + this.match = match; + this.variables = variables; + } + + public boolean isMatch() { + return this.match; + } + + public Map getVariables() { + return this.variables; + } + + public static MatchResult match() { + return new MatchResult(true, Collections.emptyMap()); + } + + public static MatchResult match(Map variables) { + return new MatchResult(true, variables); + } + + public static MatchResult notMatch() { + return new MatchResult(false, Collections.emptyMap()); + } + } +} diff --git a/src/main/java/nextstep/security/util/RequestMatcherEntry.java b/src/main/java/nextstep/security/util/RequestMatcherEntry.java new file mode 100644 index 0000000..7122584 --- /dev/null +++ b/src/main/java/nextstep/security/util/RequestMatcherEntry.java @@ -0,0 +1,20 @@ +package nextstep.security.util; + +public class RequestMatcherEntry { + + private final RequestMatcher requestMatcher; + private final T entry; + + public RequestMatcherEntry(RequestMatcher requestMatcher, T entry) { + this.requestMatcher = requestMatcher; + this.entry = entry; + } + + public RequestMatcher getRequestMatcher() { + return requestMatcher; + } + + public T getEntry() { + return entry; + } +} From ce137c55cfd3b227d637e31d31dab145a9f25e78 Mon Sep 17 00:00:00 2001 From: yeongunheo Date: Thu, 13 Feb 2025 02:59:06 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20HTTP=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=9D=B8=EA=B0=80=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/SecurityConfig.java | 14 ++++++- .../AnonymousAuthorizationStrategy.java | 11 ++++++ .../AuthenticatedAuthorizationManager.java | 18 +++++++++ .../AuthorityAuthorizationManager.java | 24 ++++++++++++ .../authorization/AuthorizationFilter.java | 37 +++++-------------- .../authorization/AuthorizationStrategy.java | 8 ++++ .../DefaultAuthorizationStrategy.java | 15 ++++++++ .../security/util/MvcRequestMatcher.java | 20 ++++++++++ 8 files changed, 119 insertions(+), 28 deletions(-) create mode 100644 src/main/java/nextstep/security/authorization/AnonymousAuthorizationStrategy.java create mode 100644 src/main/java/nextstep/security/authorization/AuthenticatedAuthorizationManager.java create mode 100644 src/main/java/nextstep/security/authorization/AuthorityAuthorizationManager.java create mode 100644 src/main/java/nextstep/security/authorization/AuthorizationStrategy.java create mode 100644 src/main/java/nextstep/security/authorization/DefaultAuthorizationStrategy.java create mode 100644 src/main/java/nextstep/security/util/MvcRequestMatcher.java diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index e2bfc3f..c2c99ef 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -5,7 +5,12 @@ import nextstep.security.authentication.AuthenticationException; import nextstep.security.authentication.BasicAuthenticationFilter; import nextstep.security.authentication.UsernamePasswordAuthenticationFilter; +import nextstep.security.authorization.AnonymousAuthorizationStrategy; +import nextstep.security.authorization.AuthenticatedAuthorizationManager; +import nextstep.security.authorization.AuthorityAuthorizationManager; import nextstep.security.authorization.AuthorizationFilter; +import nextstep.security.authorization.DefaultAuthorizationStrategy; +import nextstep.security.authorization.RequestMatcherDelegatingAuthorizationManager; import nextstep.security.authorization.SecuredMethodInterceptor; import nextstep.security.config.DefaultSecurityFilterChain; import nextstep.security.config.DelegatingFilterProxy; @@ -14,9 +19,12 @@ import nextstep.security.context.SecurityContextHolderFilter; import nextstep.security.userdetails.UserDetails; import nextstep.security.userdetails.UserDetailsService; +import nextstep.security.util.MvcRequestMatcher; +import nextstep.security.util.RequestMatcherEntry; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.http.HttpMethod; import java.util.List; import java.util.Set; @@ -57,7 +65,11 @@ public SecurityFilterChain securityFilterChain() { new SecurityContextHolderFilter(), new UsernamePasswordAuthenticationFilter(userDetailsService()), new BasicAuthenticationFilter(userDetailsService()), - new AuthorizationFilter() + new AuthorizationFilter(new RequestMatcherDelegatingAuthorizationManager(List.of( + new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members"), new AuthorityAuthorizationManager<>(Set.of("ADMIN"))), + new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members/me"), new AuthenticatedAuthorizationManager<>(new DefaultAuthorizationStrategy())), + new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/search"), new AuthenticatedAuthorizationManager<>(new AnonymousAuthorizationStrategy())) + ))) ) ); } diff --git a/src/main/java/nextstep/security/authorization/AnonymousAuthorizationStrategy.java b/src/main/java/nextstep/security/authorization/AnonymousAuthorizationStrategy.java new file mode 100644 index 0000000..0633bcd --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AnonymousAuthorizationStrategy.java @@ -0,0 +1,11 @@ +package nextstep.security.authorization; + +import nextstep.security.authentication.Authentication; + +public class AnonymousAuthorizationStrategy implements AuthorizationStrategy { + + @Override + public boolean isGranted(Authentication authentication) { + return true; + } +} diff --git a/src/main/java/nextstep/security/authorization/AuthenticatedAuthorizationManager.java b/src/main/java/nextstep/security/authorization/AuthenticatedAuthorizationManager.java new file mode 100644 index 0000000..f7fdca9 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AuthenticatedAuthorizationManager.java @@ -0,0 +1,18 @@ +package nextstep.security.authorization; + +import nextstep.security.authentication.Authentication; + +public class AuthenticatedAuthorizationManager implements AuthorizationManager { + + private final AuthorizationStrategy authorizationStrategy; + + public AuthenticatedAuthorizationManager(AuthorizationStrategy authorizationStrategy) { + this.authorizationStrategy = authorizationStrategy; + } + + @Override + public AuthorizationDecision check(Authentication authentication, T object) { + var granted = authorizationStrategy.isGranted(authentication); + return new AuthorizationDecision(granted); + } +} diff --git a/src/main/java/nextstep/security/authorization/AuthorityAuthorizationManager.java b/src/main/java/nextstep/security/authorization/AuthorityAuthorizationManager.java new file mode 100644 index 0000000..27c178a --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AuthorityAuthorizationManager.java @@ -0,0 +1,24 @@ +package nextstep.security.authorization; + +import nextstep.security.authentication.Authentication; + +import java.util.Set; + +public class AuthorityAuthorizationManager implements AuthorizationManager { + + private final Set authorities; + + public AuthorityAuthorizationManager(Set authorities) { + this.authorities = authorities; + } + + @Override + public AuthorizationDecision check(Authentication authentication, T object) { + + boolean granted = authentication.getAuthorities() + .stream() + .anyMatch(authorities::contains); + + return new AuthorizationDecision(granted); + } +} diff --git a/src/main/java/nextstep/security/authorization/AuthorizationFilter.java b/src/main/java/nextstep/security/authorization/AuthorizationFilter.java index b1e100b..8835cd4 100644 --- a/src/main/java/nextstep/security/authorization/AuthorizationFilter.java +++ b/src/main/java/nextstep/security/authorization/AuthorizationFilter.java @@ -1,7 +1,6 @@ package nextstep.security.authorization; import nextstep.security.authentication.Authentication; -import nextstep.security.authentication.AuthenticationException; import nextstep.security.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; @@ -9,44 +8,28 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + 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 { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - boolean isGranted = checkAuthorization(authentication, request); + AuthorizationDecision decision = this.authorizationManager.check(authentication, request); - if (!isGranted) { + if (decision != null && !decision.isGranted()) { throw new ForbiddenException(); } filterChain.doFilter(request, response); - } - - private boolean checkAuthorization(Authentication authentication, HttpServletRequest httpRequest) { - if (httpRequest.getRequestURI().equals("/members")) { - if (authentication == null) { - throw new AuthenticationException(); - } - - return authentication.getAuthorities().stream() - .anyMatch(authority -> authority.equals("ADMIN")); - } - - if (httpRequest.getRequestURI().equals("/members/me")) { - if (authentication == null) { - throw new AuthenticationException(); - } - - return authentication.isAuthenticated(); - } - - if (httpRequest.getRequestURI().equals("/search")) { - return true; - } - return false; } } diff --git a/src/main/java/nextstep/security/authorization/AuthorizationStrategy.java b/src/main/java/nextstep/security/authorization/AuthorizationStrategy.java new file mode 100644 index 0000000..4cc9dcd --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AuthorizationStrategy.java @@ -0,0 +1,8 @@ +package nextstep.security.authorization; + +import nextstep.security.authentication.Authentication; + +public interface AuthorizationStrategy { + + boolean isGranted(Authentication authentication); +} diff --git a/src/main/java/nextstep/security/authorization/DefaultAuthorizationStrategy.java b/src/main/java/nextstep/security/authorization/DefaultAuthorizationStrategy.java new file mode 100644 index 0000000..b7b47cf --- /dev/null +++ b/src/main/java/nextstep/security/authorization/DefaultAuthorizationStrategy.java @@ -0,0 +1,15 @@ +package nextstep.security.authorization; + +import nextstep.security.authentication.Authentication; + +public class DefaultAuthorizationStrategy implements AuthorizationStrategy { + + @Override + public boolean isGranted(Authentication authentication) { + if (authentication == null) { + return false; + } + + return authentication.isAuthenticated(); + } +} diff --git a/src/main/java/nextstep/security/util/MvcRequestMatcher.java b/src/main/java/nextstep/security/util/MvcRequestMatcher.java new file mode 100644 index 0000000..63a05cd --- /dev/null +++ b/src/main/java/nextstep/security/util/MvcRequestMatcher.java @@ -0,0 +1,20 @@ +package nextstep.security.util; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpMethod; + +public class MvcRequestMatcher implements RequestMatcher { + + private final HttpMethod method; + private final String url; + + public MvcRequestMatcher(HttpMethod method, String url) { + this.method = method; + this.url = url; + } + + @Override + public boolean matches(HttpServletRequest request) { + return request.getMethod().equals(method.name()) && request.getRequestURI().equals(url); + } +} From 7e063b0ba31f5087be93ccc7b13793f89637796d Mon Sep 17 00:00:00 2001 From: yeongunheo Date: Fri, 14 Feb 2025 00:08:03 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20=EC=95=A0=EB=84=88=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EC=9D=84=20=ED=99=9C=EC=9A=A9=ED=95=9C=20?= =?UTF-8?q?=EC=9D=B8=EA=B0=80=20=EC=B2=98=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/SecurityConfig.java | 21 ++++---- ...izationManagerBeforeMethodInterceptor.java | 42 ++++++++++++++++ .../SecuredAuthorizationManager.java | 48 +++++++++++++++++++ 3 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 src/main/java/nextstep/security/authorization/AuthorizationManagerBeforeMethodInterceptor.java create mode 100644 src/main/java/nextstep/security/authorization/SecuredAuthorizationManager.java diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index c2c99ef..284cca0 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -9,9 +9,10 @@ import nextstep.security.authorization.AuthenticatedAuthorizationManager; import nextstep.security.authorization.AuthorityAuthorizationManager; import nextstep.security.authorization.AuthorizationFilter; +import nextstep.security.authorization.AuthorizationManagerBeforeMethodInterceptor; import nextstep.security.authorization.DefaultAuthorizationStrategy; import nextstep.security.authorization.RequestMatcherDelegatingAuthorizationManager; -import nextstep.security.authorization.SecuredMethodInterceptor; +import nextstep.security.authorization.SecuredAuthorizationManager; import nextstep.security.config.DefaultSecurityFilterChain; import nextstep.security.config.DelegatingFilterProxy; import nextstep.security.config.FilterChainProxy; @@ -49,15 +50,6 @@ public FilterChainProxy filterChainProxy(List securityFilte return new FilterChainProxy(securityFilterChains); } - @Bean - public SecuredMethodInterceptor securedMethodInterceptor() { - return new SecuredMethodInterceptor(); - } -// @Bean -// public SecuredAspect securedAspect() { -// return new SecuredAspect(); -// } - @Bean public SecurityFilterChain securityFilterChain() { return new DefaultSecurityFilterChain( @@ -74,6 +66,15 @@ public SecurityFilterChain securityFilterChain() { ); } + @Bean + public AuthorizationManagerBeforeMethodInterceptor authorizationManagerBeforeMethodInterceptor() { + return new AuthorizationManagerBeforeMethodInterceptor(securedAuthorizationManager()); + } + + private SecuredAuthorizationManager securedAuthorizationManager() { + return new SecuredAuthorizationManager(new AuthorityAuthorizationManager<>(Set.of("ADMIN"))); + } + @Bean public UserDetailsService userDetailsService() { return username -> { diff --git a/src/main/java/nextstep/security/authorization/AuthorizationManagerBeforeMethodInterceptor.java b/src/main/java/nextstep/security/authorization/AuthorizationManagerBeforeMethodInterceptor.java new file mode 100644 index 0000000..e7b9131 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AuthorizationManagerBeforeMethodInterceptor.java @@ -0,0 +1,42 @@ +package nextstep.security.authorization; + +import nextstep.security.authentication.Authentication; +import nextstep.security.context.SecurityContextHolder; +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.Pointcut; +import org.springframework.aop.PointcutAdvisor; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; + +public class AuthorizationManagerBeforeMethodInterceptor implements MethodInterceptor, PointcutAdvisor { + + private final AuthorizationManager authorizationManager; + + public AuthorizationManagerBeforeMethodInterceptor(AuthorizationManager authorizationManager) { + this.authorizationManager = authorizationManager; + } + + @Override + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + AuthorizationDecision decision = authorizationManager.check(authentication, methodInvocation); + + if (decision != null && !decision.isGranted()) { + throw new ForbiddenException(); + } + + return methodInvocation.proceed(); + } + + @Override + public Pointcut getPointcut() { + return new AnnotationMatchingPointcut(null, Secured.class); + } + + @Override + public Advice getAdvice() { + return this; + } +} diff --git a/src/main/java/nextstep/security/authorization/SecuredAuthorizationManager.java b/src/main/java/nextstep/security/authorization/SecuredAuthorizationManager.java new file mode 100644 index 0000000..76c6cd7 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/SecuredAuthorizationManager.java @@ -0,0 +1,48 @@ +package nextstep.security.authorization; + +import nextstep.security.authentication.Authentication; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.support.AopUtils; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +public class SecuredAuthorizationManager implements AuthorizationManager { + + private final AuthorityAuthorizationManager> authoritiesAuthorizationManager; + + public SecuredAuthorizationManager(AuthorityAuthorizationManager> authoritiesAuthorizationManager) { + this.authoritiesAuthorizationManager = authoritiesAuthorizationManager; + } + + @Override + public AuthorizationDecision check(Authentication authentication, MethodInvocation methodInvocation) { + + Set authorities = getAuthorities(methodInvocation); + + return authoritiesAuthorizationManager.check(authentication, authorities); + } + + + private Set getAuthorities(MethodInvocation methodInvocation) { + + Method method = methodInvocation.getMethod(); + Object target = methodInvocation.getThis(); + Class targetClass = target != null ? target.getClass() : null; + + return resolveAuthorities(method, targetClass); + } + + private Set resolveAuthorities(Method method, Class targetClass) { + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + Secured secured = findSecuredAnnotation(specificMethod); + return secured != null ? Set.of(secured.value()) : Collections.emptySet(); + } + + private Secured findSecuredAnnotation(Method method) { + return method.getAnnotation(Secured.class); + } +} From 9d4ba9657e41177aa6073ac4e945e912fa9178e1 Mon Sep 17 00:00:00 2001 From: yeongunheo Date: Fri, 14 Feb 2025 15:29:59 +0900 Subject: [PATCH 07/10] =?UTF-8?q?refactor:=20RequestAuthorizationContext?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RequestAuthorizationContext.java | 29 ------------------- ...MatcherDelegatingAuthorizationManager.java | 10 +++---- 2 files changed, 5 insertions(+), 34 deletions(-) delete mode 100644 src/main/java/nextstep/security/authorization/RequestAuthorizationContext.java diff --git a/src/main/java/nextstep/security/authorization/RequestAuthorizationContext.java b/src/main/java/nextstep/security/authorization/RequestAuthorizationContext.java deleted file mode 100644 index 355964f..0000000 --- a/src/main/java/nextstep/security/authorization/RequestAuthorizationContext.java +++ /dev/null @@ -1,29 +0,0 @@ -package nextstep.security.authorization; - -import jakarta.servlet.http.HttpServletRequest; - -import java.util.Collections; -import java.util.Map; - -public class RequestAuthorizationContext { - - private final HttpServletRequest request; - private final Map variables; - - public RequestAuthorizationContext(HttpServletRequest request) { - this(request, Collections.emptyMap()); - } - - public RequestAuthorizationContext(HttpServletRequest request, Map variables) { - this.request = request; - this.variables = variables; - } - - public HttpServletRequest getRequest() { - return this.request; - } - - public Map getVariables() { - return this.variables; - } -} diff --git a/src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java b/src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java index c0f740a..2f12f64 100644 --- a/src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java +++ b/src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java @@ -9,22 +9,22 @@ public class RequestMatcherDelegatingAuthorizationManager implements AuthorizationManager { - private final List>> mappings; + private final List> mappings; - public RequestMatcherDelegatingAuthorizationManager(List>> mappings) { + public RequestMatcherDelegatingAuthorizationManager(List> mappings) { this.mappings = mappings; } @Override public AuthorizationDecision check(Authentication authentication, HttpServletRequest request) { - for (RequestMatcherEntry> mapping : this.mappings) { + for (RequestMatcherEntry mapping : this.mappings) { RequestMatcher matcher = mapping.getRequestMatcher(); RequestMatcher.MatchResult matchResult = matcher.matcher(request); if (matchResult.isMatch()) { - AuthorizationManager manager = mapping.getEntry(); + AuthorizationManager manager = mapping.getEntry(); - return manager.check(authentication, new RequestAuthorizationContext(request, matchResult.getVariables())); + return manager.check(authentication, request); } } From bfd71a40f067a5452c096c9421b341df47909176 Mon Sep 17 00:00:00 2001 From: yeongunheo Date: Fri, 14 Feb 2025 15:36:20 +0900 Subject: [PATCH 08/10] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B9=A8=EC=A7=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/nextstep/app/BasicAuthTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/nextstep/app/BasicAuthTest.java b/src/test/java/nextstep/app/BasicAuthTest.java index 2dea9bb..c201f69 100644 --- a/src/test/java/nextstep/app/BasicAuthTest.java +++ b/src/test/java/nextstep/app/BasicAuthTest.java @@ -111,6 +111,6 @@ void request_fail_members_me() throws Exception { .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); - response.andExpect(status().isUnauthorized()); + response.andExpect(status().isForbidden()); } } From 1206724d4521406e499e50e3b284794208540c41 Mon Sep 17 00:00:00 2001 From: yeongunheo Date: Fri, 14 Feb 2025 15:45:02 +0900 Subject: [PATCH 09/10] =?UTF-8?q?refactor:=20DefaultAuthorizationStrategy?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=AA=85=EC=9D=84=20Authenticate?= =?UTF-8?q?dAuthorizationStrategy=EB=A1=9C=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 | 4 ++-- ...nStrategy.java => AuthenticatedAuthorizationStrategy.java} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/main/java/nextstep/security/authorization/{DefaultAuthorizationStrategy.java => AuthenticatedAuthorizationStrategy.java} (78%) diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 284cca0..3a8135b 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -10,7 +10,7 @@ import nextstep.security.authorization.AuthorityAuthorizationManager; import nextstep.security.authorization.AuthorizationFilter; import nextstep.security.authorization.AuthorizationManagerBeforeMethodInterceptor; -import nextstep.security.authorization.DefaultAuthorizationStrategy; +import nextstep.security.authorization.AuthenticatedAuthorizationStrategy; import nextstep.security.authorization.RequestMatcherDelegatingAuthorizationManager; import nextstep.security.authorization.SecuredAuthorizationManager; import nextstep.security.config.DefaultSecurityFilterChain; @@ -59,7 +59,7 @@ public SecurityFilterChain securityFilterChain() { new BasicAuthenticationFilter(userDetailsService()), new AuthorizationFilter(new RequestMatcherDelegatingAuthorizationManager(List.of( new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members"), new AuthorityAuthorizationManager<>(Set.of("ADMIN"))), - new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members/me"), new AuthenticatedAuthorizationManager<>(new DefaultAuthorizationStrategy())), + new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members/me"), new AuthenticatedAuthorizationManager<>(new AuthenticatedAuthorizationStrategy())), new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/search"), new AuthenticatedAuthorizationManager<>(new AnonymousAuthorizationStrategy())) ))) ) diff --git a/src/main/java/nextstep/security/authorization/DefaultAuthorizationStrategy.java b/src/main/java/nextstep/security/authorization/AuthenticatedAuthorizationStrategy.java similarity index 78% rename from src/main/java/nextstep/security/authorization/DefaultAuthorizationStrategy.java rename to src/main/java/nextstep/security/authorization/AuthenticatedAuthorizationStrategy.java index b7b47cf..a6d800d 100644 --- a/src/main/java/nextstep/security/authorization/DefaultAuthorizationStrategy.java +++ b/src/main/java/nextstep/security/authorization/AuthenticatedAuthorizationStrategy.java @@ -2,7 +2,7 @@ import nextstep.security.authentication.Authentication; -public class DefaultAuthorizationStrategy implements AuthorizationStrategy { +public class AuthenticatedAuthorizationStrategy implements AuthorizationStrategy { @Override public boolean isGranted(Authentication authentication) { From 8fefdc412cef53a1fb837f945f489a1c081dc60a Mon Sep 17 00:00:00 2001 From: yeongunheo Date: Fri, 14 Feb 2025 15:53:18 +0900 Subject: [PATCH 10/10] =?UTF-8?q?refactor:=20AnonymousAuthorizationStrateg?= =?UTF-8?q?y=20=EC=82=AD=EC=A0=9C=20=ED=9B=84=20PermitAllAuthorizationMana?= =?UTF-8?q?ger=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/app/SecurityConfig.java | 8 +++++--- .../AnonymousAuthorizationStrategy.java | 11 ----------- .../PermitAllAuthorizationManager.java | 11 +++++++++++ .../nextstep/security/util/AnyRequestMatcher.java | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 14 deletions(-) delete mode 100644 src/main/java/nextstep/security/authorization/AnonymousAuthorizationStrategy.java create mode 100644 src/main/java/nextstep/security/authorization/PermitAllAuthorizationManager.java create mode 100644 src/main/java/nextstep/security/util/AnyRequestMatcher.java diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 3a8135b..528a476 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -5,12 +5,12 @@ import nextstep.security.authentication.AuthenticationException; import nextstep.security.authentication.BasicAuthenticationFilter; import nextstep.security.authentication.UsernamePasswordAuthenticationFilter; -import nextstep.security.authorization.AnonymousAuthorizationStrategy; import nextstep.security.authorization.AuthenticatedAuthorizationManager; import nextstep.security.authorization.AuthorityAuthorizationManager; import nextstep.security.authorization.AuthorizationFilter; import nextstep.security.authorization.AuthorizationManagerBeforeMethodInterceptor; import nextstep.security.authorization.AuthenticatedAuthorizationStrategy; +import nextstep.security.authorization.PermitAllAuthorizationManager; import nextstep.security.authorization.RequestMatcherDelegatingAuthorizationManager; import nextstep.security.authorization.SecuredAuthorizationManager; import nextstep.security.config.DefaultSecurityFilterChain; @@ -20,6 +20,7 @@ import nextstep.security.context.SecurityContextHolderFilter; import nextstep.security.userdetails.UserDetails; import nextstep.security.userdetails.UserDetailsService; +import nextstep.security.util.AnyRequestMatcher; import nextstep.security.util.MvcRequestMatcher; import nextstep.security.util.RequestMatcherEntry; import org.springframework.context.annotation.Bean; @@ -58,9 +59,10 @@ public SecurityFilterChain securityFilterChain() { new UsernamePasswordAuthenticationFilter(userDetailsService()), new BasicAuthenticationFilter(userDetailsService()), new AuthorizationFilter(new RequestMatcherDelegatingAuthorizationManager(List.of( - new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members"), new AuthorityAuthorizationManager<>(Set.of("ADMIN"))), new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members/me"), new AuthenticatedAuthorizationManager<>(new AuthenticatedAuthorizationStrategy())), - new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/search"), new AuthenticatedAuthorizationManager<>(new AnonymousAuthorizationStrategy())) + new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members"), new AuthorityAuthorizationManager<>(Set.of("ADMIN"))), + new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/search"), new PermitAllAuthorizationManager<>()), + new RequestMatcherEntry<>(new AnyRequestMatcher(), new PermitAllAuthorizationManager()) ))) ) ); diff --git a/src/main/java/nextstep/security/authorization/AnonymousAuthorizationStrategy.java b/src/main/java/nextstep/security/authorization/AnonymousAuthorizationStrategy.java deleted file mode 100644 index 0633bcd..0000000 --- a/src/main/java/nextstep/security/authorization/AnonymousAuthorizationStrategy.java +++ /dev/null @@ -1,11 +0,0 @@ -package nextstep.security.authorization; - -import nextstep.security.authentication.Authentication; - -public class AnonymousAuthorizationStrategy implements AuthorizationStrategy { - - @Override - public boolean isGranted(Authentication authentication) { - return true; - } -} diff --git a/src/main/java/nextstep/security/authorization/PermitAllAuthorizationManager.java b/src/main/java/nextstep/security/authorization/PermitAllAuthorizationManager.java new file mode 100644 index 0000000..fa95912 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/PermitAllAuthorizationManager.java @@ -0,0 +1,11 @@ +package nextstep.security.authorization; + +import nextstep.security.authentication.Authentication; + +public class PermitAllAuthorizationManager implements AuthorizationManager { + + @Override + public AuthorizationDecision check(Authentication authentication, T object) { + return new AuthorizationDecision(true); + } +} diff --git a/src/main/java/nextstep/security/util/AnyRequestMatcher.java b/src/main/java/nextstep/security/util/AnyRequestMatcher.java new file mode 100644 index 0000000..0be0669 --- /dev/null +++ b/src/main/java/nextstep/security/util/AnyRequestMatcher.java @@ -0,0 +1,14 @@ +package nextstep.security.util; + +import jakarta.servlet.http.HttpServletRequest; + +public class AnyRequestMatcher implements RequestMatcher { + + public AnyRequestMatcher() { + } + + @Override + public boolean matches(HttpServletRequest request) { + return true; + } +}