diff --git a/build.gradle b/build.gradle index 22640138..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() @@ -27,3 +27,4 @@ dependencies { tasks.named('test') { useJUnitPlatform() } +targetCompatibility = JavaVersion.VERSION_16 diff --git a/src/main/java/nextstep/app/ui/WebConfig.java b/src/main/java/nextstep/app/ui/WebConfig.java index e898fa59..5bffdb0d 100644 --- a/src/main/java/nextstep/app/ui/WebConfig.java +++ b/src/main/java/nextstep/app/ui/WebConfig.java @@ -3,6 +3,7 @@ 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; @@ -50,7 +51,9 @@ public DaoAuthenticationProvider daoAuthenticationProvider() { @Bean public SecurityFilterChain securityFilterChain(AuthenticationManager authenticationManager) { return new DefaultSecurityFilterChain( - List.of(new FormLoginAuthenticationFilter(authenticationManager), + 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/filter/BasicAuthenticationSecurityFilter.java b/src/main/java/nextstep/security/filter/BasicAuthenticationSecurityFilter.java index ca1f3489..271d73a5 100644 --- a/src/main/java/nextstep/security/filter/BasicAuthenticationSecurityFilter.java +++ b/src/main/java/nextstep/security/filter/BasicAuthenticationSecurityFilter.java @@ -10,6 +10,8 @@ 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; @@ -45,6 +47,10 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha ((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); diff --git a/src/main/java/nextstep/security/filter/FilterChainProxy.java b/src/main/java/nextstep/security/filter/FilterChainProxy.java index af0ebd4c..805730b5 100644 --- a/src/main/java/nextstep/security/filter/FilterChainProxy.java +++ b/src/main/java/nextstep/security/filter/FilterChainProxy.java @@ -2,7 +2,6 @@ import java.io.IOException; import java.util.List; -import java.util.stream.Collectors; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -29,7 +28,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha private List getFilters(HttpServletRequest request) { return securityFilterChainList.stream() .filter(it -> it.matches(request)) - .flatMap(it -> it.getFilters().stream()).collect(Collectors.toList()); + .flatMap(it -> it.getFilters().stream()).toList(); } private static final class VirtualFilterChain implements FilterChain { diff --git a/src/main/java/nextstep/security/filter/FormLoginAuthenticationFilter.java b/src/main/java/nextstep/security/filter/FormLoginAuthenticationFilter.java index 51a763e9..4fb00798 100644 --- a/src/main/java/nextstep/security/filter/FormLoginAuthenticationFilter.java +++ b/src/main/java/nextstep/security/filter/FormLoginAuthenticationFilter.java @@ -10,6 +10,8 @@ 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; @@ -50,6 +52,10 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha 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) { 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()); + } }