From c490a8095a553d18723fb0875ff1c0dd2a8c6d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8C=E1=85=A1=E1=86=BC=E1=84=8C=E1=85=AE=E1=86=AB?= =?UTF-8?q?=E1=84=92=E1=85=B4?= Date: Sun, 27 Oct 2024 14:53:04 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20username/password=20login=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 ++++ .../application/CustomUserDetailsService.java | 33 +++++++++++++++++ .../nextstep/app/domain/MemberRepository.java | 1 + .../java/nextstep/app/ui/LoginController.java | 35 ++++++++++++++---- src/main/java/nextstep/app/ui/WebConfig.java | 28 +++++++++++++++ .../authentication/Authentication.java | 10 ++++++ .../authentication/AuthenticationManager.java | 6 ++++ .../AuthenticationProvider.java | 8 +++++ .../DaoAuthenticationProvider.java | 34 ++++++++++++++++++ .../authentication/ProviderManager.java | 24 +++++++++++++ .../UsernamePasswordAuthenticationToken.java | 36 +++++++++++++++++++ .../security/userdetils/UserDetails.java | 8 +++++ .../userdetils/UserDetailsService.java | 6 ++++ 13 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 src/main/java/nextstep/app/application/CustomUserDetailsService.java create mode 100644 src/main/java/nextstep/app/ui/WebConfig.java create mode 100644 src/main/java/nextstep/security/authentication/Authentication.java create mode 100644 src/main/java/nextstep/security/authentication/AuthenticationManager.java create mode 100644 src/main/java/nextstep/security/authentication/AuthenticationProvider.java create mode 100644 src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java create mode 100644 src/main/java/nextstep/security/authentication/ProviderManager.java create mode 100644 src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java create mode 100644 src/main/java/nextstep/security/userdetils/UserDetails.java create mode 100644 src/main/java/nextstep/security/userdetils/UserDetailsService.java diff --git a/build.gradle b/build.gradle index 9976616..2264013 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,13 @@ 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' } 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 0000000..5d6676c --- /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 2eb5cdb..c82694e 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 index 0ea94f1..6cd7085 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,6 +1,13 @@ package nextstep.app.ui; +import java.util.Map; +import java.util.Objects; +import javax.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; import nextstep.app.domain.MemberRepository; +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; @@ -8,21 +15,35 @@ import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; +@RequiredArgsConstructor @RestController public class LoginController { - public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - private final MemberRepository memberRepository; + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - public LoginController(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } + private final AuthenticationManager authenticationManager; @PostMapping("/login") public ResponseEntity login(HttpServletRequest request, HttpSession session) { - return ResponseEntity.ok().build(); + 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(); + } + + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, authentication); + + return ResponseEntity.ok().build(); + } catch (Exception e) { + throw new AuthenticationException(); + } } @ExceptionHandler(AuthenticationException.class) 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 0000000..b112b7f --- /dev/null +++ b/src/main/java/nextstep/app/ui/WebConfig.java @@ -0,0 +1,28 @@ +package nextstep.app.ui; + +import java.util.Collections; +import lombok.RequiredArgsConstructor; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.DaoAuthenticationProvider; +import nextstep.security.authentication.ProviderManager; +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 AuthenticationManager authenticationManager() { + return new ProviderManager(Collections.singletonList(daoAuthenticationProvider())); + } + + @Bean + public DaoAuthenticationProvider daoAuthenticationProvider() { + return new DaoAuthenticationProvider(userDetailsService); + } +} 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 0000000..ed15330 --- /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 0000000..17fc676 --- /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 0000000..ea53cff --- /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 0000000..27e88bf --- /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 0000000..46162f3 --- /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 0000000..45955a1 --- /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/userdetils/UserDetails.java b/src/main/java/nextstep/security/userdetils/UserDetails.java new file mode 100644 index 0000000..c192f46 --- /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 0000000..2eb889d --- /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); +} From d1d5408ebb2f5669e72aad2620931a0aefc6d3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8C=E1=85=A1=E1=86=BC=E1=84=8C=E1=85=AE=E1=86=AB?= =?UTF-8?q?=E1=84=92=E1=85=B4?= Date: Sun, 27 Oct 2024 15:10:36 +0900 Subject: [PATCH 2/6] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20F?= =?UTF-8?q?ormLoginAuthenticationInterceptor=20=EB=A1=9C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/ui/LoginController.java | 53 ------------------- src/main/java/nextstep/app/ui/WebConfig.java | 7 +++ .../FormLoginAuthenticationInterceptor.java | 43 +++++++++++++++ 3 files changed, 50 insertions(+), 53 deletions(-) delete mode 100644 src/main/java/nextstep/app/ui/LoginController.java create mode 100644 src/main/java/nextstep/security/authentication/FormLoginAuthenticationInterceptor.java 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 6cd7085..0000000 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ /dev/null @@ -1,53 +0,0 @@ -package nextstep.app.ui; - -import java.util.Map; -import java.util.Objects; -import javax.servlet.http.HttpSession; -import lombok.RequiredArgsConstructor; -import nextstep.app.domain.MemberRepository; -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.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RestController; - -import javax.servlet.http.HttpServletRequest; - -@RequiredArgsConstructor -@RestController -public class LoginController { - - public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - - private final AuthenticationManager authenticationManager; - - @PostMapping("/login") - public ResponseEntity login(HttpServletRequest request, HttpSession session) { - 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(); - } - - session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, authentication); - - return ResponseEntity.ok().build(); - } catch (Exception e) { - throw new AuthenticationException(); - } - } - - @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 index b112b7f..daad9e1 100644 --- a/src/main/java/nextstep/app/ui/WebConfig.java +++ b/src/main/java/nextstep/app/ui/WebConfig.java @@ -4,10 +4,12 @@ import lombok.RequiredArgsConstructor; import nextstep.security.authentication.AuthenticationManager; import nextstep.security.authentication.DaoAuthenticationProvider; +import nextstep.security.authentication.FormLoginAuthenticationInterceptor; import nextstep.security.authentication.ProviderManager; import nextstep.security.userdetils.UserDetailsService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @RequiredArgsConstructor @@ -16,6 +18,11 @@ public class WebConfig implements WebMvcConfigurer { private final UserDetailsService userDetailsService; + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new FormLoginAuthenticationInterceptor(authenticationManager())).addPathPatterns("/login"); + } + @Bean public AuthenticationManager authenticationManager() { return new ProviderManager(Collections.singletonList(daoAuthenticationProvider())); diff --git a/src/main/java/nextstep/security/authentication/FormLoginAuthenticationInterceptor.java b/src/main/java/nextstep/security/authentication/FormLoginAuthenticationInterceptor.java new file mode 100644 index 0000000..bf35d11 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/FormLoginAuthenticationInterceptor.java @@ -0,0 +1,43 @@ +package nextstep.security.authentication; + +import java.util.Map; +import java.util.Objects; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import nextstep.app.ui.AuthenticationException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.servlet.HandlerInterceptor; + +@RequiredArgsConstructor +public class FormLoginAuthenticationInterceptor implements HandlerInterceptor { + + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + private final AuthenticationManager authenticationManager; + + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) { + 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(); + } + + request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, authentication); + + return false; + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + } +} From abf9791d8664139f859839661d9edb6022b118ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8C=E1=85=A1=E1=86=BC=E1=84=8C=E1=85=AE=E1=86=AB?= =?UTF-8?q?=E1=84=92=E1=85=B4?= Date: Sun, 27 Oct 2024 15:36:46 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20BasicAuthenticationInterceptor=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/app/ui/MemberController.java | 1 - src/main/java/nextstep/app/ui/WebConfig.java | 2 + .../BasicAuthenticationInterceptor.java | 76 +++++++++++++++++++ .../FormLoginAuthenticationInterceptor.java | 2 - 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 src/main/java/nextstep/security/authentication/BasicAuthenticationInterceptor.java diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index c8cc74d..b226762 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -22,5 +22,4 @@ public ResponseEntity> list() { List members = memberRepository.findAll(); return ResponseEntity.ok(members); } - } diff --git a/src/main/java/nextstep/app/ui/WebConfig.java b/src/main/java/nextstep/app/ui/WebConfig.java index daad9e1..ad34766 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 lombok.RequiredArgsConstructor; import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.BasicAuthenticationInterceptor; import nextstep.security.authentication.DaoAuthenticationProvider; import nextstep.security.authentication.FormLoginAuthenticationInterceptor; import nextstep.security.authentication.ProviderManager; @@ -21,6 +22,7 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new FormLoginAuthenticationInterceptor(authenticationManager())).addPathPatterns("/login"); + registry.addInterceptor(new BasicAuthenticationInterceptor(authenticationManager())); } @Bean diff --git a/src/main/java/nextstep/security/authentication/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/authentication/BasicAuthenticationInterceptor.java new file mode 100644 index 0000000..d0d7784 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/BasicAuthenticationInterceptor.java @@ -0,0 +1,76 @@ +package nextstep.security.authentication; + +import java.util.Objects; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import nextstep.app.ui.AuthenticationException; +import org.springframework.http.HttpHeaders; +import org.springframework.util.Base64Utils; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.HandlerInterceptor; + +public class BasicAuthenticationInterceptor implements HandlerInterceptor { + + private final AuthenticationManager authenticationManager; + + public BasicAuthenticationInterceptor(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + try { + UsernamePasswordAuthenticationToken authRequest = createAuthentication(request); + + Authentication authentication = authenticationManager.authenticate(authRequest); + + if (Objects.isNull(authentication) || !authentication.isAuthenticated()) { + throw new AuthenticationException(); + } + + return true; + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + } + + 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/authentication/FormLoginAuthenticationInterceptor.java b/src/main/java/nextstep/security/authentication/FormLoginAuthenticationInterceptor.java index bf35d11..211ee0b 100644 --- a/src/main/java/nextstep/security/authentication/FormLoginAuthenticationInterceptor.java +++ b/src/main/java/nextstep/security/authentication/FormLoginAuthenticationInterceptor.java @@ -6,7 +6,6 @@ import javax.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import nextstep.app.ui.AuthenticationException; -import org.springframework.http.ResponseEntity; import org.springframework.web.servlet.HandlerInterceptor; @RequiredArgsConstructor @@ -16,7 +15,6 @@ public class FormLoginAuthenticationInterceptor implements HandlerInterceptor { private final AuthenticationManager authenticationManager; - @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { From 5ceb33f1a556778d6da9e2cea9d3c78dd2bf74c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8C=E1=85=A1=E1=86=BC=E1=84=8C=E1=85=AE=E1=86=AB?= =?UTF-8?q?=E1=84=92=E1=85=B4?= Date: Mon, 4 Nov 2024 15:04:53 +0900 Subject: [PATCH 4/6] step1 --- src/main/java/nextstep/app/ui/WebConfig.java | 13 +++ .../BasicAuthenticationSecurityFilter.java | 79 +++++++++++++++++++ .../filter/DefaultSecurityFilterChain.java | 24 ++++++ .../filter/DelegatingFilterProxy.java | 20 +++++ .../security/filter/FilterChainProxy.java | 58 ++++++++++++++ .../filter/FormLoginAuthenticationFilter.java | 44 +++++++++++ .../security/filter/SecurityFilterChain.java | 12 +++ 7 files changed, 250 insertions(+) create mode 100644 src/main/java/nextstep/security/filter/BasicAuthenticationSecurityFilter.java create mode 100644 src/main/java/nextstep/security/filter/DefaultSecurityFilterChain.java create mode 100644 src/main/java/nextstep/security/filter/DelegatingFilterProxy.java create mode 100644 src/main/java/nextstep/security/filter/FilterChainProxy.java create mode 100644 src/main/java/nextstep/security/filter/FormLoginAuthenticationFilter.java create mode 100644 src/main/java/nextstep/security/filter/SecurityFilterChain.java diff --git a/src/main/java/nextstep/app/ui/WebConfig.java b/src/main/java/nextstep/app/ui/WebConfig.java index ad34766..f15f3be 100644 --- a/src/main/java/nextstep/app/ui/WebConfig.java +++ b/src/main/java/nextstep/app/ui/WebConfig.java @@ -1,12 +1,17 @@ package nextstep.app.ui; import java.util.Collections; +import java.util.List; import lombok.RequiredArgsConstructor; import nextstep.security.authentication.AuthenticationManager; import nextstep.security.authentication.BasicAuthenticationInterceptor; import nextstep.security.authentication.DaoAuthenticationProvider; import nextstep.security.authentication.FormLoginAuthenticationInterceptor; import nextstep.security.authentication.ProviderManager; +import nextstep.security.filter.BasicAuthenticationSecurityFilter; +import nextstep.security.filter.DefaultSecurityFilterChain; +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; @@ -34,4 +39,12 @@ public AuthenticationManager authenticationManager() { public DaoAuthenticationProvider daoAuthenticationProvider() { return new DaoAuthenticationProvider(userDetailsService); } + + @Bean + public SecurityFilterChain securityFilterChain(AuthenticationManager authenticationManager){ + return new DefaultSecurityFilterChain( + List.of(new FormLoginAuthenticationFilter(authenticationManager), + new BasicAuthenticationSecurityFilter(authenticationManager)) + ); + } } 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 0000000..e000849 --- /dev/null +++ b/src/main/java/nextstep/security/filter/BasicAuthenticationSecurityFilter.java @@ -0,0 +1,79 @@ +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.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; + + public BasicAuthenticationSecurityFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + UsernamePasswordAuthenticationToken authRequest = createAuthentication( + (HttpServletRequest) request); + + Authentication authentication = authenticationManager.authenticate(authRequest); + + if (Objects.isNull(authentication) || !authentication.isAuthenticated()) { + throw new AuthenticationException(); + } + } + + 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 0000000..4bfed14 --- /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 0000000..88a91f3 --- /dev/null +++ b/src/main/java/nextstep/security/filter/DelegatingFilterProxy.java @@ -0,0 +1,20 @@ +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 { + + Filter delegate; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + } +} 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 0000000..75caa45 --- /dev/null +++ b/src/main/java/nextstep/security/filter/FilterChainProxy.java @@ -0,0 +1,58 @@ +package nextstep.security.filter; + +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; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import org.springframework.web.filter.GenericFilterBean; + +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()).collect(Collectors.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 0000000..cb3671f --- /dev/null +++ b/src/main/java/nextstep/security/filter/FormLoginAuthenticationFilter.java @@ -0,0 +1,44 @@ +package nextstep.security.filter; + +import static nextstep.security.authentication.FormLoginAuthenticationInterceptor.SPRING_SECURITY_CONTEXT_KEY; + +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 nextstep.app.ui.AuthenticationException; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.web.filter.GenericFilterBean; + +public class FormLoginAuthenticationFilter extends GenericFilterBean { + + private final AuthenticationManager authenticationManager; + + public FormLoginAuthenticationFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + 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(); + } + + ((HttpServletRequest) request).getSession() + .setAttribute(SPRING_SECURITY_CONTEXT_KEY, authentication); + } +} 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 0000000..598fd87 --- /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(); +} From 3deb97bb901693619f0a770f4980c32527c79682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8C=E1=85=A1=E1=86=BC=E1=84=8C=E1=85=AE=E1=86=AB?= =?UTF-8?q?=E1=84=92=E1=85=B4?= Date: Mon, 4 Nov 2024 15:37:10 +0900 Subject: [PATCH 5/6] step 2-2 --- .../nextstep/app/ui/MemberController.java | 7 ++ src/main/java/nextstep/app/ui/WebConfig.java | 23 ++++-- .../BasicAuthenticationInterceptor.java | 76 ------------------- .../FormLoginAuthenticationInterceptor.java | 41 ---------- .../BasicAuthenticationSecurityFilter.java | 23 ++++-- .../filter/DelegatingFilterProxy.java | 4 +- .../security/filter/FilterChainProxy.java | 2 + .../filter/FormLoginAuthenticationFilter.java | 39 +++++++--- 8 files changed, 72 insertions(+), 143 deletions(-) delete mode 100644 src/main/java/nextstep/security/authentication/BasicAuthenticationInterceptor.java delete mode 100644 src/main/java/nextstep/security/authentication/FormLoginAuthenticationInterceptor.java diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index b226762..6e6e191 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; @@ -22,4 +24,9 @@ public ResponseEntity> list() { List members = memberRepository.findAll(); 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 index f15f3be..e898fa5 100644 --- a/src/main/java/nextstep/app/ui/WebConfig.java +++ b/src/main/java/nextstep/app/ui/WebConfig.java @@ -4,18 +4,17 @@ import java.util.List; import lombok.RequiredArgsConstructor; import nextstep.security.authentication.AuthenticationManager; -import nextstep.security.authentication.BasicAuthenticationInterceptor; import nextstep.security.authentication.DaoAuthenticationProvider; -import nextstep.security.authentication.FormLoginAuthenticationInterceptor; 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.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @RequiredArgsConstructor @@ -24,10 +23,18 @@ public class WebConfig implements WebMvcConfigurer { private final UserDetailsService userDetailsService; - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new FormLoginAuthenticationInterceptor(authenticationManager())).addPathPatterns("/login"); - registry.addInterceptor(new BasicAuthenticationInterceptor(authenticationManager())); + @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 @@ -41,7 +48,7 @@ public DaoAuthenticationProvider daoAuthenticationProvider() { } @Bean - public SecurityFilterChain securityFilterChain(AuthenticationManager authenticationManager){ + public SecurityFilterChain securityFilterChain(AuthenticationManager authenticationManager) { return new DefaultSecurityFilterChain( List.of(new FormLoginAuthenticationFilter(authenticationManager), new BasicAuthenticationSecurityFilter(authenticationManager)) diff --git a/src/main/java/nextstep/security/authentication/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/authentication/BasicAuthenticationInterceptor.java deleted file mode 100644 index d0d7784..0000000 --- a/src/main/java/nextstep/security/authentication/BasicAuthenticationInterceptor.java +++ /dev/null @@ -1,76 +0,0 @@ -package nextstep.security.authentication; - -import java.util.Objects; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import nextstep.app.ui.AuthenticationException; -import org.springframework.http.HttpHeaders; -import org.springframework.util.Base64Utils; -import org.springframework.util.StringUtils; -import org.springframework.web.servlet.HandlerInterceptor; - -public class BasicAuthenticationInterceptor implements HandlerInterceptor { - - private final AuthenticationManager authenticationManager; - - public BasicAuthenticationInterceptor(AuthenticationManager authenticationManager) { - this.authenticationManager = authenticationManager; - } - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - try { - UsernamePasswordAuthenticationToken authRequest = createAuthentication(request); - - Authentication authentication = authenticationManager.authenticate(authRequest); - - if (Objects.isNull(authentication) || !authentication.isAuthenticated()) { - throw new AuthenticationException(); - } - - return true; - } catch (Exception e) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return false; - } - } - - 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/authentication/FormLoginAuthenticationInterceptor.java b/src/main/java/nextstep/security/authentication/FormLoginAuthenticationInterceptor.java deleted file mode 100644 index 211ee0b..0000000 --- a/src/main/java/nextstep/security/authentication/FormLoginAuthenticationInterceptor.java +++ /dev/null @@ -1,41 +0,0 @@ -package nextstep.security.authentication; - -import java.util.Map; -import java.util.Objects; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import nextstep.app.ui.AuthenticationException; -import org.springframework.web.servlet.HandlerInterceptor; - -@RequiredArgsConstructor -public class FormLoginAuthenticationInterceptor implements HandlerInterceptor { - - public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - - private final AuthenticationManager authenticationManager; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, - Object handler) { - 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(); - } - - request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, authentication); - - return false; - } catch (Exception e) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return false; - } - } -} diff --git a/src/main/java/nextstep/security/filter/BasicAuthenticationSecurityFilter.java b/src/main/java/nextstep/security/filter/BasicAuthenticationSecurityFilter.java index e000849..ca1f348 100644 --- a/src/main/java/nextstep/security/filter/BasicAuthenticationSecurityFilter.java +++ b/src/main/java/nextstep/security/filter/BasicAuthenticationSecurityFilter.java @@ -1,5 +1,6 @@ package nextstep.security.filter; + import java.io.IOException; import java.util.Objects; import javax.servlet.FilterChain; @@ -20,6 +21,7 @@ public class BasicAuthenticationSecurityFilter extends GenericFilterBean { private final AuthenticationManager authenticationManager; + private static final String DEFAULT_REQUEST_URI = "/members"; public BasicAuthenticationSecurityFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; @@ -28,13 +30,24 @@ public BasicAuthenticationSecurityFilter(AuthenticationManager authenticationMan @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - UsernamePasswordAuthenticationToken authRequest = createAuthentication( - (HttpServletRequest) request); + if (!DEFAULT_REQUEST_URI.equals(((HttpServletRequest) request).getRequestURI())) { + chain.doFilter(request, response); + return; + } - Authentication authentication = authenticationManager.authenticate(authRequest); + try { + UsernamePasswordAuthenticationToken authRequest = createAuthentication( + (HttpServletRequest) request); - if (Objects.isNull(authentication) || !authentication.isAuthenticated()) { - throw new AuthenticationException(); + Authentication authentication = authenticationManager.authenticate(authRequest); + + if (Objects.isNull(authentication) || !authentication.isAuthenticated()) { + ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + chain.doFilter(request, response); + } catch (Exception e) { + ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); } } diff --git a/src/main/java/nextstep/security/filter/DelegatingFilterProxy.java b/src/main/java/nextstep/security/filter/DelegatingFilterProxy.java index 88a91f3..566c3a4 100644 --- a/src/main/java/nextstep/security/filter/DelegatingFilterProxy.java +++ b/src/main/java/nextstep/security/filter/DelegatingFilterProxy.java @@ -6,8 +6,10 @@ 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; @@ -15,6 +17,6 @@ public class DelegatingFilterProxy extends GenericFilterBean { @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 index 75caa45..af0ebd4 100644 --- a/src/main/java/nextstep/security/filter/FilterChainProxy.java +++ b/src/main/java/nextstep/security/filter/FilterChainProxy.java @@ -9,8 +9,10 @@ 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; diff --git a/src/main/java/nextstep/security/filter/FormLoginAuthenticationFilter.java b/src/main/java/nextstep/security/filter/FormLoginAuthenticationFilter.java index cb3671f..51a763e 100644 --- a/src/main/java/nextstep/security/filter/FormLoginAuthenticationFilter.java +++ b/src/main/java/nextstep/security/filter/FormLoginAuthenticationFilter.java @@ -1,7 +1,5 @@ package nextstep.security.filter; -import static nextstep.security.authentication.FormLoginAuthenticationInterceptor.SPRING_SECURITY_CONTEXT_KEY; - import java.io.IOException; import java.util.Map; import java.util.Objects; @@ -10,15 +8,22 @@ 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.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; @@ -27,18 +32,28 @@ public FormLoginAuthenticationFilter(AuthenticationManager authenticationManager @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - 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(); + if (!DEFAULT_REQUEST_URI.equals(((HttpServletRequest)request).getRequestURI())) { + chain.doFilter(request, response); + return; } - ((HttpServletRequest) request).getSession() - .setAttribute(SPRING_SECURITY_CONTEXT_KEY, authentication); + 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(); + } + + ((HttpServletRequest) request).getSession() + .setAttribute(SPRING_SECURITY_CONTEXT_KEY, authentication); + } catch (Exception e) { + ((HttpServletResponse)response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } } } From 714eec2166a04e8845483a501fa9ba2981b7526f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8C=E1=85=A1=E1=86=BC=E1=84=8C=E1=85=AE=E1=86=AB?= =?UTF-8?q?=E1=84=92=E1=85=B4?= Date: Tue, 5 Nov 2024 10:24:49 +0900 Subject: [PATCH 6/6] step3 --- build.gradle | 3 +- src/main/java/nextstep/app/ui/WebConfig.java | 5 ++- .../HttpSessionSecurityContextRepository.java | 29 +++++++++++++++ .../nextstep/security/SecurityContext.java | 18 ++++++++++ .../security/SecurityContextHolder.java | 35 +++++++++++++++++++ .../security/SecurityContextHolderFilter.java | 25 +++++++++++++ .../BasicAuthenticationSecurityFilter.java | 6 ++++ .../security/filter/FilterChainProxy.java | 3 +- .../filter/FormLoginAuthenticationFilter.java | 6 ++++ src/test/java/nextstep/app/MemberTest.java | 27 ++++++++++++++ 10 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 src/main/java/nextstep/security/HttpSessionSecurityContextRepository.java create mode 100644 src/main/java/nextstep/security/SecurityContext.java create mode 100644 src/main/java/nextstep/security/SecurityContextHolder.java create mode 100644 src/main/java/nextstep/security/SecurityContextHolderFilter.java diff --git a/build.gradle b/build.gradle index 2264013..64ecd92 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 e898fa5..5bffdb0 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 0000000..c0de756 --- /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 0000000..3eb0043 --- /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 0000000..daa3136 --- /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 0000000..376b70b --- /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 ca1f348..271d73a 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 af0ebd4..805730b 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 51a763e..4fb0079 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 58aba17..49a620e 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()); + } }