diff --git a/build.gradle b/build.gradle index 99766160..64ecd927 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { group = 'nextstep' version = '0.0.1-SNAPSHOT' -sourceCompatibility = '11' +sourceCompatibility = "16" repositories { mavenCentral() @@ -14,9 +14,17 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + + // lombok + compileOnly "org.projectlombok:lombok" + testCompileOnly "org.projectlombok:lombok" + annotationProcessor "org.projectlombok:lombok" + testAnnotationProcessor "org.projectlombok:lombok" + testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { useJUnitPlatform() } +targetCompatibility = JavaVersion.VERSION_16 diff --git a/src/main/java/nextstep/app/application/CustomUserDetailsService.java b/src/main/java/nextstep/app/application/CustomUserDetailsService.java new file mode 100644 index 00000000..5d6676c8 --- /dev/null +++ b/src/main/java/nextstep/app/application/CustomUserDetailsService.java @@ -0,0 +1,33 @@ +package nextstep.app.application; + +import nextstep.app.domain.MemberRepository; +import nextstep.security.userdetils.UserDetails; +import nextstep.security.userdetils.UserDetailsService; +import org.springframework.stereotype.Service; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + public CustomUserDetailsService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) { + return memberRepository.findByEmail(username) + .map(member -> new UserDetails() { + + @Override + public String getUsername() { + return member.getEmail(); + } + + @Override + public String getPassword() { + return member.getPassword(); + } + }).orElse(null); + } +} diff --git a/src/main/java/nextstep/app/domain/MemberRepository.java b/src/main/java/nextstep/app/domain/MemberRepository.java index 2eb5cdbb..c82694ef 100644 --- a/src/main/java/nextstep/app/domain/MemberRepository.java +++ b/src/main/java/nextstep/app/domain/MemberRepository.java @@ -4,6 +4,7 @@ import java.util.Optional; public interface MemberRepository { + Optional findByEmail(String email); List findAll(); diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java deleted file mode 100644 index 0ea94f1b..00000000 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ /dev/null @@ -1,32 +0,0 @@ -package nextstep.app.ui; - -import nextstep.app.domain.MemberRepository; -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) { - return ResponseEntity.ok().build(); - } - - @ExceptionHandler(AuthenticationException.class) - public ResponseEntity handleAuthenticationException() { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } -} diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index c8cc74d6..6e6e1910 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -2,7 +2,9 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -23,4 +25,8 @@ public ResponseEntity> list() { return ResponseEntity.ok(members); } + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException() { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } } diff --git a/src/main/java/nextstep/app/ui/WebConfig.java b/src/main/java/nextstep/app/ui/WebConfig.java new file mode 100644 index 00000000..5bffdb0d --- /dev/null +++ b/src/main/java/nextstep/app/ui/WebConfig.java @@ -0,0 +1,60 @@ +package nextstep.app.ui; + +import java.util.Collections; +import java.util.List; +import lombok.RequiredArgsConstructor; +import nextstep.security.SecurityContextHolderFilter; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.DaoAuthenticationProvider; +import nextstep.security.authentication.ProviderManager; +import nextstep.security.filter.BasicAuthenticationSecurityFilter; +import nextstep.security.filter.DefaultSecurityFilterChain; +import nextstep.security.filter.DelegatingFilterProxy; +import nextstep.security.filter.FilterChainProxy; +import nextstep.security.filter.FormLoginAuthenticationFilter; +import nextstep.security.filter.SecurityFilterChain; +import nextstep.security.userdetils.UserDetailsService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@RequiredArgsConstructor +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final UserDetailsService userDetailsService; + + @Bean + public DelegatingFilterProxy delegatingFilterProxy( + AuthenticationManager authenticationManager + ) { + return new DelegatingFilterProxy( + filterChainProxy(List.of(securityFilterChain(authenticationManager)) + )); + } + + @Bean + public FilterChainProxy filterChainProxy(List securityFilterChainList) { + return new FilterChainProxy(securityFilterChainList); + } + + @Bean + public AuthenticationManager authenticationManager() { + return new ProviderManager(Collections.singletonList(daoAuthenticationProvider())); + } + + @Bean + public DaoAuthenticationProvider daoAuthenticationProvider() { + return new DaoAuthenticationProvider(userDetailsService); + } + + @Bean + public SecurityFilterChain securityFilterChain(AuthenticationManager authenticationManager) { + return new DefaultSecurityFilterChain( + List.of( + new SecurityContextHolderFilter(), + new FormLoginAuthenticationFilter(authenticationManager), + new BasicAuthenticationSecurityFilter(authenticationManager)) + ); + } +} diff --git a/src/main/java/nextstep/security/HttpSessionSecurityContextRepository.java b/src/main/java/nextstep/security/HttpSessionSecurityContextRepository.java new file mode 100644 index 00000000..c0de756a --- /dev/null +++ b/src/main/java/nextstep/security/HttpSessionSecurityContextRepository.java @@ -0,0 +1,29 @@ +package nextstep.security; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +public class HttpSessionSecurityContextRepository { + + private final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + public SecurityContext loadContext(HttpServletRequest request) { + HttpSession session = request.getSession(false); + + if (session == null) { + return null; + } + + return (SecurityContext) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY); + } + + private void saveContext( + SecurityContext context, + HttpServletRequest request, + HttpServletResponse response + ) { + HttpSession session = request.getSession(); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context); + } +} diff --git a/src/main/java/nextstep/security/SecurityContext.java b/src/main/java/nextstep/security/SecurityContext.java new file mode 100644 index 00000000..3eb0043e --- /dev/null +++ b/src/main/java/nextstep/security/SecurityContext.java @@ -0,0 +1,18 @@ +package nextstep.security; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import nextstep.security.authentication.Authentication; + +@Setter +@Getter +@NoArgsConstructor +public class SecurityContext { + + private Authentication authentication; + + public SecurityContext(Authentication authentication) { + this.authentication = authentication; + } +} diff --git a/src/main/java/nextstep/security/SecurityContextHolder.java b/src/main/java/nextstep/security/SecurityContextHolder.java new file mode 100644 index 00000000..daa31362 --- /dev/null +++ b/src/main/java/nextstep/security/SecurityContextHolder.java @@ -0,0 +1,35 @@ +package nextstep.security; + +public class SecurityContextHolder { + + private static final ThreadLocal contextHolder; + + static { + 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/SecurityContextHolderFilter.java b/src/main/java/nextstep/security/SecurityContextHolderFilter.java new file mode 100644 index 00000000..376b70be --- /dev/null +++ b/src/main/java/nextstep/security/SecurityContextHolderFilter.java @@ -0,0 +1,25 @@ +package nextstep.security; + +import java.io.IOException; +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 SecurityContextHolderFilter extends GenericFilterBean { + + private final HttpSessionSecurityContextRepository sessionSecurityContextRepository = new HttpSessionSecurityContextRepository(); + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + SecurityContext context = this.sessionSecurityContextRepository.loadContext((HttpServletRequest) request); + SecurityContextHolder.setContext(context); + + chain.doFilter(request, response); + + SecurityContextHolder.clearContext(); + } +} 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..ed153308 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/Authentication.java @@ -0,0 +1,10 @@ +package nextstep.security.authentication; + +public interface Authentication { + + Object getCredentials(); + + Object getPrincipal(); + + 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..17fc6760 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationManager.java @@ -0,0 +1,6 @@ +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..ea53cff7 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java @@ -0,0 +1,8 @@ +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..27e88bf1 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java @@ -0,0 +1,34 @@ +package nextstep.security.authentication; + +import nextstep.app.ui.AuthenticationException; +import nextstep.security.userdetils.UserDetails; +import nextstep.security.userdetils.UserDetailsService; + +public class DaoAuthenticationProvider implements AuthenticationProvider { + + private final UserDetailsService userDetailsService; + + public DaoAuthenticationProvider(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + UserDetails userDetails = userDetailsService.loadUserByUsername(authentication.getPrincipal().toString()); + + if (userDetails == null) { + throw new AuthenticationException(); + } + + if (!userDetails.getPassword().equals(authentication.getCredentials())) { + throw new AuthenticationException(); + } + + return UsernamePasswordAuthenticationToken.authenticated(userDetails, userDetails.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..46162f38 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/ProviderManager.java @@ -0,0 +1,24 @@ +package nextstep.security.authentication; + +import java.util.List; + +public class ProviderManager implements AuthenticationManager { + private List providers; + + public ProviderManager(List providers) { + this.providers = providers; + } + + @Override + public Authentication authenticate(Authentication authentication) { + for (AuthenticationProvider provider : providers) { + if (!provider.supports(authentication.getClass())) { + continue; + } + Authentication result = provider.authenticate(authentication); + return result; + } + + 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..45955a14 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java @@ -0,0 +1,36 @@ +package nextstep.security.authentication; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class UsernamePasswordAuthenticationToken implements Authentication { + + private final Object principal; + private Object credentials; + private boolean authenticated; + + public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, + Object credentials) { + return new UsernamePasswordAuthenticationToken(principal, credentials, false); + } + + public static UsernamePasswordAuthenticationToken authenticated(Object principal, + Object credentials) { + return new UsernamePasswordAuthenticationToken(principal, credentials, true); + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public boolean isAuthenticated() { + return authenticated; + } +} diff --git a/src/main/java/nextstep/security/filter/BasicAuthenticationSecurityFilter.java b/src/main/java/nextstep/security/filter/BasicAuthenticationSecurityFilter.java new file mode 100644 index 00000000..271d73a5 --- /dev/null +++ b/src/main/java/nextstep/security/filter/BasicAuthenticationSecurityFilter.java @@ -0,0 +1,98 @@ +package nextstep.security.filter; + + +import java.io.IOException; +import java.util.Objects; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import nextstep.app.ui.AuthenticationException; +import nextstep.security.SecurityContext; +import nextstep.security.SecurityContextHolder; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.http.HttpHeaders; +import org.springframework.util.Base64Utils; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; + +public class BasicAuthenticationSecurityFilter extends GenericFilterBean { + + private final AuthenticationManager authenticationManager; + private static final String DEFAULT_REQUEST_URI = "/members"; + + public BasicAuthenticationSecurityFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (!DEFAULT_REQUEST_URI.equals(((HttpServletRequest) request).getRequestURI())) { + chain.doFilter(request, response); + return; + } + + try { + UsernamePasswordAuthenticationToken authRequest = createAuthentication( + (HttpServletRequest) request); + + Authentication authentication = authenticationManager.authenticate(authRequest); + + if (Objects.isNull(authentication) || !authentication.isAuthenticated()) { + ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(authentication); + SecurityContextHolder.setContext(context); + + chain.doFilter(request, response); + } catch (Exception e) { + ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } + + private UsernamePasswordAuthenticationToken createAuthentication(HttpServletRequest request) { + String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + if (!StringUtils.hasText(authorization)) { + return null; + } + + if (!checkBasicAuth(authorization)) { + return null; + } + + String credential = extractCredential(authorization); + String decodedCredential = new String(Base64Utils.decodeFromString(credential)); + String[] emailAndPassword = decodedCredential.split(":"); + + String email = emailAndPassword[0]; + String password = emailAndPassword[1]; + + return UsernamePasswordAuthenticationToken.unauthenticated(email, password); + } + + private boolean checkBasicAuth(String authorization) { + String[] split = authorization.split(" "); + if (split.length != 2) { + throw new AuthenticationException(); + } + + String type = split[0]; + return "Basic".equalsIgnoreCase(type); + } + + private String extractCredential(String authorization) { + String[] split = authorization.split(" "); + if (split.length != 2) { + throw new AuthenticationException(); + } + + return split[1]; + } +} 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..4bfed146 --- /dev/null +++ b/src/main/java/nextstep/security/filter/DefaultSecurityFilterChain.java @@ -0,0 +1,24 @@ +package nextstep.security.filter; + +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; + +public class DefaultSecurityFilterChain implements SecurityFilterChain { + + private final List filters; + + public DefaultSecurityFilterChain(List filters) { + this.filters = filters; + } + + @Override + public boolean matches(HttpServletRequest request) { + 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..566c3a4c --- /dev/null +++ b/src/main/java/nextstep/security/filter/DelegatingFilterProxy.java @@ -0,0 +1,22 @@ +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 lombok.AllArgsConstructor; +import org.springframework.web.filter.GenericFilterBean; + +@AllArgsConstructor +public class DelegatingFilterProxy extends GenericFilterBean { + + Filter delegate; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + delegate.doFilter(request, response, chain); + } +} 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..805730b5 --- /dev/null +++ b/src/main/java/nextstep/security/filter/FilterChainProxy.java @@ -0,0 +1,59 @@ +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 lombok.AllArgsConstructor; +import org.springframework.web.filter.GenericFilterBean; + +@AllArgsConstructor +public class FilterChainProxy extends GenericFilterBean { + + List securityFilterChainList; + + @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); + } + + private List getFilters(HttpServletRequest request) { + return securityFilterChainList.stream() + .filter(it -> it.matches(request)) + .flatMap(it -> it.getFilters().stream()).toList(); + } + + 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 request, ServletResponse response) + throws IOException, ServletException { + if (this.currentPosition == this.size) { + originalChain.doFilter(request, response); + return; + } + + this.currentPosition++; + Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1); + nextFilter.doFilter(request, response, this); + } + } +} diff --git a/src/main/java/nextstep/security/filter/FormLoginAuthenticationFilter.java b/src/main/java/nextstep/security/filter/FormLoginAuthenticationFilter.java new file mode 100644 index 00000000..4fb00798 --- /dev/null +++ b/src/main/java/nextstep/security/filter/FormLoginAuthenticationFilter.java @@ -0,0 +1,65 @@ +package nextstep.security.filter; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import nextstep.app.ui.AuthenticationException; +import nextstep.security.SecurityContext; +import nextstep.security.SecurityContextHolder; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.filter.GenericFilterBean; + +public class FormLoginAuthenticationFilter extends GenericFilterBean { + + private final AuthenticationManager authenticationManager; + private final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + private static final String DEFAULT_REQUEST_URI = "/login"; + + public FormLoginAuthenticationFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + if (!DEFAULT_REQUEST_URI.equals(((HttpServletRequest)request).getRequestURI())) { + chain.doFilter(request, response); + return; + } + + try { + Map paramMap = request.getParameterMap(); + String email = paramMap.get("username")[0]; + String password = paramMap.get("password")[0]; + + Authentication authentication = authenticationManager.authenticate( + UsernamePasswordAuthenticationToken.unauthenticated(email, password)); + + if (Objects.isNull(authentication) || !authentication.isAuthenticated()) { + throw new AuthenticationException(); + } + + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(authentication); + SecurityContextHolder.setContext(context); + + ((HttpServletRequest) request).getSession() + .setAttribute(SPRING_SECURITY_CONTEXT_KEY, authentication); + } catch (Exception e) { + ((HttpServletResponse)response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } +} 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..598fd879 --- /dev/null +++ b/src/main/java/nextstep/security/filter/SecurityFilterChain.java @@ -0,0 +1,12 @@ +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(); +} diff --git a/src/main/java/nextstep/security/userdetils/UserDetails.java b/src/main/java/nextstep/security/userdetils/UserDetails.java new file mode 100644 index 00000000..c192f46a --- /dev/null +++ b/src/main/java/nextstep/security/userdetils/UserDetails.java @@ -0,0 +1,8 @@ +package nextstep.security.userdetils; + +public interface UserDetails { + + String getUsername(); + + String getPassword(); +} diff --git a/src/main/java/nextstep/security/userdetils/UserDetailsService.java b/src/main/java/nextstep/security/userdetils/UserDetailsService.java new file mode 100644 index 00000000..2eb889dd --- /dev/null +++ b/src/main/java/nextstep/security/userdetils/UserDetailsService.java @@ -0,0 +1,6 @@ +package nextstep.security.userdetils; + +public interface UserDetailsService { + + UserDetails loadUserByUsername(String username); +} diff --git a/src/test/java/nextstep/app/MemberTest.java b/src/test/java/nextstep/app/MemberTest.java index 58aba17b..49a620e4 100644 --- a/src/test/java/nextstep/app/MemberTest.java +++ b/src/test/java/nextstep/app/MemberTest.java @@ -1,5 +1,7 @@ package nextstep.app; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpSession; import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import nextstep.app.infrastructure.InmemoryMemberRepository; @@ -11,11 +13,13 @@ 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 org.springframework.util.Base64Utils; import static org.hamcrest.Matchers.hasItem; 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.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -62,4 +66,27 @@ void members_fail() throws Exception { loginResponse.andDo(print()); loginResponse.andExpect(status().isUnauthorized()); } + + @DisplayName("로그인 후 세션을 통해 회원 목록 조회") + @Test + void login_after_members() throws Exception { + ResultActions loginResponse = mockMvc.perform(post("/login") + .param("username", TEST_MEMBER.getEmail()) + .param("password", TEST_MEMBER.getPassword()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ); + + loginResponse.andExpect(status().isOk()); + + MvcResult loginResult = loginResponse.andReturn(); + HttpSession session = loginResult.getRequest().getSession(); + String sessionId = session.getId(); + + ResultActions membersResponse = mockMvc.perform(get("/members") + .cookie(new Cookie("JSESSIONID", sessionId)) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ); + + membersResponse.andExpect(status().isOk()); + } }