diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 1683058..fb68c0f 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -1,13 +1,20 @@ package nextstep.app; +import jakarta.servlet.http.HttpServletRequest; +import java.util.ArrayList; import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import nextstep.security.authentication.AuthenticationException; import nextstep.security.authentication.BasicAuthenticationFilter; import nextstep.security.authentication.UsernamePasswordAuthenticationFilter; -import nextstep.security.authorization.CheckAuthenticationFilter; -import nextstep.security.authorization.SecuredAspect; +import nextstep.security.authorization.AuthorizationFilter; +import nextstep.security.authorization.AuthorizationManager; import nextstep.security.authorization.SecuredMethodInterceptor; +import nextstep.security.authorization.method.SecuredAuthorizationManager; +import nextstep.security.authorization.web.AuthenticatedAuthorizationManager; +import nextstep.security.authorization.web.AuthorityAuthorizationManager; +import nextstep.security.authorization.web.DenyAllAuthorizationManager; +import nextstep.security.authorization.web.RequestMatcherDelegatingAuthorizationManager; import nextstep.security.config.DefaultSecurityFilterChain; import nextstep.security.config.DelegatingFilterProxy; import nextstep.security.config.FilterChainProxy; @@ -15,12 +22,18 @@ 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 nextstep.security.authorization.web.PermitAllAuthorizationManager; +import org.aopalliance.intercept.MethodInvocation; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import java.util.List; import java.util.Set; +import org.springframework.http.HttpMethod; @EnableAspectJAutoProxy @Configuration @@ -44,12 +57,26 @@ public FilterChainProxy filterChainProxy(List securityFilte @Bean public SecuredMethodInterceptor securedMethodInterceptor() { - return new SecuredMethodInterceptor(); + return new SecuredMethodInterceptor(securedAuthorizationManager()); + } + + @Bean + public AuthorizationManager securedAuthorizationManager() { + return new SecuredAuthorizationManager(); + } + + @Bean + public AuthorizationManager requestAuthorizationManager() { + List> mappings = new ArrayList<>(); + mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members"), + new AuthorityAuthorizationManager(Set.of("ADMIN")))); + mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members/me"), + new AuthenticatedAuthorizationManager())); + mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/search"), + new PermitAllAuthorizationManager())); + mappings.add(new RequestMatcherEntry<>(AnyRequestMatcher.INSTANCE, new DenyAllAuthorizationManager())); + return new RequestMatcherDelegatingAuthorizationManager(mappings); } -// @Bean -// public SecuredAspect securedAspect() { -// return new SecuredAspect(); -// } @Bean public SecurityFilterChain securityFilterChain() { @@ -58,7 +85,7 @@ public SecurityFilterChain securityFilterChain() { new SecurityContextHolderFilter(), new UsernamePasswordAuthenticationFilter(userDetailsService()), new BasicAuthenticationFilter(userDetailsService()), - new CheckAuthenticationFilter() + new AuthorizationFilter(requestAuthorizationManager()) ) ); } diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index 823cf7e..11d680b 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,15 @@ public ResponseEntity> search() { List members = memberRepository.findAll(); return ResponseEntity.ok(members); } + + @GetMapping("/members/me") + public ResponseEntity me() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + String email = authentication.getPrincipal().toString(); + Member member = memberRepository.findByEmail(email) + .orElseThrow(RuntimeException::new); + + return ResponseEntity.ok(member); + } } 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/AuthorizationDecision.java b/src/main/java/nextstep/security/authorization/AuthorizationDecision.java new file mode 100644 index 0000000..859efcf --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AuthorizationDecision.java @@ -0,0 +1,22 @@ +package nextstep.security.authorization; + +import nextstep.security.authorization.web.AuthorizationResult; + +public class AuthorizationDecision implements AuthorizationResult { + public static final AuthorizationDecision ALLOW = new AuthorizationDecision(true); + public static final AuthorizationDecision DENY = new AuthorizationDecision(false); + + private final boolean granted; + + public AuthorizationDecision(boolean granted) { + this.granted = granted; + } + + public boolean isGranted() { + return granted; + } + + public static AuthorizationDecision of(boolean granted) { + return granted ? ALLOW : DENY; + } +} 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..36c3b40 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AuthorizationFilter.java @@ -0,0 +1,34 @@ +package nextstep.security.authorization; + +import nextstep.security.authentication.Authentication; +import nextstep.security.authorization.web.AuthorizationResult; +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 { + + 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(); + AuthorizationResult authorizeResult = authorizationManager.authorize(authentication, request); + + if (!authorizeResult.isGranted()) { + throw new ForbiddenException(); + } + + filterChain.doFilter(request, response); + } +} 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..4be5334 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AuthorizationManager.java @@ -0,0 +1,14 @@ +package nextstep.security.authorization; + +import nextstep.security.authentication.Authentication; +import nextstep.security.authorization.web.AuthorizationResult; + +@FunctionalInterface +public interface AuthorizationManager { + @Deprecated + AuthorizationDecision check(Authentication authentication, T object); + + default AuthorizationResult authorize(Authentication authentication, T object) { + return check(authentication, object); + } +} 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); - } -} diff --git a/src/main/java/nextstep/security/authorization/SecuredMethodInterceptor.java b/src/main/java/nextstep/security/authorization/SecuredMethodInterceptor.java index 8ee7409..3490828 100644 --- a/src/main/java/nextstep/security/authorization/SecuredMethodInterceptor.java +++ b/src/main/java/nextstep/security/authorization/SecuredMethodInterceptor.java @@ -1,7 +1,7 @@ package nextstep.security.authorization; import nextstep.security.authentication.Authentication; -import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authorization.web.AuthorizationResult; import nextstep.security.context.SecurityContextHolder; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; @@ -11,29 +11,25 @@ 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 AuthorizationManager authorizationManager; private final Pointcut pointcut; - public SecuredMethodInterceptor() { + public SecuredMethodInterceptor(AuthorizationManager authorizationManager) { + this.authorizationManager = authorizationManager; this.pointcut = new AnnotationMatchingPointcut(null, Secured.class); } @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(); - } + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + AuthorizationResult authorizationResult = authorizationManager.authorize(authentication, invocation); + + if (!authorizationResult.isGranted()) { + throw new ForbiddenException(); } + return invocation.proceed(); } diff --git a/src/main/java/nextstep/security/authorization/method/SecuredAuthorizationManager.java b/src/main/java/nextstep/security/authorization/method/SecuredAuthorizationManager.java new file mode 100644 index 0000000..ddc03ee --- /dev/null +++ b/src/main/java/nextstep/security/authorization/method/SecuredAuthorizationManager.java @@ -0,0 +1,43 @@ +package nextstep.security.authorization.method; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import nextstep.security.authentication.Authentication; +import nextstep.security.authorization.AuthorizationDecision; +import nextstep.security.authorization.AuthorizationManager; +import nextstep.security.authorization.Secured; +import nextstep.security.authorization.web.AuthorityAuthorizationManager; +import org.aopalliance.intercept.MethodInvocation; + +public class SecuredAuthorizationManager implements AuthorizationManager { + + private AuthorityAuthorizationManager> authorityAuthorizationManager; + + public void setAuthorityAuthorizationManager(Collection authorities) { + authorityAuthorizationManager = new AuthorityAuthorizationManager<>(authorities); + } + + @Override + public AuthorizationDecision check(Authentication authentication, MethodInvocation invocation) { + Collection authorities = getAuthorities(invocation); + + if (authorities.isEmpty()) { + return null; + } + setAuthorityAuthorizationManager(authorities); + return authorities.isEmpty() ? null : authorityAuthorizationManager.check(authentication, authorities); + } + + private Collection getAuthorities(MethodInvocation invocation) { + Method method = invocation.getMethod(); + + if (!method.isAnnotationPresent(Secured.class)) { + return Collections.emptySet(); + } + + Secured secured = method.getAnnotation(Secured.class); + return Set.of(secured.value()); + } +} diff --git a/src/main/java/nextstep/security/authorization/web/AuthenticatedAuthorizationManager.java b/src/main/java/nextstep/security/authorization/web/AuthenticatedAuthorizationManager.java new file mode 100644 index 0000000..7787aac --- /dev/null +++ b/src/main/java/nextstep/security/authorization/web/AuthenticatedAuthorizationManager.java @@ -0,0 +1,16 @@ +package nextstep.security.authorization.web; + +import jakarta.servlet.http.HttpServletRequest; +import nextstep.security.authentication.Authentication; +import nextstep.security.authorization.AuthorizationDecision; +import nextstep.security.authorization.AuthorizationManager; + +public class AuthenticatedAuthorizationManager implements AuthorizationManager { + @Override + public AuthorizationDecision check(Authentication authentication, HttpServletRequest object) { + if (authentication != null && authentication.isAuthenticated()) { + return AuthorizationDecision.ALLOW; + } + return AuthorizationDecision.DENY; + } +} diff --git a/src/main/java/nextstep/security/authorization/web/AuthorityAuthorizationManager.java b/src/main/java/nextstep/security/authorization/web/AuthorityAuthorizationManager.java new file mode 100644 index 0000000..e99689f --- /dev/null +++ b/src/main/java/nextstep/security/authorization/web/AuthorityAuthorizationManager.java @@ -0,0 +1,38 @@ +package nextstep.security.authorization.web; + +import java.util.Collection; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authorization.AuthorizationDecision; +import nextstep.security.authorization.AuthorizationManager; + +public class AuthorityAuthorizationManager implements AuthorizationManager { + + private final Collection authorities; + + public AuthorityAuthorizationManager(Collection authorities) { + this.authorities = authorities; + } + + + @Override + public AuthorizationDecision check(Authentication authentication, T object) { + if (authentication == null) { + throw new AuthenticationException(); + } + + boolean hasAuthority = isAuthorized(authentication, authorities); + + return AuthorizationDecision.of(hasAuthority); + } + + + private boolean isAuthorized(Authentication authentication, Collection authorities) { + for (String authority : authentication.getAuthorities()) { + if (authorities.contains(authority)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/nextstep/security/authorization/web/AuthorizationResult.java b/src/main/java/nextstep/security/authorization/web/AuthorizationResult.java new file mode 100644 index 0000000..d18b14e --- /dev/null +++ b/src/main/java/nextstep/security/authorization/web/AuthorizationResult.java @@ -0,0 +1,5 @@ +package nextstep.security.authorization.web; + +public interface AuthorizationResult { + boolean isGranted(); +} diff --git a/src/main/java/nextstep/security/authorization/web/DenyAllAuthorizationManager.java b/src/main/java/nextstep/security/authorization/web/DenyAllAuthorizationManager.java new file mode 100644 index 0000000..35eed04 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/web/DenyAllAuthorizationManager.java @@ -0,0 +1,13 @@ +package nextstep.security.authorization.web; + +import jakarta.servlet.http.HttpServletRequest; +import nextstep.security.authentication.Authentication; +import nextstep.security.authorization.AuthorizationDecision; +import nextstep.security.authorization.AuthorizationManager; + +public class DenyAllAuthorizationManager implements AuthorizationManager { + @Override + public AuthorizationDecision check(Authentication authentication, HttpServletRequest object) { + return AuthorizationDecision.DENY; + } +} diff --git a/src/main/java/nextstep/security/authorization/web/PermitAllAuthorizationManager.java b/src/main/java/nextstep/security/authorization/web/PermitAllAuthorizationManager.java new file mode 100644 index 0000000..fa96f58 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/web/PermitAllAuthorizationManager.java @@ -0,0 +1,13 @@ +package nextstep.security.authorization.web; + +import jakarta.servlet.http.HttpServletRequest; +import nextstep.security.authentication.Authentication; +import nextstep.security.authorization.AuthorizationDecision; +import nextstep.security.authorization.AuthorizationManager; + +public class PermitAllAuthorizationManager implements AuthorizationManager { + @Override + public AuthorizationDecision check(Authentication authentication, HttpServletRequest object) { + return AuthorizationDecision.ALLOW; + } +} diff --git a/src/main/java/nextstep/security/authorization/web/RequestMatcherDelegatingAuthorizationManager.java b/src/main/java/nextstep/security/authorization/web/RequestMatcherDelegatingAuthorizationManager.java new file mode 100644 index 0000000..cb3f6f9 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/web/RequestMatcherDelegatingAuthorizationManager.java @@ -0,0 +1,28 @@ +package nextstep.security.authorization.web; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.List; +import nextstep.security.authentication.Authentication; +import nextstep.security.authorization.AuthorizationDecision; +import nextstep.security.authorization.AuthorizationManager; +import nextstep.security.util.RequestMatcher; +import nextstep.security.util.RequestMatcherEntry; + +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 : mappings) { + if (mapping.getRequestMatcher().matches(request)) { + return mapping.getEntry().check(authentication, request); + } + } + return AuthorizationDecision.DENY; + } +} 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..67e72d9 --- /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 static final RequestMatcher INSTANCE = new AnyRequestMatcher(); + + @Override + public boolean matches(HttpServletRequest request) { + return true; + } + +} 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..fc6ba85 --- /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 pattern; + + public MvcRequestMatcher(HttpMethod method, String pattern) { + this.method = method; + this.pattern = pattern; + } + + @Override + public boolean matches(HttpServletRequest request) { + return this.method.equals(HttpMethod.valueOf(request.getMethod())) && pattern.equals(request.getRequestURI()); + } +} 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..6e77696 --- /dev/null +++ b/src/main/java/nextstep/security/util/RequestMatcher.java @@ -0,0 +1,7 @@ +package nextstep.security.util; + +import jakarta.servlet.http.HttpServletRequest; + +public interface RequestMatcher { + boolean matches(HttpServletRequest request); +} 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..9153740 --- /dev/null +++ b/src/main/java/nextstep/security/util/RequestMatcherEntry.java @@ -0,0 +1,19 @@ +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; + } +} diff --git a/src/test/java/nextstep/app/BasicAuthTest.java b/src/test/java/nextstep/app/BasicAuthTest.java index ac117be..f281e01 100644 --- a/src/test/java/nextstep/app/BasicAuthTest.java +++ b/src/test/java/nextstep/app/BasicAuthTest.java @@ -1,6 +1,5 @@ package nextstep.app; -import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -14,16 +13,15 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import java.util.Base64; -import java.util.Set; +import static nextstep.security.fixture.MemberTestFixture.TEST_ADMIN_MEMBER; +import static nextstep.security.fixture.MemberTestFixture.TEST_USER_MEMBER; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc class BasicAuthTest { - private final Member TEST_ADMIN_MEMBER = new Member("a@a.com", "password", "a", "", Set.of("ADMIN")); - private final Member TEST_USER_MEMBER = new Member("b@b.com", "password", "b", "", Set.of()); @Autowired private MockMvc mockMvc; @@ -89,4 +87,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().isForbidden()); + } } diff --git a/src/test/java/nextstep/app/FormLoginTest.java b/src/test/java/nextstep/app/FormLoginTest.java index 6301c1a..dfed774 100644 --- a/src/test/java/nextstep/app/FormLoginTest.java +++ b/src/test/java/nextstep/app/FormLoginTest.java @@ -1,6 +1,5 @@ package nextstep.app; -import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -13,8 +12,8 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; -import java.util.Set; - +import static nextstep.security.fixture.MemberTestFixture.TEST_ADMIN_MEMBER; +import static nextstep.security.fixture.MemberTestFixture.TEST_USER_MEMBER; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -22,8 +21,6 @@ @SpringBootTest @AutoConfigureMockMvc class FormLoginTest { - private final Member TEST_ADMIN_MEMBER = new Member("a@a.com", "password", "a", "", Set.of("ADMIN")); - private final Member TEST_USER_MEMBER = new Member("b@b.com", "password", "b", "", Set.of()); @Autowired private MockMvc mockMvc; diff --git a/src/test/java/nextstep/app/SecuredTest.java b/src/test/java/nextstep/app/SecuredTest.java index 9e672e5..e9f19c6 100644 --- a/src/test/java/nextstep/app/SecuredTest.java +++ b/src/test/java/nextstep/app/SecuredTest.java @@ -1,7 +1,7 @@ package nextstep.app; -import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; +import nextstep.security.fixture.MemberTestFixture; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -13,9 +13,7 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; -import java.util.Base64; -import java.util.Set; - +import static nextstep.security.fixture.MemberTestFixture.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -23,8 +21,6 @@ @SpringBootTest @AutoConfigureMockMvc class SecuredTest { - private final Member TEST_ADMIN_MEMBER = new Member("a@a.com", "password", "a", "", Set.of("ADMIN")); - private final Member TEST_USER_MEMBER = new Member("b@b.com", "password", "b", "", Set.of()); @Autowired private MockMvc mockMvc; @@ -41,7 +37,7 @@ void setUp() { @DisplayName("ADMIN 권한을 가진 사용자가 요청할 경우 모든 회원 정보를 조회할 수 있다.") @Test void request_search_success_with_admin_user() throws Exception { - String token = Base64.getEncoder().encodeToString((TEST_ADMIN_MEMBER.getEmail() + ":" + TEST_ADMIN_MEMBER.getPassword()).getBytes()); + String token = createAdminToken(); ResultActions response = mockMvc.perform(get("/search") .header("Authorization", "Basic " + token) @@ -55,7 +51,7 @@ void request_search_success_with_admin_user() throws Exception { @DisplayName("일반 사용자가 요청할 경우 권한이 없어야 한다.") @Test void request_search_fail_with_general_user() throws Exception { - String token = Base64.getEncoder().encodeToString((TEST_USER_MEMBER.getEmail() + ":" + TEST_USER_MEMBER.getPassword()).getBytes()); + String token = createMemberToken(); ResultActions response = mockMvc.perform(get("/search") .header("Authorization", "Basic " + token) diff --git a/src/test/java/nextstep/app/SecurityAuthorizationApplicationTests.java b/src/test/java/nextstep/app/SecurityAuthorizationApplicationTests.java deleted file mode 100644 index 759cc3f..0000000 --- a/src/test/java/nextstep/app/SecurityAuthorizationApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package nextstep.app; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class SecurityAuthorizationApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/nextstep/security/fixture/MemberTestFixture.java b/src/test/java/nextstep/security/fixture/MemberTestFixture.java new file mode 100644 index 0000000..5b0c3bd --- /dev/null +++ b/src/test/java/nextstep/security/fixture/MemberTestFixture.java @@ -0,0 +1,18 @@ +package nextstep.security.fixture; + +import java.util.Base64; +import java.util.Set; +import nextstep.app.domain.Member; + +public class MemberTestFixture { + public static final Member TEST_ADMIN_MEMBER = new Member("a@a.com", "password", "a", "", Set.of("ADMIN")); + public static final Member TEST_USER_MEMBER = new Member("b@b.com", "password", "b", "", Set.of("USER")); + + public static String createAdminToken(){ + return Base64.getEncoder().encodeToString((TEST_ADMIN_MEMBER.getEmail() + ":" + TEST_ADMIN_MEMBER.getPassword()).getBytes()); + } + + public static String createMemberToken(){ + return Base64.getEncoder().encodeToString((TEST_USER_MEMBER.getEmail() + ":" + TEST_USER_MEMBER.getPassword()).getBytes()); + } +} diff --git a/src/test/java/nextstep/security/util/MvcRequestMatcherTest.java b/src/test/java/nextstep/security/util/MvcRequestMatcherTest.java new file mode 100644 index 0000000..0632f0d --- /dev/null +++ b/src/test/java/nextstep/security/util/MvcRequestMatcherTest.java @@ -0,0 +1,47 @@ +package nextstep.security.util; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockHttpServletRequest; + +@DisplayName("MvcRequestMatcher 테스트") +class MvcRequestMatcherTest { + + @DisplayName("matches() - http 메서드와 uri가 같으면 true 반환") + @Test + void MatchesWhenMethodAndPatternMatch() { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.setRequestURI("/test"); + + MvcRequestMatcher matcher = new MvcRequestMatcher(HttpMethod.GET, "/test"); + + // when + boolean matches = matcher.matches(request); + + // then + assertThat(matches).isTrue(); + } + + @DisplayName("matches() - http 메서드와 uri중 하나라도 다르면 false 반환") + @Test + void MatchesWhenMethodAndPatternIsNotMatch() { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setRequestURI("/test"); + + MvcRequestMatcher matcher = new MvcRequestMatcher(HttpMethod.GET, "/test"); + + // when + boolean matches = matcher.matches(request); + + // then + assertThat(matches).isFalse(); + } + +} \ No newline at end of file