From 9db8150f450bdcace90d010be4872a364d77a174 Mon Sep 17 00:00:00 2001 From: JoJo Date: Thu, 8 Dec 2022 13:12:21 +0900 Subject: [PATCH 1/6] feat: add AuthorizeRequestMatcherRegistry --- .../access/matcher/MvcRequestMatcher.java | 2 +- .../AuthorizeRequestMatcherRegistry.java | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/main/java/nextstep/security/config/AuthorizeRequestMatcherRegistry.java diff --git a/src/main/java/nextstep/security/access/matcher/MvcRequestMatcher.java b/src/main/java/nextstep/security/access/matcher/MvcRequestMatcher.java index abfb3d4..eca7322 100644 --- a/src/main/java/nextstep/security/access/matcher/MvcRequestMatcher.java +++ b/src/main/java/nextstep/security/access/matcher/MvcRequestMatcher.java @@ -28,6 +28,6 @@ public boolean matches(HttpServletRequest request) { return false; } - return request.getRequestURI().contains(pattern); + return request.getRequestURI().equals(pattern); } } diff --git a/src/main/java/nextstep/security/config/AuthorizeRequestMatcherRegistry.java b/src/main/java/nextstep/security/config/AuthorizeRequestMatcherRegistry.java new file mode 100644 index 0000000..ff5f505 --- /dev/null +++ b/src/main/java/nextstep/security/config/AuthorizeRequestMatcherRegistry.java @@ -0,0 +1,62 @@ +package nextstep.security.config; + +import nextstep.security.access.matcher.RequestMatcher; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +public class AuthorizeRequestMatcherRegistry { + private final Map mappings = new HashMap<>(); + + public AuthorizedUrl matcher(RequestMatcher requestMatcher) { + return new AuthorizedUrl(requestMatcher); + } + + AuthorizeRequestMatcherRegistry addMapping(RequestMatcher requestMatcher, String attributes) { + mappings.put(requestMatcher, attributes); + return this; + } + + public String getAttribute(HttpServletRequest request) { + for (Map.Entry entry : mappings.entrySet()) { + if (entry.getKey().matches(request)) { + return entry.getValue(); + } + } + + return null; + } + + public class AuthorizedUrl { + public static final String PERMIT_ALL = "permitAll"; + public static final String DENY_ALL = "denyAll"; + public static final String AUTHENTICATED = "authenticated"; + private final RequestMatcher requestMatcher; + + public AuthorizedUrl(RequestMatcher requestMatcher) { + this.requestMatcher = requestMatcher; + } + + public AuthorizeRequestMatcherRegistry permitAll() { + return access(PERMIT_ALL); + } + + public AuthorizeRequestMatcherRegistry denyAll() { + return access(DENY_ALL); + } + + public AuthorizeRequestMatcherRegistry hasAuthority(String authority) { + return access("hasAuthority(" + authority + ")"); + } + + public AuthorizeRequestMatcherRegistry authenticated() { + return access(AUTHENTICATED); + } + + private AuthorizeRequestMatcherRegistry access(String attribute) { + return addMapping(requestMatcher, attribute); + } + } + +} From beac23b4f25797fe8e942bb31e9fc713d24a0a16 Mon Sep 17 00:00:00 2001 From: "jaylin.re" Date: Sat, 10 Dec 2022 16:32:18 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20MemberTest=20=EB=8F=99=EC=9E=91?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20basic=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=8B=9C=EC=A0=90=EC=97=90=20session=EA=B0=92=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 --- src/test/java/nextstep/app/MemberTest.java | 30 +++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/test/java/nextstep/app/MemberTest.java b/src/test/java/nextstep/app/MemberTest.java index 1961fb4..bf72a52 100644 --- a/src/test/java/nextstep/app/MemberTest.java +++ b/src/test/java/nextstep/app/MemberTest.java @@ -1,6 +1,8 @@ package nextstep.app; import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.UsernamePasswordAuthentication; +import nextstep.security.context.SecurityContext; import nextstep.security.context.SecurityContextHolder; import nextstep.app.domain.Member; import nextstep.app.infrastructure.InmemoryMemberRepository; @@ -9,11 +11,14 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpSession; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import java.util.Base64; +import java.util.Collections; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -30,7 +35,9 @@ public class MemberTest { @Test void request_success_with_admin_user() throws Exception { - ResultActions response = requestWithBasicAuth(TEST_ADMIN_MEMBER.getEmail(), TEST_ADMIN_MEMBER.getPassword()); + ResultActions response = requestWithBasicAuthAndSession( + TEST_ADMIN_MEMBER.getEmail(), TEST_ADMIN_MEMBER.getPassword(), TEST_ADMIN_MEMBER.getRoles() + ); response.andExpect(status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(2)); @@ -42,9 +49,11 @@ void request_success_with_admin_user() throws Exception { @Test void request_fail_with_general_user() throws Exception { - ResultActions response = requestWithBasicAuth(TEST_USER_MEMBER.getEmail(), TEST_USER_MEMBER.getPassword()); + ResultActions response = requestWithBasicAuthAndSession( + TEST_USER_MEMBER.getEmail(), TEST_USER_MEMBER.getPassword(), TEST_USER_MEMBER.getRoles() + ); - response.andExpect(status().isForbidden()); + response.andExpect(status().isForbidden()); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); assertThat(authentication.isAuthenticated()).isTrue(); @@ -53,24 +62,33 @@ void request_fail_with_general_user() throws Exception { @Test void request_fail_with_no_user() throws Exception { - ResultActions response = requestWithBasicAuth("none", "none"); + ResultActions response = requestWithBasicAuthAndSession("none", "none", Collections.emptySet()); response.andExpect(status().isUnauthorized()); } @Test void request_fail_invalid_password() throws Exception { - ResultActions response = requestWithBasicAuth(TEST_ADMIN_MEMBER.getEmail(), "invalid"); + ResultActions response = requestWithBasicAuthAndSession(TEST_ADMIN_MEMBER.getEmail(), "invalid", Collections.emptySet()); response.andExpect(status().isUnauthorized()); } - private ResultActions requestWithBasicAuth(String username, String password) throws Exception { + private ResultActions requestWithBasicAuthAndSession(String username, String password, Set roles) throws Exception { String token = Base64.getEncoder().encodeToString((username + ":" + password).getBytes()); return mockMvc.perform(get("/members") .header("Authorization", "Basic " + token) + .session(getLoginSession(username, password, roles)) .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); } + + private MockHttpSession getLoginSession(String username, String password, Set roles) { + MockHttpSession session = new MockHttpSession(); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(UsernamePasswordAuthentication.ofAuthenticated(username, password, roles)); + session.setAttribute("SPRING_SECURITY_CONTEXT", context); + return session; + } } From 96248b5703eda0baa8b77eeaefe113dd7d4bc4e8 Mon Sep 17 00:00:00 2001 From: "jaylin.re" Date: Sun, 11 Dec 2022 15:06:00 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=EB=90=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90(=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90)=EB=A5=BC=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=ED=95=98=EB=8A=94=20=ED=95=84=ED=84=B0=EB=AA=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/app/config/AuthConfig.java | 4 ++-- ...thorizationFilter.java => SessionAuthorizationFilter.java} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/main/java/nextstep/security/authorization/{LoginAuthorizationFilter.java => SessionAuthorizationFilter.java} (93%) diff --git a/src/main/java/nextstep/app/config/AuthConfig.java b/src/main/java/nextstep/app/config/AuthConfig.java index e59870d..37417b3 100644 --- a/src/main/java/nextstep/app/config/AuthConfig.java +++ b/src/main/java/nextstep/app/config/AuthConfig.java @@ -2,7 +2,7 @@ import nextstep.security.access.matcher.MvcRequestMatcher; import nextstep.security.authentication.*; -import nextstep.security.authorization.LoginAuthorizationFilter; +import nextstep.security.authorization.SessionAuthorizationFilter; import nextstep.security.authorization.RoleAuthorizationFilter; import nextstep.security.config.DefaultSecurityFilterChain; import nextstep.security.config.FilterChainProxy; @@ -50,7 +50,7 @@ public SecurityFilterChain loginSecurityFilterChain() { public SecurityFilterChain membersSecurityFilterChain() { List filters = new ArrayList<>(); filters.add(new BasicAuthenticationFilter(authenticationManager())); - filters.add(new LoginAuthorizationFilter(securityContextRepository())); + filters.add(new SessionAuthorizationFilter(securityContextRepository())); filters.add(new RoleAuthorizationFilter()); return new DefaultSecurityFilterChain(new MvcRequestMatcher(HttpMethod.GET, "/members"), filters); } diff --git a/src/main/java/nextstep/security/authorization/LoginAuthorizationFilter.java b/src/main/java/nextstep/security/authorization/SessionAuthorizationFilter.java similarity index 93% rename from src/main/java/nextstep/security/authorization/LoginAuthorizationFilter.java rename to src/main/java/nextstep/security/authorization/SessionAuthorizationFilter.java index a5ad5e0..71d75e6 100644 --- a/src/main/java/nextstep/security/authorization/LoginAuthorizationFilter.java +++ b/src/main/java/nextstep/security/authorization/SessionAuthorizationFilter.java @@ -17,13 +17,13 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; -public class LoginAuthorizationFilter extends GenericFilterBean { +public class SessionAuthorizationFilter extends GenericFilterBean { private static final MvcRequestMatcher DEFAULT_REQUEST_MATCHER = new MvcRequestMatcher(HttpMethod.GET, "/members"); private final SecurityContextRepository securityContextRepository; - public LoginAuthorizationFilter(SecurityContextRepository securityContextRepository) { + public SessionAuthorizationFilter(SecurityContextRepository securityContextRepository) { this.securityContextRepository = securityContextRepository; } From 1362e11faf602999c54426dbc6522529634bb611 Mon Sep 17 00:00:00 2001 From: "jaylin.re" Date: Sun, 11 Dec 2022 17:08:27 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=9D=84=20=EB=82=B4=EB=A0=A4=EC=A4=80?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /members/me 요청 추가 및 SecurityFilterChain에 해당 요청에 대한 필터들 추가 - 테스트 추가 --- .../java/nextstep/app/config/AuthConfig.java | 10 ++- .../exception/MemberNotFoundException.java | 4 + .../nextstep/app/ui/MemberController.java | 8 ++ .../SessionAuthorizationFilter.java | 9 --- .../nextstep/app/MemberAcceptanceTest.java | 74 ++++++++++++++++--- src/test/java/nextstep/app/MemberTest.java | 50 ++++++++++--- 6 files changed, 124 insertions(+), 31 deletions(-) create mode 100644 src/main/java/nextstep/app/exception/MemberNotFoundException.java diff --git a/src/main/java/nextstep/app/config/AuthConfig.java b/src/main/java/nextstep/app/config/AuthConfig.java index 37417b3..ea50e6c 100644 --- a/src/main/java/nextstep/app/config/AuthConfig.java +++ b/src/main/java/nextstep/app/config/AuthConfig.java @@ -36,7 +36,7 @@ public DelegatingFilterProxy securityFilterChainProxy() { @Bean public FilterChainProxy filterChainProxy() { - return new FilterChainProxy(List.of(loginSecurityFilterChain(), membersSecurityFilterChain())); + return new FilterChainProxy(List.of(loginSecurityFilterChain(), membersSecurityFilterChain(), sessionSecurityFilterChain())); } @Bean @@ -55,6 +55,14 @@ public SecurityFilterChain membersSecurityFilterChain() { return new DefaultSecurityFilterChain(new MvcRequestMatcher(HttpMethod.GET, "/members"), filters); } + @Bean + public SecurityFilterChain sessionSecurityFilterChain() { + List filters = new ArrayList<>(); + filters.add(new BasicAuthenticationFilter(authenticationManager())); + filters.add(new SessionAuthorizationFilter(securityContextRepository())); + return new DefaultSecurityFilterChain(new MvcRequestMatcher(HttpMethod.GET, "/members/me"), filters); + } + @Bean public SecurityContextRepository securityContextRepository() { return new HttpSessionSecurityContextRepository(); diff --git a/src/main/java/nextstep/app/exception/MemberNotFoundException.java b/src/main/java/nextstep/app/exception/MemberNotFoundException.java new file mode 100644 index 0000000..874b3c9 --- /dev/null +++ b/src/main/java/nextstep/app/exception/MemberNotFoundException.java @@ -0,0 +1,4 @@ +package nextstep.app.exception; + +public class MemberNotFoundException extends RuntimeException { +} diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index fce08d7..fb48c05 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import javax.servlet.http.HttpServletRequest; import java.util.List; @RestController @@ -25,4 +26,11 @@ public ResponseEntity> list() { return ResponseEntity.ok(members); } + @GetMapping("/members/me") + public ResponseEntity me(HttpServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = authentication.getPrincipal().toString(); + Member member = memberRepository.findByEmail(email).orElseThrow(); + return ResponseEntity.ok(member); + } } diff --git a/src/main/java/nextstep/security/authorization/SessionAuthorizationFilter.java b/src/main/java/nextstep/security/authorization/SessionAuthorizationFilter.java index 71d75e6..fcc671e 100644 --- a/src/main/java/nextstep/security/authorization/SessionAuthorizationFilter.java +++ b/src/main/java/nextstep/security/authorization/SessionAuthorizationFilter.java @@ -1,11 +1,9 @@ package nextstep.security.authorization; -import nextstep.security.access.matcher.MvcRequestMatcher; import nextstep.security.context.SecurityContext; import nextstep.security.context.SecurityContextHolder; import nextstep.security.context.SecurityContextRepository; import nextstep.security.exception.AuthenticationException; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.web.filter.GenericFilterBean; @@ -18,8 +16,6 @@ import java.io.IOException; public class SessionAuthorizationFilter extends GenericFilterBean { - private static final MvcRequestMatcher DEFAULT_REQUEST_MATCHER = new MvcRequestMatcher(HttpMethod.GET, - "/members"); private final SecurityContextRepository securityContextRepository; @@ -30,11 +26,6 @@ public SessionAuthorizationFilter(SecurityContextRepository securityContextRepos @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { - if (!DEFAULT_REQUEST_MATCHER.matches((HttpServletRequest) request)) { - chain.doFilter(request, response); - return; - } - SecurityContext loadedContext = securityContextRepository.loadContext((HttpServletRequest) request); if (loadedContext == null) { ((HttpServletResponse) response).sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase()); diff --git a/src/test/java/nextstep/app/MemberAcceptanceTest.java b/src/test/java/nextstep/app/MemberAcceptanceTest.java index dc861af..617cef0 100644 --- a/src/test/java/nextstep/app/MemberAcceptanceTest.java +++ b/src/test/java/nextstep/app/MemberAcceptanceTest.java @@ -24,36 +24,86 @@ void get_members_after_form_login() { params.put("username", TEST_MEMBER.getEmail()); params.put("password", TEST_MEMBER.getPassword()); - ExtractableResponse loginResponse = RestAssured.given().log().all() - .formParams(params) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - .when() - .post("/login") - .then().log().all() - .extract(); + ExtractableResponse loginResponse = requestLogin(params); + + ExtractableResponse memberResponse = requestMembers(loginResponse); + assertThat(memberResponse.statusCode()).isEqualTo(HttpStatus.OK.value()); + List members = memberResponse.jsonPath().getList(".", Member.class); + assertThat(members.size()).isEqualTo(2); + } + + @Test + void get_members_before_form_login() { ExtractableResponse memberResponse = RestAssured.given().log().all() - .cookies(loginResponse.cookies()) .contentType(MediaType.APPLICATION_JSON_VALUE) .when() .get("/members") .then().log().all() .extract(); + assertThat(memberResponse.statusCode()).isEqualTo(HttpStatus.FORBIDDEN.value()); + } + + @Test + void get_members_me_after_form_login() { + Map params = new HashMap<>(); + params.put("username", TEST_MEMBER.getEmail()); + params.put("password", TEST_MEMBER.getPassword()); + + ExtractableResponse loginResponse = requestLogin(params); + + ExtractableResponse memberResponse = requestMembersMe(loginResponse); + assertThat(memberResponse.statusCode()).isEqualTo(HttpStatus.OK.value()); - List members = memberResponse.jsonPath().getList(".", Member.class); - assertThat(members.size()).isEqualTo(2); + assertThat(memberResponse.jsonPath().getString("email")).isEqualTo(TEST_MEMBER.getEmail()); } @Test - void get_members_before_form_login() { + void get_members_me_before_form_login() { + ExtractableResponse memberResponse = requestMembersMe(null); + + assertThat(memberResponse.statusCode()).isEqualTo(HttpStatus.FORBIDDEN.value()); + } + + private static ExtractableResponse requestLogin(Map params) { + ExtractableResponse loginResponse = RestAssured.given().log().all() + .formParams(params) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .when() + .post("/login") + .then().log().all() + .extract(); + return loginResponse; + } + + private static ExtractableResponse requestMembers(ExtractableResponse loginResponse) { ExtractableResponse memberResponse = RestAssured.given().log().all() + .cookies(loginResponse.cookies()) .contentType(MediaType.APPLICATION_JSON_VALUE) .when() .get("/members") .then().log().all() .extract(); + return memberResponse; + } - assertThat(memberResponse.statusCode()).isEqualTo(HttpStatus.FORBIDDEN.value()); + private static ExtractableResponse requestMembersMe(ExtractableResponse loginResponse) { + if (loginResponse == null) { + return RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when() + .get("/members/me") + .then().log().all() + .extract(); + } else { + return RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .cookies(loginResponse.cookies()) + .when() + .get("/members/me") + .then().log().all() + .extract(); + } } } diff --git a/src/test/java/nextstep/app/MemberTest.java b/src/test/java/nextstep/app/MemberTest.java index bf72a52..3e1941f 100644 --- a/src/test/java/nextstep/app/MemberTest.java +++ b/src/test/java/nextstep/app/MemberTest.java @@ -34,8 +34,8 @@ public class MemberTest { private MockMvc mockMvc; @Test - void request_success_with_admin_user() throws Exception { - ResultActions response = requestWithBasicAuthAndSession( + void members_request_success_with_admin_user() throws Exception { + ResultActions response = requestMembersWithBasicAuthAndSession( TEST_ADMIN_MEMBER.getEmail(), TEST_ADMIN_MEMBER.getPassword(), TEST_ADMIN_MEMBER.getRoles() ); @@ -48,8 +48,8 @@ void request_success_with_admin_user() throws Exception { } @Test - void request_fail_with_general_user() throws Exception { - ResultActions response = requestWithBasicAuthAndSession( + void members_request_fail_with_general_user() throws Exception { + ResultActions response = requestMembersWithBasicAuthAndSession( TEST_USER_MEMBER.getEmail(), TEST_USER_MEMBER.getPassword(), TEST_USER_MEMBER.getRoles() ); @@ -61,20 +61,42 @@ void request_fail_with_general_user() throws Exception { } @Test - void request_fail_with_no_user() throws Exception { - ResultActions response = requestWithBasicAuthAndSession("none", "none", Collections.emptySet()); + void members_request_fail_with_no_user() throws Exception { + ResultActions response = requestMembersWithBasicAuthAndSession("none", "none", Collections.emptySet()); response.andExpect(status().isUnauthorized()); } @Test - void request_fail_invalid_password() throws Exception { - ResultActions response = requestWithBasicAuthAndSession(TEST_ADMIN_MEMBER.getEmail(), "invalid", Collections.emptySet()); + void members_request_fail_invalid_password() throws Exception { + ResultActions response = requestMembersWithBasicAuthAndSession(TEST_ADMIN_MEMBER.getEmail(), "invalid", Collections.emptySet()); response.andExpect(status().isUnauthorized()); } - private ResultActions requestWithBasicAuthAndSession(String username, String password, Set roles) throws Exception { + @Test + void members_me_request_success() throws Exception { + ResultActions response = requestMembersMeWithBasicAuthAndSession( + TEST_USER_MEMBER.getEmail(), TEST_ADMIN_MEMBER.getPassword(), TEST_ADMIN_MEMBER.getRoles() + ); + + response.andExpect(status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.email").value(TEST_USER_MEMBER.getEmail())); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + assertThat(authentication.isAuthenticated()).isTrue(); + } + + @Test + void members_me_request_fail() throws Exception { + ResultActions response = requestMembersMeWithBasicAuthAndSession( + TEST_USER_MEMBER.getEmail(), "invalid", Collections.emptySet() + ); + + response.andExpect(status().isUnauthorized()); + } + + private ResultActions requestMembersWithBasicAuthAndSession(String username, String password, Set roles) throws Exception { String token = Base64.getEncoder().encodeToString((username + ":" + password).getBytes()); return mockMvc.perform(get("/members") @@ -84,6 +106,16 @@ private ResultActions requestWithBasicAuthAndSession(String username, String pas ); } + private ResultActions requestMembersMeWithBasicAuthAndSession(String username, String password, Set roles) throws Exception { + String token = Base64.getEncoder().encodeToString((username + ":" + password).getBytes()); + + return mockMvc.perform(get("/members/me") + .header("Authorization", "Basic " + token) + .session(getLoginSession(username, password, roles)) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ); + } + private MockHttpSession getLoginSession(String username, String password, Set roles) { MockHttpSession session = new MockHttpSession(); SecurityContext context = SecurityContextHolder.createEmptyContext(); From db24c17d82ece92e03e85f9d4f16734509e46533 Mon Sep 17 00:00:00 2001 From: "jaylin.re" Date: Mon, 12 Dec 2022 01:57:23 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=EC=9A=94=EC=B2=AD=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EB=8B=A4=EB=A5=B8=20=EC=9D=B8=EA=B0=80=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthorizeRequestMatcherRegistry에 request별 matcher 등록 - 각 인가 필터에서 AuthorizeRequestMatcherRegistry에 등록된 attribute 체크 --- .../java/nextstep/app/config/AuthConfig.java | 25 ++++++++++--------- .../nextstep/app/ui/MemberController.java | 3 +-- .../access/matcher/MvcRequestMatcher.java | 2 +- .../RoleAuthorizationFilter.java | 21 ++++++++-------- .../SessionAuthorizationFilter.java | 16 +++++++++--- .../AuthorizeRequestMatcherRegistry.java | 22 +++++++++++++++- src/test/java/nextstep/app/MemberTest.java | 2 +- 7 files changed, 59 insertions(+), 32 deletions(-) diff --git a/src/main/java/nextstep/app/config/AuthConfig.java b/src/main/java/nextstep/app/config/AuthConfig.java index ea50e6c..c130393 100644 --- a/src/main/java/nextstep/app/config/AuthConfig.java +++ b/src/main/java/nextstep/app/config/AuthConfig.java @@ -4,6 +4,7 @@ import nextstep.security.authentication.*; import nextstep.security.authorization.SessionAuthorizationFilter; import nextstep.security.authorization.RoleAuthorizationFilter; +import nextstep.security.config.AuthorizeRequestMatcherRegistry; import nextstep.security.config.DefaultSecurityFilterChain; import nextstep.security.config.FilterChainProxy; import nextstep.security.config.SecurityFilterChain; @@ -29,6 +30,15 @@ public AuthConfig(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } + @Bean + public AuthorizeRequestMatcherRegistry requestMatcherRegistryMapping() { + AuthorizeRequestMatcherRegistry requestMatcherRegistry = new AuthorizeRequestMatcherRegistry(); + requestMatcherRegistry + .matcher(new MvcRequestMatcher(HttpMethod.GET, "/members")).hasAuthority("ADMIN") + .matcher(new MvcRequestMatcher(HttpMethod.GET, "/members/me")).authenticated(); + return requestMatcherRegistry; + } + @Bean public DelegatingFilterProxy securityFilterChainProxy() { return new DelegatingFilterProxy("filterChainProxy"); @@ -36,7 +46,7 @@ public DelegatingFilterProxy securityFilterChainProxy() { @Bean public FilterChainProxy filterChainProxy() { - return new FilterChainProxy(List.of(loginSecurityFilterChain(), membersSecurityFilterChain(), sessionSecurityFilterChain())); + return new FilterChainProxy(List.of(loginSecurityFilterChain(), membersSecurityFilterChain())); } @Bean @@ -50,19 +60,11 @@ public SecurityFilterChain loginSecurityFilterChain() { public SecurityFilterChain membersSecurityFilterChain() { List filters = new ArrayList<>(); filters.add(new BasicAuthenticationFilter(authenticationManager())); - filters.add(new SessionAuthorizationFilter(securityContextRepository())); - filters.add(new RoleAuthorizationFilter()); + filters.add(new SessionAuthorizationFilter(securityContextRepository(), requestMatcherRegistryMapping())); + filters.add(new RoleAuthorizationFilter(requestMatcherRegistryMapping())); return new DefaultSecurityFilterChain(new MvcRequestMatcher(HttpMethod.GET, "/members"), filters); } - @Bean - public SecurityFilterChain sessionSecurityFilterChain() { - List filters = new ArrayList<>(); - filters.add(new BasicAuthenticationFilter(authenticationManager())); - filters.add(new SessionAuthorizationFilter(securityContextRepository())); - return new DefaultSecurityFilterChain(new MvcRequestMatcher(HttpMethod.GET, "/members/me"), filters); - } - @Bean public SecurityContextRepository securityContextRepository() { return new HttpSessionSecurityContextRepository(); @@ -72,5 +74,4 @@ public SecurityContextRepository securityContextRepository() { public AuthenticationManager authenticationManager() { return new AuthenticationManager(new UsernamePasswordAuthenticationProvider(userDetailsService)); } - } diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index fb48c05..226b300 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -8,7 +8,6 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; import java.util.List; @RestController @@ -27,7 +26,7 @@ public ResponseEntity> list() { } @GetMapping("/members/me") - public ResponseEntity me(HttpServletRequest request) { + public ResponseEntity me() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String email = authentication.getPrincipal().toString(); Member member = memberRepository.findByEmail(email).orElseThrow(); diff --git a/src/main/java/nextstep/security/access/matcher/MvcRequestMatcher.java b/src/main/java/nextstep/security/access/matcher/MvcRequestMatcher.java index eca7322..0b02326 100644 --- a/src/main/java/nextstep/security/access/matcher/MvcRequestMatcher.java +++ b/src/main/java/nextstep/security/access/matcher/MvcRequestMatcher.java @@ -28,6 +28,6 @@ public boolean matches(HttpServletRequest request) { return false; } - return request.getRequestURI().equals(pattern); + return request.getRequestURI().contains(pattern) || request.getRequestURI().startsWith(pattern); } } diff --git a/src/main/java/nextstep/security/authorization/RoleAuthorizationFilter.java b/src/main/java/nextstep/security/authorization/RoleAuthorizationFilter.java index 42e97e0..1276531 100644 --- a/src/main/java/nextstep/security/authorization/RoleAuthorizationFilter.java +++ b/src/main/java/nextstep/security/authorization/RoleAuthorizationFilter.java @@ -1,10 +1,9 @@ package nextstep.security.authorization; -import nextstep.security.access.matcher.MvcRequestMatcher; import nextstep.security.authentication.Authentication; +import nextstep.security.config.AuthorizeRequestMatcherRegistry; import nextstep.security.context.SecurityContextHolder; import nextstep.security.exception.AuthenticationException; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.web.filter.GenericFilterBean; @@ -17,22 +16,22 @@ import java.io.IOException; public class RoleAuthorizationFilter extends GenericFilterBean { - private static final MvcRequestMatcher DEFAULT_REQUEST_MATCHER = new MvcRequestMatcher(HttpMethod.GET, - "/members"); - private static final String ADMIN_ROLE = "ADMIN"; + private final AuthorizeRequestMatcherRegistry requestMatcherRegistry; + + public RoleAuthorizationFilter(AuthorizeRequestMatcherRegistry requestMatcherRegistry) { + this.requestMatcherRegistry = requestMatcherRegistry; + } + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { - if (!DEFAULT_REQUEST_MATCHER.matches((HttpServletRequest) request)) { - chain.doFilter(request, response); - return; - } - + String attribute = requestMatcherRegistry.getAttribute((HttpServletRequest) request); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + boolean isAuthorized = requestMatcherRegistry.isAuthorized(attribute, authentication); - if (!authentication.getAuthorities().contains(ADMIN_ROLE)) { + if (!isAuthorized) { ((HttpServletResponse) response).sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase()); return; } diff --git a/src/main/java/nextstep/security/authorization/SessionAuthorizationFilter.java b/src/main/java/nextstep/security/authorization/SessionAuthorizationFilter.java index fcc671e..53723e5 100644 --- a/src/main/java/nextstep/security/authorization/SessionAuthorizationFilter.java +++ b/src/main/java/nextstep/security/authorization/SessionAuthorizationFilter.java @@ -1,5 +1,6 @@ package nextstep.security.authorization; +import nextstep.security.config.AuthorizeRequestMatcherRegistry; import nextstep.security.context.SecurityContext; import nextstep.security.context.SecurityContextHolder; import nextstep.security.context.SecurityContextRepository; @@ -19,22 +20,29 @@ public class SessionAuthorizationFilter extends GenericFilterBean { private final SecurityContextRepository securityContextRepository; - public SessionAuthorizationFilter(SecurityContextRepository securityContextRepository) { + private final AuthorizeRequestMatcherRegistry requestMatcherRegistry; + + public SessionAuthorizationFilter(SecurityContextRepository securityContextRepository, AuthorizeRequestMatcherRegistry requestMatcherRegistry) { this.securityContextRepository = securityContextRepository; + this.requestMatcherRegistry = requestMatcherRegistry; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { + String attribute = requestMatcherRegistry.getAttribute((HttpServletRequest) request); SecurityContext loadedContext = securityContextRepository.loadContext((HttpServletRequest) request); if (loadedContext == null) { ((HttpServletResponse) response).sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase()); return; } + boolean isAuthorized = requestMatcherRegistry.isAuthorized(attribute, loadedContext.getAuthentication()); - SecurityContext context = SecurityContextHolder.createEmptyContext(); - context.setAuthentication(loadedContext.getAuthentication()); - SecurityContextHolder.setContext(context); + if (isAuthorized) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(loadedContext.getAuthentication()); + SecurityContextHolder.setContext(context); + } } catch (AuthenticationException e) { ((HttpServletResponse) response).sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); return; diff --git a/src/main/java/nextstep/security/config/AuthorizeRequestMatcherRegistry.java b/src/main/java/nextstep/security/config/AuthorizeRequestMatcherRegistry.java index ff5f505..39c235d 100644 --- a/src/main/java/nextstep/security/config/AuthorizeRequestMatcherRegistry.java +++ b/src/main/java/nextstep/security/config/AuthorizeRequestMatcherRegistry.java @@ -1,10 +1,13 @@ package nextstep.security.config; import nextstep.security.access.matcher.RequestMatcher; +import nextstep.security.authentication.Authentication; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class AuthorizeRequestMatcherRegistry { private final Map mappings = new HashMap<>(); @@ -32,6 +35,8 @@ public class AuthorizedUrl { public static final String PERMIT_ALL = "permitAll"; public static final String DENY_ALL = "denyAll"; public static final String AUTHENTICATED = "authenticated"; + public static final String AUTHORITY = "hasAuthority"; + private final RequestMatcher requestMatcher; public AuthorizedUrl(RequestMatcher requestMatcher) { @@ -47,7 +52,7 @@ public AuthorizeRequestMatcherRegistry denyAll() { } public AuthorizeRequestMatcherRegistry hasAuthority(String authority) { - return access("hasAuthority(" + authority + ")"); + return access(AUTHORITY + "(" + authority + ")"); } public AuthorizeRequestMatcherRegistry authenticated() { @@ -59,4 +64,19 @@ private AuthorizeRequestMatcherRegistry access(String attribute) { } } + public Boolean isAuthorized(String attribute, Authentication authentication) { + if (attribute.contains(AuthorizedUrl.AUTHORITY)) { + Pattern pattern = Pattern.compile("\\((.*?)\\)"); + Matcher matcher = pattern.matcher(attribute); + if(matcher.find()) { + String role = matcher.group(1).trim(); + return authentication.getAuthorities().contains(role); + } + } else if (attribute.contains(AuthorizedUrl.AUTHENTICATED)) { + return authentication.isAuthenticated(); + } + + return false; + } + } diff --git a/src/test/java/nextstep/app/MemberTest.java b/src/test/java/nextstep/app/MemberTest.java index 3e1941f..ac650d0 100644 --- a/src/test/java/nextstep/app/MemberTest.java +++ b/src/test/java/nextstep/app/MemberTest.java @@ -53,7 +53,7 @@ void members_request_fail_with_general_user() throws Exception { TEST_USER_MEMBER.getEmail(), TEST_USER_MEMBER.getPassword(), TEST_USER_MEMBER.getRoles() ); - response.andExpect(status().isForbidden()); + response.andExpect(status().isForbidden()); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); assertThat(authentication.isAuthenticated()).isTrue(); From e0dbdf5641065066f4918a23963bb9d73634eb42 Mon Sep 17 00:00:00 2001 From: "jaylin.re" Date: Mon, 12 Dec 2022 02:05:35 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=EB=B6=88=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/exception/MemberNotFoundException.java | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 src/main/java/nextstep/app/exception/MemberNotFoundException.java diff --git a/src/main/java/nextstep/app/exception/MemberNotFoundException.java b/src/main/java/nextstep/app/exception/MemberNotFoundException.java deleted file mode 100644 index 874b3c9..0000000 --- a/src/main/java/nextstep/app/exception/MemberNotFoundException.java +++ /dev/null @@ -1,4 +0,0 @@ -package nextstep.app.exception; - -public class MemberNotFoundException extends RuntimeException { -}