diff --git a/README.md b/README.md index 1e7ba652..8c2db020 100644 --- a/README.md +++ b/README.md @@ -1 +1,69 @@ # spring-security-authentication + +## 인증과 서비스 로직간의 분리 + +- [x] 인증은 security 패키지에 위치해야한다. +- [x] 서비스는 app 패키지에 위치해야한다. + +## 기능 요구 사항 + +1. 아이디와 비밀번호 기반의 로그인 기능 구현 +2. Basic 인증을 사용하여 사용자를 식별할 수 있도록 스프링 프래임워크를 통한 웹 앱 구현 + +### 아이디와 비밀번호 기반 로그인 구현 + +- [x] 사용자가 입력한 아이디와 비밀번호를 확인하여 인증한다. +- [x] 로그인 성공시 Session 을 사용하여 인증 정보를 저장한다. +- [x] LoginTest 의 모든 테스트가 성공해야한다. + +### Basic 인증 구현 + +- [x] 요청의 Authorization 헤더에 Basic 인증 정보를 추출 하여 인증을 추출한다. +- [x] 인증을 성공한 경우 Session 을 사용하여 인증 정보를 저장한다. +- [x] MemberTest 의 모든 테스트가 통과해야한다. + +## 프로그래밍 요구사항 + +- [x] 자바 코드 컨벤션을 준수한다. +- [x] 들여쓰기는 depth 가 3 이 넘지 않도록 한다. +- [x] 3항 연산자를 사용하지 않는다. +- [x] 함수의 길이가 15 라인을 넘지 않도록 한다. +- [x] else 예약어를 사용하지 않는다. +- [x] 정리한 기능 목록이 정상적으로 동작하는지 테스트 코드를 구현한다. + +# 페어 코딩 + +- `SecurityFilterChain` : 보안 필터의 묶음을 정의하는 인터페이스 +- `DefaultSecurityFilterChain` : `SecurityFilterChain` 의 구현체 +- `FilterChainProxy` : 보안 필터의 묶음을 관리하는 객체 +- `DelegatingFilterProxy` : 스프링에 등록한 필터를 실행을 위임할 객체 + +패키지 분리 + +## 1. Interceptor 에서 Filter 로 변경하기 + +### `GenericFilterBean` 와 `OncePerRequestFilter` 의 차이점 + +실행 횟수: `GenericFilterBean` 은 요청마다 실행될 수 있지만, `OncePerRequestFilter` 는 요청당 한 번만 실행됩니다. +사용 목적: 요청별로 한 번만 실행되어야 하는 필터링 로직에는 `OncePerRequestFilter` 가 적합하며, 그렇지 않으면 `GenericFilterBean` 을 사용할 +수 있습니다. + +- [x] `BasicAuthorizationInterceptor` -> `BasicAuthenticationFilter` +- [x] `FormLoginAuthorizationInterceptor` -> `UsernamePasswordAuthenticationFilter` +- [x] `WebMvcConfigurer` -> `SecurityConfig` 로 변경 + +## 2. AuthenticationManager 로 인증 추상화 하기 + +- [x] `AuthenticationManager` 구현 +- [x] `ProviderManager` 구현 +- [x] `AuthenticationProvider` 구현 +- [x] `DaoAuthenticationProvider` 구현 + +## SecurityContextHolder 로 인증 정보 객체 저장하기 + +- [x] `SecurityContext` 구현 +- [x] `SecurityContextHolder` 구현 +- [x] seesion 에서 `SecurityContext` 로 인증 정보 저장하도록 변경 + +## SecurityContextHolderFilter 구현하기 + diff --git a/src/main/java/nextstep/app/config/SecurityConfig.java b/src/main/java/nextstep/app/config/SecurityConfig.java new file mode 100644 index 00000000..5a0be553 --- /dev/null +++ b/src/main/java/nextstep/app/config/SecurityConfig.java @@ -0,0 +1,45 @@ +package nextstep.app.config; + +import java.util.List; +import nextstep.security.BasicAuthenticationFilter; +import nextstep.security.UsernamePasswordAuthenticationFilter; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.filter.DefaultSecurityFilterChain; +import nextstep.security.filter.DelegatingFilterProxy; +import nextstep.security.filter.FilterChainProxy; +import nextstep.security.filter.SecurityFilterChain; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SecurityConfig { + + private final AuthenticationManager authenticationManager; + + public SecurityConfig(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Bean + public SecurityFilterChain securityFilterChain() { + return new DefaultSecurityFilterChain( + List.of( + new BasicAuthenticationFilter(authenticationManager), + new UsernamePasswordAuthenticationFilter(authenticationManager) + ) + ); + } + + @Bean + public FilterChainProxy filterChainProxy(List securityFilterChains) { + // 여러 개의 시큐리티 필터 체인을 목록으로 가진다. + return new FilterChainProxy(securityFilterChains); + } + + @Bean + public DelegatingFilterProxy delegatingFilterProxy() { + // 필터 체인 프록시에게 위임하는 역할을 한다. + return new DelegatingFilterProxy(filterChainProxy(List.of(securityFilterChain()))); + } + +} diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java new file mode 100644 index 00000000..ae056c7b --- /dev/null +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -0,0 +1,32 @@ +package nextstep.app.config; + +import java.util.List; +import nextstep.app.service.UserDetailService; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.AuthenticationProvider; +import nextstep.security.authentication.DaoAuthenticationProvider; +import nextstep.security.authentication.ProviderManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final UserDetailService userDetailService; + + public WebConfig(UserDetailService userDetailService) { + this.userDetailService = userDetailService; + } + + @Bean + public AuthenticationManager authenticationManager() { + return new ProviderManager(authenticationProviders()); + } + + @Bean + public List authenticationProviders() { + return List.of(new DaoAuthenticationProvider(userDetailService)); + } + +} diff --git a/src/main/java/nextstep/app/service/UserDetail.java b/src/main/java/nextstep/app/service/UserDetail.java new file mode 100644 index 00000000..e44d2295 --- /dev/null +++ b/src/main/java/nextstep/app/service/UserDetail.java @@ -0,0 +1,20 @@ +package nextstep.app.service; + +public class UserDetail { + + private final String email; + private final String password; + + public UserDetail(String email, String password) { + this.email = email; + this.password = password; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } +} diff --git a/src/main/java/nextstep/app/service/UserDetailService.java b/src/main/java/nextstep/app/service/UserDetailService.java new file mode 100644 index 00000000..0771f796 --- /dev/null +++ b/src/main/java/nextstep/app/service/UserDetailService.java @@ -0,0 +1,21 @@ +package nextstep.app.service; + +import nextstep.app.domain.MemberRepository; +import org.springframework.stereotype.Service; + +@Service +public class UserDetailService { + + private final MemberRepository memberRepository; + + public UserDetailService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public UserDetail findUserDetail(String email) { + return memberRepository.findByEmail(email) + .map(member -> new UserDetail(member.getEmail(), member.getPassword())) + .orElse(null); + } + +} diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 0ea94f1b..a9f65597 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,24 +1,15 @@ package nextstep.app.ui; -import nextstep.app.domain.MemberRepository; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - @RestController public class LoginController { - public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - - private final MemberRepository memberRepository; - - public LoginController(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } @PostMapping("/login") public ResponseEntity login(HttpServletRequest request, HttpSession session) { diff --git a/src/main/java/nextstep/security/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/BasicAuthenticationFilter.java new file mode 100644 index 00000000..d1715f8a --- /dev/null +++ b/src/main/java/nextstep/security/BasicAuthenticationFilter.java @@ -0,0 +1,107 @@ +package nextstep.security; + +import static nextstep.security.SecurityConstants.BASIC_TOKEN_PREFIX; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import nextstep.app.ui.AuthenticationException; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; +import nextstep.security.context.SecurityContext; +import nextstep.security.context.SecurityContextHolder; +import org.springframework.http.HttpHeaders; +import org.springframework.util.Base64Utils; +import org.springframework.web.filter.OncePerRequestFilter; + +public class BasicAuthenticationFilter extends OncePerRequestFilter { + + private final AuthenticationManager authenticationManager; + + private final List ACCEPTED_URIS = List.of( + "/members" + ); + + public BasicAuthenticationFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + if (!ACCEPTED_URIS.contains(request.getRequestURI())) { + filterChain.doFilter(request, response); + return; + } + + try { + checkAuthentication(request); + filterChain.doFilter(request, response); + } catch (Exception ex) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + SecurityContextHolder.clearContext(); + } + } + + private void checkAuthentication(HttpServletRequest request) { + String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + + validateBasicToken(authorization); + + String decodedToken = decodeToken(authorization); + + Authentication authentication = authenticationManager.authenticate( + createAuthentication(decodedToken)); + + validateAuthentication(authentication); + + SecurityContext ctx = SecurityContextHolder.createEmptyContext(); + ctx.setAuthentication(authentication); + SecurityContextHolder.setContext(ctx); + } + + private void validateBasicToken(String authorization) { + if (authorization == null) { + throw new AuthenticationException(); + } + + if (!authorization.startsWith(BASIC_TOKEN_PREFIX)) { + throw new AuthenticationException(); + } + } + + private String decodeToken(String authorization) { + String encodedToken = authorization.substring(BASIC_TOKEN_PREFIX.length()); + + if (encodedToken.isBlank()) { + throw new AuthenticationException(); + } + return new String(Base64Utils.decodeFromString(encodedToken), StandardCharsets.UTF_8); + } + + private Authentication createAuthentication(String decodedToken) { + String[] emailAndPassword = decodedToken.split(":"); + + if (emailAndPassword.length != 2) { + throw new AuthenticationException(); + } + + return UsernamePasswordAuthenticationToken.unauthenticated(emailAndPassword[0], + emailAndPassword[1]); + } + + private void validateAuthentication(Authentication authentication) { + if (authentication == null) { + throw new AuthenticationException(); + } + + if (!authentication.isAuthenticated()) { + throw new AuthenticationException(); + } + } +} diff --git a/src/main/java/nextstep/security/SecurityConstants.java b/src/main/java/nextstep/security/SecurityConstants.java new file mode 100644 index 00000000..8304f80e --- /dev/null +++ b/src/main/java/nextstep/security/SecurityConstants.java @@ -0,0 +1,13 @@ +package nextstep.security; + +public class SecurityConstants { + + private SecurityConstants() { + throw new IllegalStateException("Utility class"); + } + + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + public static final String BASIC_TOKEN_PREFIX = "Basic "; + +} diff --git a/src/main/java/nextstep/security/UsernamePasswordAuthenticationFilter.java b/src/main/java/nextstep/security/UsernamePasswordAuthenticationFilter.java new file mode 100644 index 00000000..e3ca7948 --- /dev/null +++ b/src/main/java/nextstep/security/UsernamePasswordAuthenticationFilter.java @@ -0,0 +1,73 @@ +package nextstep.security; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import nextstep.app.ui.AuthenticationException; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; +import nextstep.security.context.SecurityContext; +import nextstep.security.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +public class UsernamePasswordAuthenticationFilter extends OncePerRequestFilter { + + private final AuthenticationManager authenticationManager; + + private final List ACCEPTED_URIS = List.of( + "/login" + ); + + public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + if (!ACCEPTED_URIS.contains(request.getRequestURI())) { + filterChain.doFilter(request, response); + return; + } + + try { + Map paramMap = request.getParameterMap(); + + Authentication authentication = authenticationManager.authenticate( + createAuthentication(paramMap)); + + validateAuthentication(authentication); + + SecurityContext ctx = SecurityContextHolder.createEmptyContext(); + ctx.setAuthentication(authentication); + SecurityContextHolder.setContext(ctx); + + filterChain.doFilter(request, response); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + SecurityContextHolder.clearContext(); + } + } + + private Authentication createAuthentication(Map paramMap) { + return UsernamePasswordAuthenticationToken.unauthenticated( + paramMap.get("username")[0], + paramMap.get("password")[0]); + } + + private void validateAuthentication(Authentication authentication) { + if (authentication == null) { + throw new AuthenticationException(); + } + + if (!authentication.isAuthenticated()) { + throw new AuthenticationException(); + } + } + +} diff --git a/src/main/java/nextstep/security/authentication/Authentication.java b/src/main/java/nextstep/security/authentication/Authentication.java new file mode 100644 index 00000000..26f466f6 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/Authentication.java @@ -0,0 +1,10 @@ +package nextstep.security.authentication; + +public interface Authentication { + + Object getPrincipal(); + + Object getCredentials(); + + boolean isAuthenticated(); +} diff --git a/src/main/java/nextstep/security/authentication/AuthenticationManager.java b/src/main/java/nextstep/security/authentication/AuthenticationManager.java new file mode 100644 index 00000000..47973f66 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationManager.java @@ -0,0 +1,7 @@ +package nextstep.security.authentication; + +public interface AuthenticationManager { + + Authentication authenticate(Authentication authentication); + +} diff --git a/src/main/java/nextstep/security/authentication/AuthenticationProvider.java b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java new file mode 100644 index 00000000..37c69738 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java @@ -0,0 +1,9 @@ +package nextstep.security.authentication; + +public interface AuthenticationProvider { + + Authentication authenticate(Authentication authentication); + + boolean supports(Class authentication); + +} diff --git a/src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java b/src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java new file mode 100644 index 00000000..81c4a726 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java @@ -0,0 +1,35 @@ +package nextstep.security.authentication; + +import nextstep.app.service.UserDetail; +import nextstep.app.service.UserDetailService; + +public class DaoAuthenticationProvider implements AuthenticationProvider { + + private final UserDetailService userDetailService; + + public DaoAuthenticationProvider(UserDetailService userDetailService) { + this.userDetailService = userDetailService; + } + + @Override + public Authentication authenticate(Authentication authentication) { + UserDetail userDetail = userDetailService.findUserDetail( + authentication.getPrincipal().toString()); + + if (userDetail == null) { + return null; + } + + if (!userDetail.getPassword().equals(authentication.getCredentials().toString())) { + return null; + } + + return UsernamePasswordAuthenticationToken.authenticated( + userDetail.getEmail(), userDetail.getPassword()); + } + + @Override + public boolean supports(Class authentication) { + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/nextstep/security/authentication/ProviderManager.java b/src/main/java/nextstep/security/authentication/ProviderManager.java new file mode 100644 index 00000000..7c160cd0 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/ProviderManager.java @@ -0,0 +1,27 @@ +package nextstep.security.authentication; + +import java.util.List; + +public class ProviderManager implements AuthenticationManager { + + private final List authenticationProviders; + + public ProviderManager(List authenticationProviders) { + this.authenticationProviders = authenticationProviders; + } + + @Override + public Authentication authenticate(Authentication authentication) { + for (AuthenticationProvider provider : authenticationProviders) { + + if (!provider.supports(authentication.getClass())) { + continue; + } + + return provider.authenticate(authentication); + } + + return null; + } + +} diff --git a/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java new file mode 100644 index 00000000..d20b8f68 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java @@ -0,0 +1,40 @@ +package nextstep.security.authentication; + +public class UsernamePasswordAuthenticationToken implements Authentication { + + private final Object principal; + private final Object credentials; + private final boolean authenticated; + + private UsernamePasswordAuthenticationToken(Object principal, Object credentials, + boolean authenticated) { + this.principal = principal; + this.credentials = credentials; + this.authenticated = authenticated; + } + + public static UsernamePasswordAuthenticationToken unauthenticated(Object email, + Object password) { + return new UsernamePasswordAuthenticationToken(email, password, false); + } + + public static UsernamePasswordAuthenticationToken authenticated(Object email, Object password) { + return new UsernamePasswordAuthenticationToken(email, password, true); + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public boolean isAuthenticated() { + return authenticated; + } + +} diff --git a/src/main/java/nextstep/security/context/SecurityContext.java b/src/main/java/nextstep/security/context/SecurityContext.java new file mode 100644 index 00000000..0cd3560a --- /dev/null +++ b/src/main/java/nextstep/security/context/SecurityContext.java @@ -0,0 +1,24 @@ +package nextstep.security.context; + +import nextstep.security.authentication.Authentication; + +public class SecurityContext { + + private Authentication authentication; + + public SecurityContext() { + } + + public SecurityContext(Authentication authentication) { + this.authentication = authentication; + } + + public Authentication getAuthentication() { + return authentication; + } + + public void setAuthentication(Authentication authentication) { + this.authentication = authentication; + } + +} diff --git a/src/main/java/nextstep/security/context/SecurityContextHolder.java b/src/main/java/nextstep/security/context/SecurityContextHolder.java new file mode 100644 index 00000000..705ecf82 --- /dev/null +++ b/src/main/java/nextstep/security/context/SecurityContextHolder.java @@ -0,0 +1,32 @@ +package nextstep.security.context; + +public class SecurityContextHolder { + + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + + public static void clearContext() { + contextHolder.remove(); + } + + public static SecurityContext getContext() { + SecurityContext ctx = contextHolder.get(); + + if (ctx == null) { + ctx = createEmptyContext(); + contextHolder.set(ctx); + } + + return ctx; + } + + public static void setContext(SecurityContext context) { + if (context != null) { + contextHolder.set(context); + } + } + + public static SecurityContext createEmptyContext() { + return new SecurityContext(); + } + +} diff --git a/src/main/java/nextstep/security/filter/DefaultSecurityFilterChain.java b/src/main/java/nextstep/security/filter/DefaultSecurityFilterChain.java new file mode 100644 index 00000000..6d47995a --- /dev/null +++ b/src/main/java/nextstep/security/filter/DefaultSecurityFilterChain.java @@ -0,0 +1,25 @@ +package nextstep.security.filter; + +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; + +public class DefaultSecurityFilterChain implements SecurityFilterChain { + + private List filters; + + public DefaultSecurityFilterChain(List filters) { + this.filters = filters; + } + + @Override + public boolean matches(HttpServletRequest request) { + // todo 구현 예정 + return true; + } + + @Override + public List getFilters() { + return filters; + } +} diff --git a/src/main/java/nextstep/security/filter/DelegatingFilterProxy.java b/src/main/java/nextstep/security/filter/DelegatingFilterProxy.java new file mode 100644 index 00000000..038e0880 --- /dev/null +++ b/src/main/java/nextstep/security/filter/DelegatingFilterProxy.java @@ -0,0 +1,25 @@ +package nextstep.security.filter; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import org.springframework.web.filter.GenericFilterBean; + +public class DelegatingFilterProxy extends GenericFilterBean { + + private final Filter delegate; + + public DelegatingFilterProxy(Filter delegate) { + this.delegate = delegate; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) throws IOException, ServletException { + delegate.doFilter(servletRequest, servletResponse, filterChain); + } + +} diff --git a/src/main/java/nextstep/security/filter/FilterChainProxy.java b/src/main/java/nextstep/security/filter/FilterChainProxy.java new file mode 100644 index 00000000..818c363e --- /dev/null +++ b/src/main/java/nextstep/security/filter/FilterChainProxy.java @@ -0,0 +1,79 @@ +package nextstep.security.filter; + +import java.io.IOException; +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import org.springframework.web.filter.GenericFilterBean; + +public class FilterChainProxy extends GenericFilterBean { + + // 필터가 아닌 필터 체인을 목록으로 가지는 이유? + private final List filterChains; + + public FilterChainProxy(List filterChains) { + this.filterChains = filterChains; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) throws IOException, ServletException { + // 필터 체인에서 일치하는 필터 목록을 가져온다. + List filters = getFilterChain(servletRequest); + + // 필터 체인과 찾은 목록으로 버츄얼 필터체인을 생성한다. + VirtualFilterChain virtualFilterChain = new VirtualFilterChain(filterChain, filters); + + // 생성한 버츄얼 필터 체인이 실행된다. + virtualFilterChain.doFilter(servletRequest, servletResponse); + } + + private static final class VirtualFilterChain implements FilterChain { + + private final FilterChain originalChain; + private final List additionalFilters; + + private final int size; + private int currentPosition = 0; + + private VirtualFilterChain(FilterChain originalChain, List additionalFilters) { + this.originalChain = originalChain; + this.additionalFilters = additionalFilters; + this.size = additionalFilters.size(); + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + + // 인덱스와 필터 목록의 갯수를 비교한다. + if (this.currentPosition == this.size) { + this.originalChain.doFilter(servletRequest, servletResponse); + return; + } + + // 인덱스를 한칸 이동 + this.currentPosition++; + + // 실행할 필터를 선택 + Filter nextFilter = additionalFilters.get(currentPosition - 1); + + // 필터를 실행한다. + nextFilter.doFilter(servletRequest, servletResponse, this); + } + } + + private List getFilterChain(ServletRequest servletRequest) { + for (SecurityFilterChain chain : filterChains) { + if (chain.matches((HttpServletRequest) servletRequest)) { + return chain.getFilters(); + } + } + return null; + } + +} diff --git a/src/main/java/nextstep/security/filter/SecurityFilterChain.java b/src/main/java/nextstep/security/filter/SecurityFilterChain.java new file mode 100644 index 00000000..80ae35b2 --- /dev/null +++ b/src/main/java/nextstep/security/filter/SecurityFilterChain.java @@ -0,0 +1,13 @@ +package nextstep.security.filter; + +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; + +public interface SecurityFilterChain { + + boolean matches(HttpServletRequest request); + + List getFilters(); + +} \ No newline at end of file diff --git a/src/test/java/nextstep/app/LoginTest.java b/src/test/java/nextstep/app/LoginTest.java index 717bcc8a..31efcf26 100644 --- a/src/test/java/nextstep/app/LoginTest.java +++ b/src/test/java/nextstep/app/LoginTest.java @@ -1,7 +1,13 @@ package nextstep.app; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import nextstep.app.domain.Member; import nextstep.app.infrastructure.InmemoryMemberRepository; +import nextstep.security.authentication.Authentication; +import nextstep.security.context.SecurityContextHolder; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -9,18 +15,12 @@ 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.MvcResult; import org.springframework.test.web.servlet.ResultActions; -import javax.servlet.http.HttpSession; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @SpringBootTest @AutoConfigureMockMvc class LoginTest { + private static final Member TEST_MEMBER = InmemoryMemberRepository.TEST_MEMBER_1; @Autowired @@ -37,9 +37,9 @@ void login_success() throws Exception { loginResponse.andExpect(status().isOk()); - HttpSession session = loginResponse.andReturn().getRequest().getSession(); - assertThat(session).isNotNull(); - assertThat(session.getAttribute("SPRING_SECURITY_CONTEXT")).isNotNull(); + // SecurityContextHolder 로 변경 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + assertThat(authentication).isNotNull(); } @DisplayName("로그인 실패 - 사용자 없음")