From 86ca5cae502bf1b44b12c6239f80d986bd9f12aa Mon Sep 17 00:00:00 2001 From: im1sun Date: Mon, 4 Nov 2024 21:07:28 +0900 Subject: [PATCH 1/2] =?UTF-8?q?repository=20=EC=9E=98=EB=AA=BB=20=EC=98=AC?= =?UTF-8?q?=EB=A6=BC.=20=ED=94=84=EB=A6=AC=EA=B3=BC=EC=A0=9C=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/app/WebConfig.java | 30 +++++++++ .../nextstep/app/domain/LoginService.java | 24 +++++++ .../java/nextstep/app/ui/LoginController.java | 24 ++++++- .../nextstep/app/ui/MemberController.java | 40 ++++++++++- .../BasicAuthenticationInterceptor.java | 67 +++++++++++++++++++ .../security/FormLoginInterceptor.java | 53 +++++++++++++++ 6 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 src/main/java/nextstep/app/WebConfig.java create mode 100644 src/main/java/nextstep/app/domain/LoginService.java create mode 100644 src/main/java/nextstep/security/BasicAuthenticationInterceptor.java create mode 100644 src/main/java/nextstep/security/FormLoginInterceptor.java diff --git a/src/main/java/nextstep/app/WebConfig.java b/src/main/java/nextstep/app/WebConfig.java new file mode 100644 index 00000000..1e8a2d1d --- /dev/null +++ b/src/main/java/nextstep/app/WebConfig.java @@ -0,0 +1,30 @@ +package nextstep.app; + +import nextstep.app.domain.LoginService; +import nextstep.app.domain.MemberRepository; +import nextstep.security.BasicAuthenticationInterceptor; +import nextstep.security.FormLoginInterceptor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final LoginService loginService; + private final MemberRepository memberRepository; + + public WebConfig(LoginService loginService, MemberRepository memberRepository) { + this.loginService = loginService; + this.memberRepository = memberRepository; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + System.out.println("WebConfig.addInterceptors"); + registry.addInterceptor(new FormLoginInterceptor(loginService)) + .addPathPatterns("/login"); + registry.addInterceptor(new BasicAuthenticationInterceptor(loginService, memberRepository)) + .addPathPatterns("/members"); + } +} diff --git a/src/main/java/nextstep/app/domain/LoginService.java b/src/main/java/nextstep/app/domain/LoginService.java new file mode 100644 index 00000000..31642f75 --- /dev/null +++ b/src/main/java/nextstep/app/domain/LoginService.java @@ -0,0 +1,24 @@ +package nextstep.app.domain; + +import org.springframework.stereotype.Service; + +@Service +public class LoginService { + + private final MemberRepository memberRepository; + + public LoginService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + /** + * @return null 로그인 실패 + */ + public Member login(String email, String password) { + + return memberRepository.findByEmail(email) + .filter(m -> m.getPassword().equals(password)) + .orElse(null); + } +} + diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 0ea94f1b..431cb9e4 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,5 +1,7 @@ package nextstep.app.ui; +import nextstep.app.domain.LoginService; +import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -8,6 +10,7 @@ import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @RestController @@ -15,13 +18,30 @@ public class LoginController { public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; private final MemberRepository memberRepository; + private final LoginService loginService; - public LoginController(MemberRepository memberRepository) { + public LoginController(MemberRepository memberRepository, LoginService loginService) { this.memberRepository = memberRepository; + this.loginService = loginService; } @PostMapping("/login") - public ResponseEntity login(HttpServletRequest request, HttpSession session) { + public ResponseEntity login(HttpServletRequest request, HttpServletResponse response, HttpSession session) { + +// try { +// Member loginMember = loginService.login(request.getParameter("username"), request.getParameter("password")); +// +// if (loginMember == null) { +// System.out.println("로그인에 실패하였습니다."); +// return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); +// } +//// 로그인 성공 처리 +//// 세션에 로그인 회원 정보 보관 +// session.setAttribute("SPRING_SECURITY_CONTEXT", loginMember); +// } catch (Exception e) { +// throw new AuthenticationException(); +// } + return ResponseEntity.ok().build(); } diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index c8cc74d6..2a4114d3 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -2,10 +2,13 @@ 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; +import javax.servlet.http.HttpServletRequest; import java.util.List; @RestController @@ -18,9 +21,40 @@ public MemberController(MemberRepository memberRepository) { } @GetMapping("/members") - public ResponseEntity> list() { - List members = memberRepository.findAll(); - return ResponseEntity.ok(members); + public ResponseEntity> list(HttpServletRequest request) { + + String authHeader = request.getHeader("Authorization"); + + String[] split = authHeader.split(" "); + String type = split[0]; + String credential = split[1]; + + System.out.println("type = " + type); + System.out.println("credential = " + credential); + + try { + String decodedCredential = new String(org.springframework.util.Base64Utils.decodeFromString(credential)); + + String[] emailAndPassword = decodedCredential.split(":"); + String email = emailAndPassword[0]; + String password = emailAndPassword[1]; + + // 사용자 인증 검증 + Member authenticatedMember = memberRepository.findByEmail(email).orElse(null); + if (authenticatedMember == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + + List members = memberRepository.findAll(); + return ResponseEntity.ok(members); + } 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/security/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/BasicAuthenticationInterceptor.java new file mode 100644 index 00000000..b6df2a48 --- /dev/null +++ b/src/main/java/nextstep/security/BasicAuthenticationInterceptor.java @@ -0,0 +1,67 @@ +package nextstep.security; + +import nextstep.app.domain.LoginService; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.app.ui.AuthenticationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.List; + +@Component +public class BasicAuthenticationInterceptor implements HandlerInterceptor { + + private final LoginService loginService; + private final MemberRepository memberRepository; + + public BasicAuthenticationInterceptor(LoginService loginService, MemberRepository memberRepository) { + this.loginService = loginService; + this.memberRepository = memberRepository; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + System.out.println("BasicAuthenticationInterceptor.preHandle"); + + String authHeader = request.getHeader("Authorization"); + + String[] split = authHeader.split(" "); + String type = split[0]; + String credential = split[1]; + + System.out.println("type = " + type); + System.out.println("credential = " + credential); + + try { + String decodedCredential = new String(org.springframework.util.Base64Utils.decodeFromString(credential)); + + String[] emailAndPassword = decodedCredential.split(":"); + String email = emailAndPassword[0]; + String password = emailAndPassword[1]; + + // 사용자 인증 검증 + Member authenticatedMember = memberRepository.findByEmail(email).orElse(null); + if (authenticatedMember == null) { + throw new AuthenticationException(); + } + + List members = memberRepository.findAll(); + return true; + } 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/security/FormLoginInterceptor.java b/src/main/java/nextstep/security/FormLoginInterceptor.java new file mode 100644 index 00000000..72c87e10 --- /dev/null +++ b/src/main/java/nextstep/security/FormLoginInterceptor.java @@ -0,0 +1,53 @@ +package nextstep.security; + +import nextstep.app.domain.LoginService; +import nextstep.app.domain.Member; +import nextstep.app.ui.AuthenticationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +@Component +public class FormLoginInterceptor implements HandlerInterceptor { + + private final LoginService loginService; + + public FormLoginInterceptor(LoginService loginService) { + this.loginService = loginService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + + try { + Member loginMember = loginService.login(request.getParameter("username"), request.getParameter("password")); + + if (loginMember == null) { + System.out.println("로그인에 실패하였습니다."); + throw new AuthenticationException(); + } + //로그인 성공 처리 + //세션에 로그인 회원 정보 보관 + System.out.println("로그인 성공 처리"); + HttpSession session = request.getSession(); + session.setAttribute("SPRING_SECURITY_CONTEXT", loginMember); + ResponseEntity.status(HttpStatus.OK).build(); + } catch (Exception e) { + throw new AuthenticationException(); + } + + return true; + } + + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException() { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + +} From 99959c08d271f6cc6b07e16fea880bdc42815df8 Mon Sep 17 00:00:00 2001 From: im1sun Date: Sun, 10 Nov 2024 16:08:27 +0900 Subject: [PATCH 2/2] =?UTF-8?q?authentication=20=EC=8B=A4=EC=8A=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/SecurityConfig.java | 66 ++++++++++++++ src/main/java/nextstep/app/WebConfig.java | 40 ++++++--- src/main/java/nextstep/app/domain/Member.java | 6 ++ .../app/ui/AuthenticationException.java | 4 - .../java/nextstep/app/ui/LoginController.java | 42 +++------ .../nextstep/app/ui/MemberController.java | 52 +++++------ .../BasicAuthenticationInterceptor.java | 67 -------------- .../security/FormLoginInterceptor.java | 53 ----------- .../authentication/Authentication.java | 11 +++ .../AuthenticationException.java | 12 +++ .../authentication/AuthenticationManager.java | 7 ++ .../AuthenticationProvider.java | 9 ++ .../BasicAuthenticationFilter.java | 87 +++++++++++++++++++ .../BasicAuthenticationInterceptor.java | 60 +++++++++++++ .../DaoAuthenticationProvider.java | 33 +++++++ .../authentication/FormLoginInterceptor.java | 40 +++++++++ .../authentication/ProviderManager.java | 26 ++++++ .../UsernamePasswordAuthenticationFilter.java | 73 ++++++++++++++++ .../UsernamePasswordAuthenticationToken.java | 40 +++++++++ .../config/DefaultSecurityFilterChain.java | 24 +++++ .../config/DelegatingFilterProxy.java | 18 ++++ .../security/config/FilterChainProxy.java | 69 +++++++++++++++ .../security/config/SecurityFilterChain.java | 13 +++ .../HttpSessionSecurityContextRepository.java | 29 +++++++ .../security/context/SecurityContext.java | 25 ++++++ .../context/SecurityContextHolder.java | 38 ++++++++ .../context/SecurityContextHolderFilter.java | 26 ++++++ .../security/userdetails/UserDetails.java | 9 ++ .../userdetails/UserDetailsService.java | 5 ++ src/main/resources/application.properties | 1 + src/test/java/nextstep/app/LoginTest.java | 23 +++++ 31 files changed, 816 insertions(+), 192 deletions(-) create mode 100644 src/main/java/nextstep/app/SecurityConfig.java delete mode 100644 src/main/java/nextstep/app/ui/AuthenticationException.java delete mode 100644 src/main/java/nextstep/security/BasicAuthenticationInterceptor.java delete mode 100644 src/main/java/nextstep/security/FormLoginInterceptor.java create mode 100644 src/main/java/nextstep/security/authentication/Authentication.java create mode 100644 src/main/java/nextstep/security/authentication/AuthenticationException.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/BasicAuthenticationFilter.java create mode 100644 src/main/java/nextstep/security/authentication/BasicAuthenticationInterceptor.java create mode 100644 src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java create mode 100644 src/main/java/nextstep/security/authentication/FormLoginInterceptor.java create mode 100644 src/main/java/nextstep/security/authentication/ProviderManager.java create mode 100644 src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationFilter.java create mode 100644 src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java create mode 100644 src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java create mode 100644 src/main/java/nextstep/security/config/DelegatingFilterProxy.java create mode 100644 src/main/java/nextstep/security/config/FilterChainProxy.java create mode 100644 src/main/java/nextstep/security/config/SecurityFilterChain.java create mode 100644 src/main/java/nextstep/security/context/HttpSessionSecurityContextRepository.java create mode 100644 src/main/java/nextstep/security/context/SecurityContext.java create mode 100644 src/main/java/nextstep/security/context/SecurityContextHolder.java create mode 100644 src/main/java/nextstep/security/context/SecurityContextHolderFilter.java create mode 100644 src/main/java/nextstep/security/userdetails/UserDetails.java create mode 100644 src/main/java/nextstep/security/userdetails/UserDetailsService.java diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java new file mode 100644 index 00000000..b9aecc70 --- /dev/null +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -0,0 +1,66 @@ +package nextstep.app; + +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authentication.BasicAuthenticationFilter; +import nextstep.security.authentication.UsernamePasswordAuthenticationFilter; +import nextstep.security.userdetails.UserDetails; +import nextstep.security.userdetails.UserDetailsService; +import nextstep.security.config.*; +import nextstep.security.context.SecurityContextHolderFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class SecurityConfig { + + private final MemberRepository memberRepository; + public SecurityConfig(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Bean + public DelegatingFilterProxy delegatingFilterProxy() { + return new DelegatingFilterProxy(filterChainProxy(List.of(securityFilterChain()))); + } + + @Bean + public FilterChainProxy filterChainProxy(List securityFilterChains) { + return new FilterChainProxy(securityFilterChains); + } + + + @Bean + public SecurityFilterChain securityFilterChain() { + return new DefaultSecurityFilterChain( + List.of( + + new SecurityContextHolderFilter(), + new UsernamePasswordAuthenticationFilter(userDetailsService()), + new BasicAuthenticationFilter(userDetailsService()) + ) + ); + } + + @Bean + public UserDetailsService userDetailsService() { + return username -> { + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new AuthenticationException("존재하지 않는 사용자입니다.")); + return new UserDetails() { + @Override + public String getUsername() { + return member.getEmail(); + } + @Override + public String getPassword() { + return member.getPassword(); + } + }; + }; + } + +} diff --git a/src/main/java/nextstep/app/WebConfig.java b/src/main/java/nextstep/app/WebConfig.java index 1e8a2d1d..ff55c6e8 100644 --- a/src/main/java/nextstep/app/WebConfig.java +++ b/src/main/java/nextstep/app/WebConfig.java @@ -1,30 +1,50 @@ package nextstep.app; -import nextstep.app.domain.LoginService; +import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; -import nextstep.security.BasicAuthenticationInterceptor; -import nextstep.security.FormLoginInterceptor; -import org.springframework.context.annotation.Configuration; +import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authentication.BasicAuthenticationInterceptor; +import nextstep.security.authentication.FormLoginInterceptor; +import nextstep.security.userdetails.UserDetails; +import nextstep.security.userdetails.UserDetailsService; +import org.springframework.context.annotation.Bean; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -@Configuration +//@Configuration public class WebConfig implements WebMvcConfigurer { - private final LoginService loginService; private final MemberRepository memberRepository; - public WebConfig(LoginService loginService, MemberRepository memberRepository) { - this.loginService = loginService; + public WebConfig(MemberRepository memberRepository) { this.memberRepository = memberRepository; } @Override public void addInterceptors(InterceptorRegistry registry) { System.out.println("WebConfig.addInterceptors"); - registry.addInterceptor(new FormLoginInterceptor(loginService)) + registry.addInterceptor(new FormLoginInterceptor(userDetailsService())) .addPathPatterns("/login"); - registry.addInterceptor(new BasicAuthenticationInterceptor(loginService, memberRepository)) + registry.addInterceptor(new BasicAuthenticationInterceptor(userDetailsService())) .addPathPatterns("/members"); } + + @Bean + public UserDetailsService userDetailsService() { + return username -> { + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new AuthenticationException("존재하지 않는 사용자입니다.")); + return new UserDetails() { + @Override + public String getUsername() { + return member.getEmail(); + } + + @Override + public String getPassword() { + return member.getPassword(); + } + }; + }; + } } diff --git a/src/main/java/nextstep/app/domain/Member.java b/src/main/java/nextstep/app/domain/Member.java index 6cafa9c7..f37a0f3a 100644 --- a/src/main/java/nextstep/app/domain/Member.java +++ b/src/main/java/nextstep/app/domain/Member.java @@ -28,4 +28,10 @@ public String getName() { public String getImageUrl() { return imageUrl; } + + public void checkPassword(String password) { + if (!this.password.equals(password)) { + throw new IllegalArgumentException("비밀번호가 일치하지 않습니다."); + } + } } diff --git a/src/main/java/nextstep/app/ui/AuthenticationException.java b/src/main/java/nextstep/app/ui/AuthenticationException.java deleted file mode 100644 index f809b6e4..00000000 --- a/src/main/java/nextstep/app/ui/AuthenticationException.java +++ /dev/null @@ -1,4 +0,0 @@ -package nextstep.app.ui; - -public class AuthenticationException extends RuntimeException { -} diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 431cb9e4..d47689d8 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,49 +1,33 @@ package nextstep.app.ui; -import nextstep.app.domain.LoginService; -import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; +import nextstep.security.authentication.AuthenticationException; 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.HttpServletResponse; -import javax.servlet.http.HttpSession; - @RestController public class LoginController { public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; private final MemberRepository memberRepository; - private final LoginService loginService; - public LoginController(MemberRepository memberRepository, LoginService loginService) { + public LoginController(MemberRepository memberRepository) { this.memberRepository = memberRepository; - this.loginService = loginService; } - @PostMapping("/login") - public ResponseEntity login(HttpServletRequest request, HttpServletResponse response, HttpSession session) { - -// try { -// Member loginMember = loginService.login(request.getParameter("username"), request.getParameter("password")); -// -// if (loginMember == null) { -// System.out.println("로그인에 실패하였습니다."); -// return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); -// } -//// 로그인 성공 처리 -//// 세션에 로그인 회원 정보 보관 -// session.setAttribute("SPRING_SECURITY_CONTEXT", loginMember); -// } catch (Exception e) { -// throw new AuthenticationException(); -// } - - return ResponseEntity.ok().build(); - } +// @PostMapping("/login") +// public ResponseEntity login(HttpServletRequest request, HttpSession session) { +// Map parameterMap = request.getParameterMap(); +// String username = parameterMap.get("username")[0]; +// String password = parameterMap.get("password")[0]; +// Member member = memberRepository.findByEmail(username) +// .orElseThrow(AuthenticationException::new); +// member.checkPassword(password); +// session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member); +// return ResponseEntity.ok().build(); +// } @ExceptionHandler(AuthenticationException.class) public ResponseEntity handleAuthenticationException() { diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index 2a4114d3..4dfd347f 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -2,6 +2,7 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; +import nextstep.security.authentication.AuthenticationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -22,34 +23,27 @@ public MemberController(MemberRepository memberRepository) { @GetMapping("/members") public ResponseEntity> list(HttpServletRequest request) { - - String authHeader = request.getHeader("Authorization"); - - String[] split = authHeader.split(" "); - String type = split[0]; - String credential = split[1]; - - System.out.println("type = " + type); - System.out.println("credential = " + credential); - - try { - String decodedCredential = new String(org.springframework.util.Base64Utils.decodeFromString(credential)); - - String[] emailAndPassword = decodedCredential.split(":"); - String email = emailAndPassword[0]; - String password = emailAndPassword[1]; - - // 사용자 인증 검증 - Member authenticatedMember = memberRepository.findByEmail(email).orElse(null); - if (authenticatedMember == null) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - - List members = memberRepository.findAll(); - return ResponseEntity.ok(members); - } catch (Exception e) { - throw new AuthenticationException(); - } +// String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); +// String[] split = authorization.split(" "); +// String type = split[0]; +// String credential = split[1]; +// if (!"Basic".equalsIgnoreCase(type)) { +// throw new AuthenticationException(); +// } +// try { +// String decodedCredential = new String(Base64Utils.decodeFromString(credential)); +// String[] emailAndPassword = decodedCredential.split(":"); +// String email = emailAndPassword[0]; +// String password = emailAndPassword[1]; +// Member member = memberRepository.findByEmail(email).orElseThrow(AuthenticationException::new); +// member.checkPassword(password); +// List members = memberRepository.findAll(); +// return ResponseEntity.ok(members); +// } catch (Exception e) { +// throw new AuthenticationException(); +// } + List members = memberRepository.findAll(); + return ResponseEntity.ok(members); } @ExceptionHandler(AuthenticationException.class) @@ -57,4 +51,4 @@ public ResponseEntity handleAuthenticationException() { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } -} +} \ No newline at end of file diff --git a/src/main/java/nextstep/security/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/BasicAuthenticationInterceptor.java deleted file mode 100644 index b6df2a48..00000000 --- a/src/main/java/nextstep/security/BasicAuthenticationInterceptor.java +++ /dev/null @@ -1,67 +0,0 @@ -package nextstep.security; - -import nextstep.app.domain.LoginService; -import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; -import nextstep.app.ui.AuthenticationException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.servlet.HandlerInterceptor; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import java.util.List; - -@Component -public class BasicAuthenticationInterceptor implements HandlerInterceptor { - - private final LoginService loginService; - private final MemberRepository memberRepository; - - public BasicAuthenticationInterceptor(LoginService loginService, MemberRepository memberRepository) { - this.loginService = loginService; - this.memberRepository = memberRepository; - } - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - System.out.println("BasicAuthenticationInterceptor.preHandle"); - - String authHeader = request.getHeader("Authorization"); - - String[] split = authHeader.split(" "); - String type = split[0]; - String credential = split[1]; - - System.out.println("type = " + type); - System.out.println("credential = " + credential); - - try { - String decodedCredential = new String(org.springframework.util.Base64Utils.decodeFromString(credential)); - - String[] emailAndPassword = decodedCredential.split(":"); - String email = emailAndPassword[0]; - String password = emailAndPassword[1]; - - // 사용자 인증 검증 - Member authenticatedMember = memberRepository.findByEmail(email).orElse(null); - if (authenticatedMember == null) { - throw new AuthenticationException(); - } - - List members = memberRepository.findAll(); - return true; - } 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/security/FormLoginInterceptor.java b/src/main/java/nextstep/security/FormLoginInterceptor.java deleted file mode 100644 index 72c87e10..00000000 --- a/src/main/java/nextstep/security/FormLoginInterceptor.java +++ /dev/null @@ -1,53 +0,0 @@ -package nextstep.security; - -import nextstep.app.domain.LoginService; -import nextstep.app.domain.Member; -import nextstep.app.ui.AuthenticationException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.servlet.HandlerInterceptor; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -@Component -public class FormLoginInterceptor implements HandlerInterceptor { - - private final LoginService loginService; - - public FormLoginInterceptor(LoginService loginService) { - this.loginService = loginService; - } - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - - try { - Member loginMember = loginService.login(request.getParameter("username"), request.getParameter("password")); - - if (loginMember == null) { - System.out.println("로그인에 실패하였습니다."); - throw new AuthenticationException(); - } - //로그인 성공 처리 - //세션에 로그인 회원 정보 보관 - System.out.println("로그인 성공 처리"); - HttpSession session = request.getSession(); - session.setAttribute("SPRING_SECURITY_CONTEXT", loginMember); - ResponseEntity.status(HttpStatus.OK).build(); - } catch (Exception e) { - throw new AuthenticationException(); - } - - return true; - } - - @ExceptionHandler(AuthenticationException.class) - public ResponseEntity handleAuthenticationException() { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - -} 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..ed3c6ebc --- /dev/null +++ b/src/main/java/nextstep/security/authentication/Authentication.java @@ -0,0 +1,11 @@ +package nextstep.security.authentication; + +public interface Authentication { + + Object getCredentials(); + + Object getPrincipal(); + + Object isAuthenticated(); + +} diff --git a/src/main/java/nextstep/security/authentication/AuthenticationException.java b/src/main/java/nextstep/security/authentication/AuthenticationException.java new file mode 100644 index 00000000..7fc4aaed --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationException.java @@ -0,0 +1,12 @@ +package nextstep.security.authentication; + +public class AuthenticationException extends RuntimeException { + + public AuthenticationException() { + super("인증에 실패하였습니다."); + } + public AuthenticationException(String message) { + super(message); + } + +} diff --git a/src/main/java/nextstep/security/authentication/AuthenticationManager.java b/src/main/java/nextstep/security/authentication/AuthenticationManager.java new file mode 100644 index 00000000..47973f66 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationManager.java @@ -0,0 +1,7 @@ +package nextstep.security.authentication; + +public interface AuthenticationManager { + + Authentication authenticate(Authentication authentication); + +} diff --git a/src/main/java/nextstep/security/authentication/AuthenticationProvider.java b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java new file mode 100644 index 00000000..5bb7ec01 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java @@ -0,0 +1,9 @@ +package nextstep.security.authentication; + +public interface AuthenticationProvider { + + Authentication authenticate(Authentication authentication) throws AuthenticationException; + + boolean supports(Class authentication); + +} diff --git a/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java new file mode 100644 index 00000000..97a4c5c1 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java @@ -0,0 +1,87 @@ +package nextstep.security.authentication; + +import nextstep.security.userdetails.UserDetailsService; +import nextstep.security.context.SecurityContext; +import nextstep.security.context.SecurityContextHolder; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; + +public class BasicAuthenticationFilter extends OncePerRequestFilter { + + public static final String AUTHENTICATION_SCHEME_BASIC = "Basic"; + + private final AuthenticationManager authenticationManager; + + public BasicAuthenticationFilter(UserDetailsService userDetailsService) { + System.out.println("BasicAuthenticationFilter.BasicAuthenticationFilter"); + this.authenticationManager = new ProviderManager(List.of(new DaoAuthenticationProvider(userDetailsService))); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { + System.out.println("BasicAuthenticationFilter.doFilterInternal"); + try { + Authentication authentication = convert(request); + if (authentication == null) { + System.out.println("authentication null"); + filterChain.doFilter(request, response); + return; + } + Authentication authenticate = this.authenticationManager.authenticate(authentication); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authenticate); + SecurityContextHolder.setContext(context); + + filterChain.doFilter(request, response); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } + + private Authentication convert(HttpServletRequest request) { + String header = request.getHeader(HttpHeaders.AUTHORIZATION); + if (header == null) { + return null; + } + header = header.trim(); + if (!StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) { + return null; + } + if (header.equalsIgnoreCase(AUTHENTICATION_SCHEME_BASIC)) { + throw new AuthenticationException(); + } + byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8); + byte[] decoded = decode(base64Token); + String token = new String(decoded, StandardCharsets.UTF_8); + int delim = token.indexOf(":"); + if (delim == -1) { + throw new AuthenticationException(); + } + return UsernamePasswordAuthenticationToken + .unauthenticated(token.substring(0, delim), token.substring(delim + 1)); + } + + private byte[] decode(byte[] base64Token) { + try { + return Base64.getDecoder().decode(base64Token); + } catch (IllegalArgumentException ex) { + throw new AuthenticationException(); + } + } + + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException() { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } +} 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 00000000..8ee3fc28 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/BasicAuthenticationInterceptor.java @@ -0,0 +1,60 @@ +package nextstep.security.authentication; + +import nextstep.security.userdetails.UserDetails; +import nextstep.security.userdetails.UserDetailsService; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.Base64Utils; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Objects; + +@Component +public class BasicAuthenticationInterceptor implements HandlerInterceptor { + + private final UserDetailsService userDetailsService; + + public BasicAuthenticationInterceptor(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + try { + checkAuthentication(request); + return true; + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + } + + private void checkAuthentication(HttpServletRequest request) { + String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + String[] split = authorization.split(" "); + String type = split[0]; + String credential = split[1]; + if (!"Basic".equalsIgnoreCase(type)) { + throw new AuthenticationException(); + } + String decodedCredential = new String(Base64Utils.decodeFromString(credential)); + String[] emailAndPassword = decodedCredential.split(":"); + String email = emailAndPassword[0]; + String password = emailAndPassword[1]; + UserDetails userDetails = userDetailsService.loadUserByUsername(email); + if (!Objects.equals(userDetails.getPassword(), password)) { + throw new AuthenticationException(); + } + } + + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException() { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + +} 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..3333a270 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java @@ -0,0 +1,33 @@ +package nextstep.security.authentication; + +import nextstep.security.userdetails.UserDetails; +import nextstep.security.userdetails.UserDetailsService; + +import java.util.Objects; + +public class DaoAuthenticationProvider implements AuthenticationProvider{ + + private final UserDetailsService userDetailsService; + + public DaoAuthenticationProvider(UserDetailsService userDetailsService) { + System.out.println("DaoAuthenticationProvider.DaoAuthenticationProvider"); + this.userDetailsService = userDetailsService; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + System.out.println("DaoAuthenticationProvider.authenticate"); + UserDetails userDetails = userDetailsService.loadUserByUsername(authentication.getPrincipal().toString()); + if(!Objects.equals(userDetails.getPassword(), authentication.getCredentials())) { + System.out.println("userDetails null"); + throw new AuthenticationException(); + } + return UsernamePasswordAuthenticationToken.authenticated(userDetails.getUsername(), userDetails.getPassword()); + } + + @Override + public boolean supports(Class authentication) { + System.out.println("DaoAuthenticationProvider.supports"); + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/nextstep/security/authentication/FormLoginInterceptor.java b/src/main/java/nextstep/security/authentication/FormLoginInterceptor.java new file mode 100644 index 00000000..f5e406c0 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/FormLoginInterceptor.java @@ -0,0 +1,40 @@ +package nextstep.security.authentication; + +import nextstep.security.userdetails.UserDetails; +import nextstep.security.userdetails.UserDetailsService; +import org.springframework.web.servlet.HandlerInterceptor; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.Map; +import java.util.Objects; + +public class FormLoginInterceptor implements HandlerInterceptor { + + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + private final UserDetailsService userDetailsService; + + public FormLoginInterceptor(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + try { + Map parameterMap = request.getParameterMap(); + String username = parameterMap.get("username")[0]; + String password = parameterMap.get("password")[0]; + + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + if (!Objects.equals(userDetails.getPassword(), password)) { + throw new AuthenticationException(); + } + HttpSession session = request.getSession(); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetails); + } catch (AuthenticationException e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + return false; + } +} \ No newline at end of file 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..3e4d425f --- /dev/null +++ b/src/main/java/nextstep/security/authentication/ProviderManager.java @@ -0,0 +1,26 @@ +package nextstep.security.authentication; + +import java.util.List; + +public class ProviderManager implements AuthenticationManager{ + + private final List providers; + + public ProviderManager(List providers) { + System.out.println("ProviderManager.ProviderManager"); + this.providers = providers; + } + + @Override + public Authentication authenticate(Authentication authentication) { + System.out.println("ProviderManager.authenticate"); + for (AuthenticationProvider provider : providers) { + System.out.println("provider = " + provider); + if(provider.supports(authentication.getClass())) { + System.out.println("ProviderManager.authenticate"); + return provider.authenticate(authentication); + } + } + return null; + } +} diff --git a/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationFilter.java b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationFilter.java new file mode 100644 index 00000000..9394f353 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationFilter.java @@ -0,0 +1,73 @@ +package nextstep.security.authentication; + +import nextstep.security.userdetails.UserDetailsService; +import nextstep.security.context.HttpSessionSecurityContextRepository; +import nextstep.security.context.SecurityContext; +import nextstep.security.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +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 java.io.IOException; +import java.util.List; +import java.util.Map; + +public class UsernamePasswordAuthenticationFilter extends GenericFilterBean { + + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + private static final String DEFAULT_REQUEST_URI = "/login"; + + private final AuthenticationManager authenticationManager; + private final HttpSessionSecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository(); + + public UsernamePasswordAuthenticationFilter(UserDetailsService userDetailsService) { + System.out.println("UsernamePasswordAuthenticationFilter.UsernamePasswordAuthenticationFilter"); + this.authenticationManager = new ProviderManager(List.of(new DaoAuthenticationProvider(userDetailsService))); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + System.out.println("UsernamePasswordAuthenticationFilter.doFilter"); + if (!DEFAULT_REQUEST_URI.equals(((HttpServletRequest) request).getRequestURI())) { + System.out.println("DEFAULT_REQUEST_URI = " + DEFAULT_REQUEST_URI); + chain.doFilter(request, response); + return; + } + try { + Authentication authentication = convert(request); + if(authentication == null) { + chain.doFilter(request, response); + return; + } + + Authentication authenticate = authenticationManager.authenticate(authentication); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authenticate); + SecurityContextHolder.setContext(context); + +// HttpSession session = ((HttpServletRequest) request).getSession(); +// session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, authenticate); + securityContextRepository.saveContext(context, (HttpServletRequest) request, (HttpServletResponse) response); + + } catch (AuthenticationException e) { + System.out.println("UsernamePasswordAuthenticationFilter-Exception"); + ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } + + private Authentication convert(ServletRequest request) { + System.out.println("UsernamePasswordAuthenticationFilter.convert"); + try { + Map parameterMap = request.getParameterMap(); + String username = parameterMap.get("username")[0]; + String password = parameterMap.get("password")[0]; + return UsernamePasswordAuthenticationToken.unauthenticated(username, password); + } catch (Exception e) { + 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..abf95fb2 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java @@ -0,0 +1,40 @@ +package nextstep.security.authentication; + +public class UsernamePasswordAuthenticationToken implements Authentication{ + + private final Object principal; + private final Object credentials; + private final boolean authenticated; + + public UsernamePasswordAuthenticationToken(Object principal, Object credentials, boolean authenticated) { + System.out.println("UsernamePasswordAuthenticationToken.UsernamePasswordAuthenticationToken"); + this.principal = principal; + this.credentials = credentials; + this.authenticated = authenticated; + } + + public static UsernamePasswordAuthenticationToken unauthenticated(String principal, String credentials) { + System.out.println("UsernamePasswordAuthenticationToken.unauthenticated"); + return new UsernamePasswordAuthenticationToken(principal, credentials, false); + } + + public static UsernamePasswordAuthenticationToken authenticated(String principal, String credentials) { + System.out.println("UsernamePasswordAuthenticationToken.authenticated"); + return new UsernamePasswordAuthenticationToken(principal, credentials, true); + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public Object isAuthenticated() { + return authenticated; + } +} diff --git a/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java b/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java new file mode 100644 index 00000000..26e2c58e --- /dev/null +++ b/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java @@ -0,0 +1,24 @@ +package nextstep.security.config; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +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/config/DelegatingFilterProxy.java b/src/main/java/nextstep/security/config/DelegatingFilterProxy.java new file mode 100644 index 00000000..efbaa5ed --- /dev/null +++ b/src/main/java/nextstep/security/config/DelegatingFilterProxy.java @@ -0,0 +1,18 @@ +package nextstep.security.config; +import org.springframework.web.filter.GenericFilterBean; +import javax.servlet.*; +import java.io.IOException; +public class DelegatingFilterProxy extends GenericFilterBean { + private final Filter delegate; + + public DelegatingFilterProxy(Filter delegate) { + System.out.println("DelegatingFilterProxy.DelegatingFilterProxy"); + this.delegate = delegate; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + System.out.println("DelegatingFilterProxy.doFilter"); + delegate.doFilter(request, response, chain); + } +} diff --git a/src/main/java/nextstep/security/config/FilterChainProxy.java b/src/main/java/nextstep/security/config/FilterChainProxy.java new file mode 100644 index 00000000..31d8b581 --- /dev/null +++ b/src/main/java/nextstep/security/config/FilterChainProxy.java @@ -0,0 +1,69 @@ +package nextstep.security.config; +import org.springframework.web.filter.GenericFilterBean; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.List; +public class FilterChainProxy extends GenericFilterBean { + private final List filterChains; + + public FilterChainProxy(List filterChains) { + System.out.println("FilterChainProxy.FilterChainProxy"); + this.filterChains = filterChains; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + System.out.println("FilterChainProxy.doFilter"); + List filters = getFilters((HttpServletRequest) request); + VirtualFilterChain virtualFilterChain = new VirtualFilterChain(chain, filters); + virtualFilterChain.doFilter(request, response); + } + + private List getFilters(HttpServletRequest request) { + System.out.println("FilterChainProxy.getFilters"); + System.out.println("request = " + request); + for (SecurityFilterChain chain : this.filterChains) { + System.out.println("chain = " + chain); //DefaultSecurityFilterChain + if (chain.matches(request)) { + System.out.println("chain.getFilters() = " + chain.getFilters()); + return chain.getFilters(); + } + } + return null; + } + + 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 chain, List additionalFilters) { + System.out.println("VirtualFilterChain.VirtualFilterChain"); + for (Filter additionalFilter : additionalFilters) { + System.out.println("additionalFilter = " + additionalFilter); + } + this.originalChain = chain; + this.additionalFilters = additionalFilters; + this.size = additionalFilters.size(); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + System.out.println("VirtualFilterChain.doFilter"); + if (this.currentPosition == this.size) { + System.out.println("currentPosition = " + currentPosition); + System.out.println("size = " + size); + System.out.println("originalChain = " + originalChain); + this.originalChain.doFilter(request, response); + return; + } + this.currentPosition++; + Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1); + System.out.println("nextFilter = " + nextFilter); + nextFilter.doFilter(request, response, this); + } + } +} \ No newline at end of file diff --git a/src/main/java/nextstep/security/config/SecurityFilterChain.java b/src/main/java/nextstep/security/config/SecurityFilterChain.java new file mode 100644 index 00000000..534bea60 --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityFilterChain.java @@ -0,0 +1,13 @@ +package nextstep.security.config; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +public interface SecurityFilterChain { + + boolean matches(HttpServletRequest request); + + List getFilters(); + +} diff --git a/src/main/java/nextstep/security/context/HttpSessionSecurityContextRepository.java b/src/main/java/nextstep/security/context/HttpSessionSecurityContextRepository.java new file mode 100644 index 00000000..37d24751 --- /dev/null +++ b/src/main/java/nextstep/security/context/HttpSessionSecurityContextRepository.java @@ -0,0 +1,29 @@ +package nextstep.security.context; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +public class HttpSessionSecurityContextRepository { + + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + public SecurityContext loadContext(HttpServletRequest request) { + System.out.println("HttpSessionSecurityContextRepository.loadContext"); + HttpSession session = request.getSession(false); + if(session == null) { + System.out.println("session null"); + + return null; + } + System.out.println("session.getAttribute(SPRING_SECURITY_CONTEXT_KEY) not null"); + return (SecurityContext) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY); + } + + public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { + System.out.println("HttpSessionSecurityContextRepository.saveContext"); + HttpSession session = request.getSession(); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context); + } + +} diff --git a/src/main/java/nextstep/security/context/SecurityContext.java b/src/main/java/nextstep/security/context/SecurityContext.java new file mode 100644 index 00000000..076035e8 --- /dev/null +++ b/src/main/java/nextstep/security/context/SecurityContext.java @@ -0,0 +1,25 @@ +package nextstep.security.context; + +import nextstep.security.authentication.Authentication; + +import java.io.Serializable; + +public class SecurityContext implements Serializable { + + private Authentication authentication; + + public SecurityContext() { + } + + public SecurityContext(Authentication authentication) { + this.authentication = authentication; + } + + public Authentication getAuthentication() { + return authentication; + } + + public void setAuthentication(Authentication authentication) { + this.authentication = authentication; + } +} diff --git a/src/main/java/nextstep/security/context/SecurityContextHolder.java b/src/main/java/nextstep/security/context/SecurityContextHolder.java new file mode 100644 index 00000000..e675ba76 --- /dev/null +++ b/src/main/java/nextstep/security/context/SecurityContextHolder.java @@ -0,0 +1,38 @@ +package nextstep.security.context; + +public class SecurityContextHolder { + + private static final ThreadLocal contextHolder; + + static { + System.out.println("SecurityContextHolder.static initializer"); + contextHolder = new ThreadLocal<>(); + } + + public static void clearContext() { + System.out.println("SecurityContextHolder.clearContext"); + contextHolder.remove(); + } + + public static SecurityContext getContext() { + System.out.println("SecurityContextHolder.getContext"); + SecurityContext ctx = contextHolder.get(); + if (ctx == null) { + ctx = createEmptyContext(); + contextHolder.set(ctx); + } + return ctx; + } + + public static void setContext(SecurityContext context) { + System.out.println("SecurityContextHolder.setContext"); + if (context != null) { + System.out.println("SecurityContextHolder.setContext - not null"); + contextHolder.set(context); + } + } + public static SecurityContext createEmptyContext() { + System.out.println("SecurityContextHolder.createEmptyContext"); + return new SecurityContext(); + } +} diff --git a/src/main/java/nextstep/security/context/SecurityContextHolderFilter.java b/src/main/java/nextstep/security/context/SecurityContextHolderFilter.java new file mode 100644 index 00000000..1f16a33b --- /dev/null +++ b/src/main/java/nextstep/security/context/SecurityContextHolderFilter.java @@ -0,0 +1,26 @@ +package nextstep.security.context; + +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +public class SecurityContextHolderFilter extends GenericFilterBean { + + private final HttpSessionSecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository(); + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + System.out.println("SecurityContextHolderFilter.doFilter"); + SecurityContext context = securityContextRepository.loadContext((HttpServletRequest) servletRequest); + SecurityContextHolder.setContext(context); + + filterChain.doFilter(servletRequest, servletResponse); + + SecurityContextHolder.clearContext(); + } +} diff --git a/src/main/java/nextstep/security/userdetails/UserDetails.java b/src/main/java/nextstep/security/userdetails/UserDetails.java new file mode 100644 index 00000000..f780bf2f --- /dev/null +++ b/src/main/java/nextstep/security/userdetails/UserDetails.java @@ -0,0 +1,9 @@ +package nextstep.security.userdetails; + +public interface UserDetails { + + String getUsername(); + + String getPassword(); + +} diff --git a/src/main/java/nextstep/security/userdetails/UserDetailsService.java b/src/main/java/nextstep/security/userdetails/UserDetailsService.java new file mode 100644 index 00000000..7b0bd101 --- /dev/null +++ b/src/main/java/nextstep/security/userdetails/UserDetailsService.java @@ -0,0 +1,5 @@ +package nextstep.security.userdetails; + +public interface UserDetailsService { + UserDetails loadUserByUsername(String username); +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b137891..5c804f84 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ +#logging.level.org.springframework=TRACE \ No newline at end of file diff --git a/src/test/java/nextstep/app/LoginTest.java b/src/test/java/nextstep/app/LoginTest.java index 717bcc8a..92e8078e 100644 --- a/src/test/java/nextstep/app/LoginTest.java +++ b/src/test/java/nextstep/app/LoginTest.java @@ -8,14 +8,18 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpSession; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpSession; import static org.assertj.core.api.Assertions.assertThat; +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.status; @SpringBootTest @@ -65,4 +69,23 @@ void login_fail_with_invalid_password() throws Exception { response.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) + ).andDo(print()); + 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()); + } }