diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 1683058..8dcd6d7 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -1,13 +1,19 @@ package nextstep.app; +import jakarta.servlet.http.HttpServletRequest; import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import nextstep.security.authentication.AuthenticationException; import nextstep.security.authentication.BasicAuthenticationFilter; import nextstep.security.authentication.UsernamePasswordAuthenticationFilter; -import nextstep.security.authorization.CheckAuthenticationFilter; -import nextstep.security.authorization.SecuredAspect; +import nextstep.security.authorization.AuthorizationFilter; import nextstep.security.authorization.SecuredMethodInterceptor; +import nextstep.security.authorization.manager.AuthenticatedAuthorizationManager; +import nextstep.security.authorization.manager.AuthorityAuthorizationManager; +import nextstep.security.authorization.manager.AuthorizationManager; +import nextstep.security.authorization.manager.DenyAllAuthorizationManager; +import nextstep.security.authorization.manager.PermitAllAuthorizationManager; +import nextstep.security.authorization.manager.RequestAuthorizationManager; import nextstep.security.config.DefaultSecurityFilterChain; import nextstep.security.config.DelegatingFilterProxy; import nextstep.security.config.FilterChainProxy; @@ -22,6 +28,10 @@ import java.util.List; import java.util.Set; +import static nextstep.security.authorization.matcher.RequestMatcherEntry.createDefaultMatcher; +import static nextstep.security.authorization.matcher.RequestMatcherEntry.createMvcMatcher; +import static org.springframework.http.HttpMethod.GET; + @EnableAspectJAutoProxy @Configuration public class SecurityConfig { @@ -46,19 +56,20 @@ public FilterChainProxy filterChainProxy(List securityFilte public SecuredMethodInterceptor securedMethodInterceptor() { return new SecuredMethodInterceptor(); } -// @Bean -// public SecuredAspect securedAspect() { -// return new SecuredAspect(); -// } @Bean public SecurityFilterChain securityFilterChain() { + final AuthorizationManager authorizationManager = new RequestAuthorizationManager(List.of( + createMvcMatcher(GET, "/members", new AuthorityAuthorizationManager<>("ADMIN")), + createMvcMatcher(GET, "/members/me", new AuthenticatedAuthorizationManager<>()), + createMvcMatcher(GET, "/search", new PermitAllAuthorizationManager<>()) + ), createDefaultMatcher(new DenyAllAuthorizationManager<>())); return new DefaultSecurityFilterChain( List.of( new SecurityContextHolderFilter(), new UsernamePasswordAuthenticationFilter(userDetailsService()), new BasicAuthenticationFilter(userDetailsService()), - new CheckAuthenticationFilter() + new AuthorizationFilter(authorizationManager) ) ); } diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index 823cf7e..0999dc4 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -2,12 +2,16 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationException; import nextstep.security.authorization.Secured; +import nextstep.security.context.SecurityContextHolder; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.Optional; @RestController public class MemberController { @@ -30,4 +34,22 @@ public ResponseEntity> search() { List members = memberRepository.findAll(); return ResponseEntity.ok(members); } + + @Secured("USER") + @GetMapping("/members/me") + public ResponseEntity me() { + final Authentication authentication = SecurityContextHolder + .getContext().getAuthentication(); + return ResponseEntity.ok( + Optional.ofNullable(authentication) + .flatMap(this::findMe) + .orElseThrow(AuthenticationException::new) + ); + } + + private Optional findMe(Authentication authentication) { + return memberRepository.findByEmail( + authentication.getPrincipal().toString() + ); + } } diff --git a/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java index 406116f..4cf3809 100644 --- a/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java +++ b/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java @@ -1,6 +1,7 @@ package nextstep.security.authentication; import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import nextstep.security.context.SecurityContext; @@ -10,6 +11,7 @@ import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; @@ -26,23 +28,23 @@ public BasicAuthenticationFilter(UserDetailsService userDetailsService) { } @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { - try { - Authentication authentication = convert(request); - if (authentication == null) { - filterChain.doFilter(request, response); - return; - } - - Authentication authResult = this.authenticationManager.authenticate(authentication); - SecurityContext context = SecurityContextHolder.createEmptyContext(); - context.setAuthentication(authResult); - SecurityContextHolder.setContext(context); - + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + Authentication authentication = convert(request); + if (authentication == null) { filterChain.doFilter(request, response); - } catch (Exception e) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; } + + Authentication authResult = this.authenticationManager.authenticate(authentication); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authResult); + SecurityContextHolder.setContext(context); + + filterChain.doFilter(request, response); } private Authentication convert(HttpServletRequest request) { diff --git a/src/main/java/nextstep/security/authorization/AuthorizationFilter.java b/src/main/java/nextstep/security/authorization/AuthorizationFilter.java new file mode 100644 index 0000000..091eb77 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AuthorizationFilter.java @@ -0,0 +1,37 @@ +package nextstep.security.authorization; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authorization.manager.AuthorizationManager; +import nextstep.security.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class AuthorizationFilter extends OncePerRequestFilter { + private final AuthorizationManager authorizationManager; + + public AuthorizationFilter(AuthorizationManager authorizationManager) { + this.authorizationManager = authorizationManager; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + throw new AuthenticationException(); + } + if (!authorizationManager.authorize(authentication, request).isGranted()) { + throw new ForbiddenException(); + } + filterChain.doFilter(request, response); + } +} 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..9911085 100644 --- a/src/main/java/nextstep/security/authorization/SecuredMethodInterceptor.java +++ b/src/main/java/nextstep/security/authorization/SecuredMethodInterceptor.java @@ -2,6 +2,7 @@ import nextstep.security.authentication.Authentication; import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authorization.manager.SecuredAuthorizationManager; import nextstep.security.context.SecurityContextHolder; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; @@ -11,8 +12,6 @@ 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 Pointcut pointcut; @@ -23,16 +22,17 @@ public SecuredMethodInterceptor() { @Override public Object invoke(MethodInvocation invocation) throws Throwable { - Method method = invocation.getMethod(); - if (method.isAnnotationPresent(Secured.class)) { - Secured secured = method.getAnnotation(Secured.class); - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null) { - throw new AuthenticationException(); - } - if (!authentication.getAuthorities().contains(secured.value())) { - throw new ForbiddenException(); - } + if (!invocation.getMethod().isAnnotationPresent(Secured.class)) { + return invocation.proceed(); + } + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + throw new AuthenticationException(); + } + if (!SecuredAuthorizationManager.getInstance().authorize( + authentication, invocation + ).isGranted()) { + throw new ForbiddenException(); } return invocation.proceed(); } diff --git a/src/main/java/nextstep/security/authorization/manager/AuthenticatedAuthorizationManager.java b/src/main/java/nextstep/security/authorization/manager/AuthenticatedAuthorizationManager.java new file mode 100644 index 0000000..9cdf2a8 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/AuthenticatedAuthorizationManager.java @@ -0,0 +1,12 @@ +package nextstep.security.authorization.manager; + +import nextstep.security.authentication.Authentication; + +public class AuthenticatedAuthorizationManager implements AuthorizationManager { + @Override + public AuthorizationResult authorize(Authentication authentication, T target) { + return AuthorizationDecision.of( + authentication != null && authentication.isAuthenticated() + ); + } +} diff --git a/src/main/java/nextstep/security/authorization/manager/AuthorityAuthorizationManager.java b/src/main/java/nextstep/security/authorization/manager/AuthorityAuthorizationManager.java new file mode 100644 index 0000000..b3d52bf --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/AuthorityAuthorizationManager.java @@ -0,0 +1,33 @@ +package nextstep.security.authorization.manager; + +import nextstep.security.authentication.Authentication; + +import java.util.Set; + +public class AuthorityAuthorizationManager implements AuthorizationManager { + private final Set authorities; + + public AuthorityAuthorizationManager(String... authorities) { + this.authorities = Set.of(authorities); + } + + @Override + public AuthorizationResult authorize(Authentication authentication, T target) { + return AuthorizationDecision.of(isGranted(authentication)); + } + + private boolean isGranted(Authentication authentication) { + return authentication != null + && authentication.isAuthenticated() + && anyMatch(authentication); + } + + private boolean anyMatch(Authentication authentication) { + for (var authority : authentication.getAuthorities()) { + if (authorities.contains(authority)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/nextstep/security/authorization/manager/AuthorizationDecision.java b/src/main/java/nextstep/security/authorization/manager/AuthorizationDecision.java new file mode 100644 index 0000000..36163d3 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/AuthorizationDecision.java @@ -0,0 +1,22 @@ +package nextstep.security.authorization.manager; + +public enum AuthorizationDecision implements AuthorizationResult { + GRANTED(true), NOT_GRANTED(false); + + private final boolean granted; + + AuthorizationDecision(boolean granted) { + this.granted = granted; + } + + public static AuthorizationDecision of(boolean granted) { + if (granted) { + return GRANTED; + } + return NOT_GRANTED; + } + + public boolean isGranted() { + return this.granted; + } +} diff --git a/src/main/java/nextstep/security/authorization/manager/AuthorizationManager.java b/src/main/java/nextstep/security/authorization/manager/AuthorizationManager.java new file mode 100644 index 0000000..51a5922 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/AuthorizationManager.java @@ -0,0 +1,8 @@ +package nextstep.security.authorization.manager; + +import nextstep.security.authentication.Authentication; + +@FunctionalInterface +public interface AuthorizationManager { + AuthorizationResult authorize(Authentication authentication, T target); +} diff --git a/src/main/java/nextstep/security/authorization/manager/AuthorizationResult.java b/src/main/java/nextstep/security/authorization/manager/AuthorizationResult.java new file mode 100644 index 0000000..987157f --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/AuthorizationResult.java @@ -0,0 +1,5 @@ +package nextstep.security.authorization.manager; + +public interface AuthorizationResult { + boolean isGranted(); +} diff --git a/src/main/java/nextstep/security/authorization/manager/DenyAllAuthorizationManager.java b/src/main/java/nextstep/security/authorization/manager/DenyAllAuthorizationManager.java new file mode 100644 index 0000000..a611aa2 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/DenyAllAuthorizationManager.java @@ -0,0 +1,12 @@ +package nextstep.security.authorization.manager; + +import nextstep.security.authentication.Authentication; + +import static nextstep.security.authorization.manager.AuthorizationDecision.NOT_GRANTED; + +public class DenyAllAuthorizationManager implements AuthorizationManager { + @Override + public AuthorizationResult authorize(Authentication authentication, T target) { + return NOT_GRANTED; + } +} diff --git a/src/main/java/nextstep/security/authorization/manager/PermitAllAuthorizationManager.java b/src/main/java/nextstep/security/authorization/manager/PermitAllAuthorizationManager.java new file mode 100644 index 0000000..4df5d5d --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/PermitAllAuthorizationManager.java @@ -0,0 +1,12 @@ +package nextstep.security.authorization.manager; + +import nextstep.security.authentication.Authentication; + +import static nextstep.security.authorization.manager.AuthorizationDecision.GRANTED; + +public class PermitAllAuthorizationManager implements AuthorizationManager { + @Override + public AuthorizationResult authorize(Authentication authentication, T target) { + return GRANTED; + } +} diff --git a/src/main/java/nextstep/security/authorization/manager/RequestAuthorizationManager.java b/src/main/java/nextstep/security/authorization/manager/RequestAuthorizationManager.java new file mode 100644 index 0000000..a7c7fb6 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/RequestAuthorizationManager.java @@ -0,0 +1,58 @@ +package nextstep.security.authorization.manager; + +import jakarta.servlet.http.HttpServletRequest; +import nextstep.security.authentication.Authentication; +import nextstep.security.authorization.matcher.RequestMatcherEntry; + +import java.util.List; + +public class RequestAuthorizationManager implements AuthorizationManager { + private final List>> entries; + private final RequestMatcherEntry> defaultEntry; + + public RequestAuthorizationManager( + List>> entries, + RequestMatcherEntry> defaultEntry + ) { + this.entries = entries; + this.defaultEntry = defaultEntry; + } + + @Override + public AuthorizationResult authorize(Authentication authentication, HttpServletRequest target) { + if (noneMatch(target)) { + return AuthorizationDecision.of( + check(authentication, target, defaultEntry) + ); + } + return AuthorizationDecision.of( + allMatch(authentication, target) + ); + } + + private boolean noneMatch(HttpServletRequest request) { + for (var entry : entries) { + if (entry.requestMatcher().matches(request)) { + return false; + } + } + return true; + } + + private boolean allMatch(Authentication authentication, HttpServletRequest request) { + for (var entry : entries) { + if (!check(authentication, request, entry)) { + return false; + } + } + return true; + } + + private boolean check( + Authentication authentication, HttpServletRequest request, + RequestMatcherEntry> matcherEntry + ) { + return !matcherEntry.requestMatcher().matches(request) + || matcherEntry.entry().authorize(authentication, request).isGranted(); + } +} diff --git a/src/main/java/nextstep/security/authorization/manager/SecuredAuthorizationManager.java b/src/main/java/nextstep/security/authorization/manager/SecuredAuthorizationManager.java new file mode 100644 index 0000000..9950dfe --- /dev/null +++ b/src/main/java/nextstep/security/authorization/manager/SecuredAuthorizationManager.java @@ -0,0 +1,34 @@ +package nextstep.security.authorization.manager; + +import nextstep.security.authentication.Authentication; +import nextstep.security.authorization.Secured; +import org.aopalliance.intercept.MethodInvocation; + +import java.lang.reflect.Method; + +public class SecuredAuthorizationManager implements AuthorizationManager { + private SecuredAuthorizationManager() {} + + public static SecuredAuthorizationManager getInstance() { + return SingletonHolder.INSTANCE; + } + + @Override + public AuthorizationResult authorize(Authentication authentication, MethodInvocation target) { + return AuthorizationDecision.of(hasAuthority(authentication, target.getMethod())); + } + + private boolean hasAuthority(Authentication authentication, Method method) { + if (!method.isAnnotationPresent(Secured.class)) { + return false; + } + return authentication != null + && authentication.isAuthenticated() + && authentication.getAuthorities() + .contains(method.getAnnotation(Secured.class).value()); + } + + private static final class SingletonHolder { + private static final SecuredAuthorizationManager INSTANCE = new SecuredAuthorizationManager(); + } +} diff --git a/src/main/java/nextstep/security/authorization/matcher/AnyRequestMatcher.java b/src/main/java/nextstep/security/authorization/matcher/AnyRequestMatcher.java new file mode 100644 index 0000000..27a711a --- /dev/null +++ b/src/main/java/nextstep/security/authorization/matcher/AnyRequestMatcher.java @@ -0,0 +1,20 @@ +package nextstep.security.authorization.matcher; + +import jakarta.servlet.http.HttpServletRequest; + +public class AnyRequestMatcher implements RequestMatcher { + private AnyRequestMatcher() {} + + public static AnyRequestMatcher getInstance() { + return SingletonHolder.INSTANCE; + } + + @Override + public boolean matches(HttpServletRequest request) { + return true; + } + + private final static class SingletonHolder { + private static final AnyRequestMatcher INSTANCE = new AnyRequestMatcher(); + } +} diff --git a/src/main/java/nextstep/security/authorization/matcher/MvcRequestMatcher.java b/src/main/java/nextstep/security/authorization/matcher/MvcRequestMatcher.java new file mode 100644 index 0000000..e8266c4 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/matcher/MvcRequestMatcher.java @@ -0,0 +1,36 @@ +package nextstep.security.authorization.matcher; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpMethod; + +import java.util.regex.Pattern; + +public class MvcRequestMatcher implements RequestMatcher { + private final HttpMethod method; + private final Pattern pattern; + + public MvcRequestMatcher(HttpMethod method, String pattern) { + this.method = method; + this.pattern = compile(pattern); + } + + private Pattern compile(String regex) { + if (regex == null || regex.isBlank()) { + return null; + } + return Pattern.compile(regex); + } + + @Override + public boolean matches(HttpServletRequest request) { + return matchMethod(request) && matchPattern(request); + } + + private boolean matchMethod(HttpServletRequest request) { + return method == null || method.name().equals(request.getMethod()); + } + + private boolean matchPattern(HttpServletRequest request) { + return pattern == null || pattern.matcher(request.getRequestURI()).matches(); + } +} diff --git a/src/main/java/nextstep/security/authorization/matcher/RequestMatcher.java b/src/main/java/nextstep/security/authorization/matcher/RequestMatcher.java new file mode 100644 index 0000000..3878f78 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/matcher/RequestMatcher.java @@ -0,0 +1,7 @@ +package nextstep.security.authorization.matcher; + +import jakarta.servlet.http.HttpServletRequest; + +public interface RequestMatcher { + boolean matches(HttpServletRequest request); +} diff --git a/src/main/java/nextstep/security/authorization/matcher/RequestMatcherEntry.java b/src/main/java/nextstep/security/authorization/matcher/RequestMatcherEntry.java new file mode 100644 index 0000000..174f025 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/matcher/RequestMatcherEntry.java @@ -0,0 +1,29 @@ +package nextstep.security.authorization.matcher; + +import jakarta.servlet.http.HttpServletRequest; +import nextstep.security.authorization.manager.AuthorizationManager; +import org.springframework.http.HttpMethod; + +public record RequestMatcherEntry( + RequestMatcher requestMatcher, + T entry +) { + public static RequestMatcherEntry> createMvcMatcher( + HttpMethod method, String pattern, + AuthorizationManager authorizationManager + ) { + return new RequestMatcherEntry<>( + new MvcRequestMatcher(method, pattern), + authorizationManager + ); + } + + public static RequestMatcherEntry> createDefaultMatcher( + AuthorizationManager authorizationManager + ) { + return new RequestMatcherEntry<>( + AnyRequestMatcher.getInstance(), + authorizationManager + ); + } +} diff --git a/src/main/java/nextstep/security/config/FilterChainProxy.java b/src/main/java/nextstep/security/config/FilterChainProxy.java index e40cea2..eedfe2a 100644 --- a/src/main/java/nextstep/security/config/FilterChainProxy.java +++ b/src/main/java/nextstep/security/config/FilterChainProxy.java @@ -1,12 +1,23 @@ package nextstep.security.config; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authorization.ForbiddenException; import org.springframework.web.filter.GenericFilterBean; -import jakarta.servlet.*; -import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.List; +import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN; +import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; + public class FilterChainProxy extends GenericFilterBean { private final List filterChains; @@ -16,10 +27,19 @@ public FilterChainProxy(List filterChains) { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - List filters = getFilters((HttpServletRequest) request); - - VirtualFilterChain virtualFilterChain = new VirtualFilterChain(chain, filters); - virtualFilterChain.doFilter(request, response); + final HttpServletRequest httpRequest = (HttpServletRequest) request; + final HttpServletResponse httpResponse = (HttpServletResponse) response; + try { + new VirtualFilterChain( + chain, getFilters(httpRequest) + ).doFilter(request, response); + } catch (AuthenticationException e) { + httpResponse.setStatus(SC_UNAUTHORIZED); + } catch (ForbiddenException e) { + httpResponse.setStatus(SC_FORBIDDEN); + } catch (Exception e) { + httpResponse.setStatus(SC_INTERNAL_SERVER_ERROR); + } } private List getFilters(HttpServletRequest request) { diff --git a/src/test/java/nextstep/MemberFixture.java b/src/test/java/nextstep/MemberFixture.java new file mode 100644 index 0000000..55d0147 --- /dev/null +++ b/src/test/java/nextstep/MemberFixture.java @@ -0,0 +1,36 @@ +package nextstep; + +import nextstep.app.domain.Member; + +import java.util.Set; + +public enum MemberFixture { + TEST_ADMIN_MEMBER(new Member( + "a@a.com", "password", + "a", "", + Set.of("USER", "ADMIN") + )), + TEST_USER_MEMBER(new Member( + "b@b.com", "password", + "b", "", + Set.of("USER") + )); + + private final Member member; + + MemberFixture(Member member) { + this.member = member; + } + + public Member getMember() { + return member; + } + + public String getEmail() { + return member.getEmail(); + } + + public String getPassword() { + return member.getPassword(); + } +} diff --git a/src/test/java/nextstep/Steps.java b/src/test/java/nextstep/Steps.java new file mode 100644 index 0000000..6583afc --- /dev/null +++ b/src/test/java/nextstep/Steps.java @@ -0,0 +1,16 @@ +package nextstep; + +import nextstep.app.domain.MemberRepository; + +public final class Steps { + private Steps() {} + + public static void setUp( + final MemberRepository memberRepository + ) { + for (var memberFixture : MemberFixture.values()) { + memberRepository.save(memberFixture.getMember()); + } + } + +} diff --git a/src/test/java/nextstep/TokenFixture.java b/src/test/java/nextstep/TokenFixture.java new file mode 100644 index 0000000..1a6e107 --- /dev/null +++ b/src/test/java/nextstep/TokenFixture.java @@ -0,0 +1,32 @@ +package nextstep; + +import java.util.Base64; + +import static nextstep.MemberFixture.TEST_ADMIN_MEMBER; +import static nextstep.MemberFixture.TEST_USER_MEMBER; + +public enum TokenFixture { + TEST_ADMIN_TOKEN(TEST_ADMIN_MEMBER), + TEST_USER_TOKEN(TEST_USER_MEMBER); + + private final String token; + + TokenFixture(MemberFixture member) { + this.token = createToken( + member.getEmail(), member.getPassword() + ); + } + + public static String createToken( + final String principal, + final String credential + ) { + return "Basic " + Base64.getEncoder().encodeToString( + (principal + ":" + credential).getBytes() + ); + } + + public String getToken() { + return token; + } +} diff --git a/src/test/java/nextstep/app/BasicAuthTest.java b/src/test/java/nextstep/app/BasicAuthTest.java index ac117be..b35a84c 100644 --- a/src/test/java/nextstep/app/BasicAuthTest.java +++ b/src/test/java/nextstep/app/BasicAuthTest.java @@ -1,6 +1,6 @@ package nextstep.app; -import nextstep.app.domain.Member; +import nextstep.Steps; import nextstep.app.domain.MemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -8,23 +8,20 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; 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.Set; - +import static nextstep.MemberFixture.TEST_ADMIN_MEMBER; +import static nextstep.MemberFixture.TEST_USER_MEMBER; +import static nextstep.TokenFixture.TEST_ADMIN_TOKEN; +import static nextstep.TokenFixture.TEST_USER_TOKEN; +import static nextstep.TokenFixture.createToken; 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; @@ -33,60 +30,92 @@ class BasicAuthTest { @BeforeEach void setUp() { - memberRepository.save(TEST_ADMIN_MEMBER); - memberRepository.save(TEST_USER_MEMBER); + Steps.setUp(memberRepository); } @DisplayName("ADMIN 권한을 가진 사용자가 요청할 경우 모든 회원 정보를 조회할 수 있다.") @Test void request_success_with_admin_user() throws Exception { - String token = Base64.getEncoder().encodeToString((TEST_ADMIN_MEMBER.getEmail() + ":" + TEST_ADMIN_MEMBER.getPassword()).getBytes()); - - ResultActions response = mockMvc.perform(get("/members") - .header("Authorization", "Basic " + token) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - ); - - response.andExpect(status().isOk()) - .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(2)); + mockMvc.perform( + get("/members") + .header("Authorization", TEST_ADMIN_TOKEN.getToken()) + ).andExpect( + status().isOk() + ).andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(2)); } @DisplayName("일반 사용자가 요청할 경우 권한이 없어야 한다.") @Test void request_fail_with_general_user() throws Exception { - String token = Base64.getEncoder().encodeToString((TEST_USER_MEMBER.getEmail() + ":" + TEST_USER_MEMBER.getPassword()).getBytes()); - - ResultActions response = mockMvc.perform(get("/members") - .header("Authorization", "Basic " + token) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - ); - - response.andExpect(status().isForbidden()); + mockMvc.perform( + get("/members") + .header("Authorization", TEST_USER_TOKEN.getToken()) + ).andExpect(status().isForbidden()); } @DisplayName("사용자 정보가 없는 경우 요청이 실패해야 한다.") @Test void request_fail_with_no_user() throws Exception { - String token = Base64.getEncoder().encodeToString(("none" + ":" + "none").getBytes()); - - ResultActions response = mockMvc.perform(get("/members") - .header("Authorization", "Basic " + token) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - ); - - response.andExpect(status().isUnauthorized()); + mockMvc.perform( + get("/members") + .header("Authorization", createToken("none", "none")) + ).andExpect(status().isUnauthorized()); } @DisplayName("Invalid한 패스워드로 요청할 경우 실패해야 한다.") @Test void request_fail_invalid_password() throws Exception { - String token = Base64.getEncoder().encodeToString((TEST_ADMIN_MEMBER.getEmail() + ":" + "invalid").getBytes()); + String invalidToken = createToken(TEST_ADMIN_MEMBER.getEmail(), "invalid"); - ResultActions response = mockMvc.perform(get("/members") - .header("Authorization", "Basic " + token) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + mockMvc.perform( + get("/members") + .header("Authorization", invalidToken) + ).andExpect(status().isUnauthorized()); + } + + @DisplayName("인증없이 자신의 정보를 조회하면 실패해야 한다.") + @Test + void request_fail_me() throws Exception { + mockMvc.perform( + get("/members/me") + ).andExpect(status().isUnauthorized()); + } + + @DisplayName("일반 사용자는 인증 후 자신의 정보를 조회할 수 있다.") + @Test + void request_success_me_with_member() throws Exception { + mockMvc.perform( + get("/members/me") + .header("Authorization", TEST_USER_TOKEN.getToken()) + ).andExpect( + status().isOk() + ).andExpect( + MockMvcResultMatchers.jsonPath("$.email") + .value(TEST_USER_MEMBER.getEmail()) ); - response.andExpect(status().isUnauthorized()); + mockMvc.perform( + get("/members/me") + .header("Authorization", TEST_ADMIN_TOKEN.getToken()) + ).andExpect( + status().isOk() + ).andExpect( + MockMvcResultMatchers.jsonPath("$.email") + .value(TEST_ADMIN_MEMBER.getEmail()) + ); + } + + @DisplayName("일반 사용자는 인증 후 자신의 정보를 조회할 수 있다.") + @Test + void request_success_me_with_admin() throws Exception { + mockMvc.perform( + get("/members/me") + .header("Authorization", TEST_ADMIN_TOKEN.getToken()) + ).andExpect( + status().isOk() + ).andExpect( + MockMvcResultMatchers.jsonPath("$.email") + .value(TEST_ADMIN_MEMBER.getEmail()) + ); } } diff --git a/src/test/java/nextstep/app/FormLoginTest.java b/src/test/java/nextstep/app/FormLoginTest.java index 6301c1a..c06a590 100644 --- a/src/test/java/nextstep/app/FormLoginTest.java +++ b/src/test/java/nextstep/app/FormLoginTest.java @@ -1,6 +1,6 @@ package nextstep.app; -import nextstep.app.domain.Member; +import nextstep.Steps; import nextstep.app.domain.MemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -8,13 +8,12 @@ import org.springframework.beans.factory.annotation.Autowired; 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 java.util.Set; +import static nextstep.MemberFixture.TEST_ADMIN_MEMBER; +import static nextstep.MemberFixture.TEST_USER_MEMBER; +import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; 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,9 +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; @@ -33,44 +29,40 @@ class FormLoginTest { @BeforeEach void setUp() { - memberRepository.save(TEST_ADMIN_MEMBER); - memberRepository.save(TEST_USER_MEMBER); + Steps.setUp(memberRepository); } @DisplayName("로그인 성공") @Test void login_success() throws Exception { - ResultActions loginResponse = mockMvc.perform(post("/login") - .param("username", TEST_USER_MEMBER.getEmail()) - .param("password", TEST_USER_MEMBER.getPassword()) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - ); - - loginResponse.andExpect(status().isOk()); + mockMvc.perform( + post("/login") + .param("username", TEST_USER_MEMBER.getEmail()) + .param("password", TEST_USER_MEMBER.getPassword()) + .contentType(APPLICATION_FORM_URLENCODED_VALUE) + ).andExpect(status().isOk()); } @DisplayName("로그인 실패 - 사용자 없음") @Test void login_fail_with_no_user() throws Exception { - ResultActions response = mockMvc.perform(post("/login") - .param("username", "none") - .param("password", "none") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - ); - - response.andExpect(status().isUnauthorized()); + mockMvc.perform( + post("/login") + .param("username", "none") + .param("password", "none") + .contentType(APPLICATION_FORM_URLENCODED_VALUE) + ).andExpect(status().isUnauthorized()); } @DisplayName("로그인 실패 - 비밀번호 불일치") @Test void login_fail_with_invalid_password() throws Exception { - ResultActions response = mockMvc.perform(post("/login") - .param("username", TEST_USER_MEMBER.getEmail()) - .param("password", "invalid") - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - ); - - response.andExpect(status().isUnauthorized()); + mockMvc.perform( + post("/login") + .param("username", TEST_USER_MEMBER.getEmail()) + .param("password", "invalid") + .contentType(APPLICATION_FORM_URLENCODED_VALUE) + ).andExpect(status().isUnauthorized()); } @DisplayName("로그인 후 세션을 통해 회원 목록 조회") @@ -78,21 +70,19 @@ void login_fail_with_invalid_password() throws Exception { void admin_login_after_members() throws Exception { MockHttpSession session = new MockHttpSession(); - ResultActions loginResponse = mockMvc.perform(post("/login") - .param("username", TEST_ADMIN_MEMBER.getEmail()) - .param("password", TEST_ADMIN_MEMBER.getPassword()) - .session(session) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - ); - - loginResponse.andExpect(status().isOk()); - - ResultActions membersResponse = mockMvc.perform(get("/members") - .session(session) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - ); - - membersResponse.andExpect(status().isOk()); + mockMvc.perform( + post("/login") + .param("username", TEST_ADMIN_MEMBER.getEmail()) + .param("password", TEST_ADMIN_MEMBER.getPassword()) + .session(session) + .contentType(APPLICATION_FORM_URLENCODED_VALUE) + ).andExpect(status().isOk()); + + mockMvc.perform( + get("/members") + .session(session) + .contentType(APPLICATION_FORM_URLENCODED_VALUE) + ).andExpect(status().isOk()); } @DisplayName("일반 회원은 회원 목록 조회 불가능") @@ -100,20 +90,18 @@ void admin_login_after_members() throws Exception { void user_login_after_members() throws Exception { MockHttpSession session = new MockHttpSession(); - ResultActions loginResponse = mockMvc.perform(post("/login") - .param("username", TEST_USER_MEMBER.getEmail()) - .param("password", TEST_USER_MEMBER.getPassword()) - .session(session) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - ); - - loginResponse.andExpect(status().isOk()); - - ResultActions membersResponse = mockMvc.perform(get("/members") - .session(session) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - ); - - membersResponse.andExpect(status().isForbidden()); + mockMvc.perform( + post("/login") + .param("username", TEST_USER_MEMBER.getEmail()) + .param("password", TEST_USER_MEMBER.getPassword()) + .session(session) + .contentType(APPLICATION_FORM_URLENCODED_VALUE) + ).andExpect(status().isOk()); + + mockMvc.perform( + get("/members") + .session(session) + .contentType(APPLICATION_FORM_URLENCODED_VALUE) + ).andExpect(status().isForbidden()); } } diff --git a/src/test/java/nextstep/app/SecuredTest.java b/src/test/java/nextstep/app/SecuredTest.java index 9e672e5..df88aff 100644 --- a/src/test/java/nextstep/app/SecuredTest.java +++ b/src/test/java/nextstep/app/SecuredTest.java @@ -1,6 +1,6 @@ package nextstep.app; -import nextstep.app.domain.Member; +import nextstep.Steps; import nextstep.app.domain.MemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -8,14 +8,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; 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.Set; - +import static nextstep.TokenFixture.TEST_ADMIN_TOKEN; +import static nextstep.TokenFixture.TEST_USER_TOKEN; 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,9 +20,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; @@ -34,34 +28,35 @@ class SecuredTest { @BeforeEach void setUp() { - memberRepository.save(TEST_ADMIN_MEMBER); - memberRepository.save(TEST_USER_MEMBER); + Steps.setUp(memberRepository); } @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()); - - ResultActions response = mockMvc.perform(get("/search") - .header("Authorization", "Basic " + token) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - ).andDo(print()); - - response.andExpect(status().isOk()) - .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(2)); + mockMvc.perform( + get("/search") + .header("Authorization", TEST_ADMIN_TOKEN.getToken()) + ).andDo(print()).andExpect( + status().isOk() + ).andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(2)); } @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()); - - ResultActions response = mockMvc.perform(get("/search") - .header("Authorization", "Basic " + token) - .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) - ).andDo(print()); + mockMvc.perform( + get("/search") + .header("Authorization", TEST_USER_TOKEN.getToken()) + ).andDo(print()).andExpect(status().isForbidden()); + } - response.andExpect(status().isForbidden()); + @DisplayName("ADMIN 권한을 가진 사용자라도, 제공되지 않는 경로는 접근할 수 없다.") + @Test + void request_fail_with_admin_user() throws Exception { + mockMvc.perform( + get("/wrong-url") + .header("Authorization", TEST_ADMIN_TOKEN.getToken()) + ).andDo(print()).andExpect(status().isForbidden()); } } 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/authorization/manager/AuthenticatedAuthorizationManagerTest.java b/src/test/java/nextstep/security/authorization/manager/AuthenticatedAuthorizationManagerTest.java new file mode 100644 index 0000000..b2d0952 --- /dev/null +++ b/src/test/java/nextstep/security/authorization/manager/AuthenticatedAuthorizationManagerTest.java @@ -0,0 +1,63 @@ +package nextstep.security.authorization.manager; + +import nextstep.security.authentication.Authentication; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static nextstep.security.authorization.manager.AuthorizationDecision.GRANTED; +import static nextstep.security.authorization.manager.AuthorizationDecision.NOT_GRANTED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class AuthenticatedAuthorizationManagerTest { + private final AuthorizationManager manager = new AuthenticatedAuthorizationManager<>(); + + @DisplayName("유저가 인증되지 않았다면 인가받지 못한다.") + @Test + void notAuthenticated() { + assertAll( + () -> assertThat(manager.authorize( + null, null + )).isEqualTo(NOT_GRANTED), + () -> assertThat(manager.authorize( + createAuthentication(false), null + )).isEqualTo(NOT_GRANTED) + ); + } + + @DisplayName("유저가 인증되었다면 인가받는다.") + @Test + void authenticated() { + assertThat(manager.authorize( + createAuthentication(true), null) + ).isEqualTo(GRANTED); + } + + private Authentication createAuthentication( + boolean isAuthenticated + ) { + return new Authentication() { + @Override + public Set getAuthorities() { + return Set.of(); + } + + @Override + public Object getCredentials() { + return "PASSWORD"; + } + + @Override + public Object getPrincipal() { + return "USERNAME"; + } + + @Override + public boolean isAuthenticated() { + return isAuthenticated; + } + }; + } +} diff --git a/src/test/java/nextstep/security/authorization/manager/AuthorityAuthorizationManagerTest.java b/src/test/java/nextstep/security/authorization/manager/AuthorityAuthorizationManagerTest.java new file mode 100644 index 0000000..afe6a0e --- /dev/null +++ b/src/test/java/nextstep/security/authorization/manager/AuthorityAuthorizationManagerTest.java @@ -0,0 +1,82 @@ +package nextstep.security.authorization.manager; + +import nextstep.security.authentication.Authentication; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static nextstep.security.authorization.manager.AuthorizationDecision.GRANTED; +import static nextstep.security.authorization.manager.AuthorizationDecision.NOT_GRANTED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class AuthorityAuthorizationManagerTest { + private final AuthorizationManager manager = new AuthorityAuthorizationManager<>( + "ADMIN", "USER" + ); + + @DisplayName("유저가 인증되지 않았다면 인가받지 못한다.") + @Test + void notAuthenticated() { + assertAll( + () -> assertThat(manager.authorize( + null, null + )).isEqualTo(NOT_GRANTED), + () -> assertThat(manager.authorize( + createAuthentication(false), null + )).isEqualTo(NOT_GRANTED) + ); + } + + @DisplayName("인증된 어드민 유저가 Authority 를 가졌다면 인가받는다.") + @Test + void adminNoAuthority() { + assertThat(manager.authorize( + createAuthentication(true, "ADMIN"), null) + ).isEqualTo(GRANTED); + } + + @DisplayName("인증된 일반 유저가 Authority 를 가졌다면 인가받는다.") + @Test + void userNoAuthority() { + assertThat(manager.authorize( + createAuthentication(true, "USER"), null) + ).isEqualTo(GRANTED); + } + + @DisplayName("인증된 유저가 Authority 를 가지지 못했다면 인가받지 못한다.") + @Test + void hasAuthority() { + assertThat(manager.authorize( + createAuthentication(true, "ANONYMOUS"), null) + ).isEqualTo(NOT_GRANTED); + } + + private Authentication createAuthentication( + boolean isAuthenticated, + String... authorities + ) { + return new Authentication() { + @Override + public Set getAuthorities() { + return Set.of(authorities); + } + + @Override + public Object getCredentials() { + return "PASSWORD"; + } + + @Override + public Object getPrincipal() { + return "USERNAME"; + } + + @Override + public boolean isAuthenticated() { + return isAuthenticated; + } + }; + } +} diff --git a/src/test/java/nextstep/security/authorization/manager/AuthorizationDecisionTest.java b/src/test/java/nextstep/security/authorization/manager/AuthorizationDecisionTest.java new file mode 100644 index 0000000..b239dab --- /dev/null +++ b/src/test/java/nextstep/security/authorization/manager/AuthorizationDecisionTest.java @@ -0,0 +1,23 @@ +package nextstep.security.authorization.manager; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class AuthorizationDecisionTest { + + @DisplayName("인증 여부(granted) 를 저장할 수 있다.") + @Test + void isGranted() { + assertAll( + () -> assertThat( + AuthorizationDecision.of(true).isGranted() + ).isTrue(), + () -> assertThat( + AuthorizationDecision.of(false).isGranted() + ).isFalse() + ); + } +} diff --git a/src/test/java/nextstep/security/authorization/manager/DenyAllAuthorizationManagerTest.java b/src/test/java/nextstep/security/authorization/manager/DenyAllAuthorizationManagerTest.java new file mode 100644 index 0000000..e6d4d60 --- /dev/null +++ b/src/test/java/nextstep/security/authorization/manager/DenyAllAuthorizationManagerTest.java @@ -0,0 +1,18 @@ +package nextstep.security.authorization.manager; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static nextstep.security.authorization.manager.AuthorizationDecision.NOT_GRANTED; +import static org.assertj.core.api.Assertions.assertThat; + +class DenyAllAuthorizationManagerTest { + private final AuthorizationManager manager = new DenyAllAuthorizationManager<>(); + + @DisplayName("DenyAll 일 경우 항상 Granted 되어 있지 않다.") + @Test + void authorize() { + assertThat(manager.authorize(null, null)) + .isEqualTo(NOT_GRANTED); + } +} diff --git a/src/test/java/nextstep/security/authorization/manager/PermitAllAuthorizationManagerTest.java b/src/test/java/nextstep/security/authorization/manager/PermitAllAuthorizationManagerTest.java new file mode 100644 index 0000000..9aab7d8 --- /dev/null +++ b/src/test/java/nextstep/security/authorization/manager/PermitAllAuthorizationManagerTest.java @@ -0,0 +1,18 @@ +package nextstep.security.authorization.manager; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static nextstep.security.authorization.manager.AuthorizationDecision.GRANTED; +import static org.assertj.core.api.Assertions.assertThat; + +class PermitAllAuthorizationManagerTest { + private final AuthorizationManager manager = new PermitAllAuthorizationManager<>(); + + @DisplayName("PermitAll 일 경우 항상 Granted 되어 있다.") + @Test + void authorize() { + assertThat(manager.authorize(null, null)) + .isEqualTo(GRANTED); + } +} diff --git a/src/test/java/nextstep/security/authorization/matcher/AnyRequestMatcherTest.java b/src/test/java/nextstep/security/authorization/matcher/AnyRequestMatcherTest.java new file mode 100644 index 0000000..191c9dd --- /dev/null +++ b/src/test/java/nextstep/security/authorization/matcher/AnyRequestMatcherTest.java @@ -0,0 +1,18 @@ +package nextstep.security.authorization.matcher; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +class AnyRequestMatcherTest { + + @DisplayName("AnyRequestMatcher 는 모든 request 에 대해 match 가 true 이다.") + @Test + void matches() { + assertThat(AnyRequestMatcher.getInstance().matches( + new MockHttpServletRequest() + )).isTrue(); + } +} diff --git a/src/test/java/nextstep/security/authorization/matcher/MvcRequestMatcherTest.java b/src/test/java/nextstep/security/authorization/matcher/MvcRequestMatcherTest.java new file mode 100644 index 0000000..ad85e14 --- /dev/null +++ b/src/test/java/nextstep/security/authorization/matcher/MvcRequestMatcherTest.java @@ -0,0 +1,67 @@ +package nextstep.security.authorization.matcher; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class MvcRequestMatcherTest { + private final MvcRequestMatcher matcher = new MvcRequestMatcher( + HttpMethod.GET, "/members" + ); + + @DisplayName("method 와 pattern 이 같아야 match 가 true 이다.") + @Test + void matches() { + final MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.setRequestURI("/members"); + assertThat(matcher.matches(request)).isTrue(); + } + + @DisplayName("method 가 다르면 match 가 false 이다.") + @Test + void differentMethod() { + final MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setRequestURI("/members"); + assertThat(matcher.matches(request)).isFalse(); + } + + @DisplayName("pattern 이 다르면 match 가 false 이다.") + @Test + void differentPattern() { + final MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + request.setRequestURI("/member"); + assertThat(matcher.matches(request)).isFalse(); + } + + @DisplayName("method 와 pattern 이 다르면 match 가 false 이다.") + @Test + void different() { + final MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setRequestURI("/member"); + assertThat(matcher.matches(request)).isFalse(); + } + + @DisplayName("method 와 pattern 이 존재하지 않으면 match 가 true 이다.") + @Test + void nullMethodPattern() { + final MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setRequestURI("/member"); + assertAll( + () -> assertThat(new MvcRequestMatcher( + null, null + ).matches(request)).isTrue(), + () -> assertThat(new MvcRequestMatcher( + null, " " + ).matches(request)).isTrue() + ); + } +}