From 55d513e394b04d4eb5fe65ee75c44ae1847e30e8 Mon Sep 17 00:00:00 2001 From: juno-junho Date: Mon, 3 Feb 2025 22:14:44 +0900 Subject: [PATCH 01/24] =?UTF-8?q?refactor(Authentication):=20BasicAuthInte?= =?UTF-8?q?rceptor=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/app/BasicAuthInterceptor.java | 47 +++++++++++++++++++ src/main/java/nextstep/app/WebConfig.java | 23 +++++++++ .../nextstep/app/ui/MemberController.java | 22 ++------- 3 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 src/main/java/nextstep/app/BasicAuthInterceptor.java create mode 100644 src/main/java/nextstep/app/WebConfig.java diff --git a/src/main/java/nextstep/app/BasicAuthInterceptor.java b/src/main/java/nextstep/app/BasicAuthInterceptor.java new file mode 100644 index 00000000..88382913 --- /dev/null +++ b/src/main/java/nextstep/app/BasicAuthInterceptor.java @@ -0,0 +1,47 @@ +package nextstep.app; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.app.domain.MemberRepository; +import nextstep.app.ui.AuthenticationException; +import nextstep.app.util.Base64Convertor; +import org.springframework.web.servlet.HandlerInterceptor; + +public class BasicAuthInterceptor implements HandlerInterceptor { + + private final MemberRepository memberRepository; + + public BasicAuthInterceptor(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + try { + String authorizationHeader = request.getHeader("Authorization"); + String authType = authorizationHeader.split(" ")[0]; + String credentials = authorizationHeader.split(" ")[1]; + String decodedString = Base64Convertor.decode(credentials); + checkAuthType(authType); + + String[] usernameAndPassword = decodedString.split(":"); + String username = usernameAndPassword[0]; + String password = usernameAndPassword[1]; + + memberRepository.findByEmail(username) + .filter(it -> it.matchPassword(password)) + .orElseThrow(AuthenticationException::new); + return true; + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + } + + private void checkAuthType(String authType) { + if (!authType.equalsIgnoreCase(HttpServletRequest.BASIC_AUTH)) { + throw new AuthenticationException(); + } + } + +} diff --git a/src/main/java/nextstep/app/WebConfig.java b/src/main/java/nextstep/app/WebConfig.java new file mode 100644 index 00000000..d4435e98 --- /dev/null +++ b/src/main/java/nextstep/app/WebConfig.java @@ -0,0 +1,23 @@ +package nextstep.app; + +import nextstep.app.domain.MemberRepository; +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 MemberRepository memberRepository; + + public WebConfig(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new BasicAuthInterceptor(memberRepository)).addPathPatterns("/members"); +// registry.addInterceptor(new FormLoginInterceptor()).addPathPatterns("/login"); + } + +} diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index 15cc0395..b2267629 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -2,10 +2,8 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; -import nextstep.app.util.Base64Convertor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -20,22 +18,8 @@ public MemberController(MemberRepository memberRepository) { } @GetMapping("/members") - public ResponseEntity> list(@RequestHeader("Authorization") String authorization) { - try { - String credentials = authorization.split(" ")[1]; - String decodedString = Base64Convertor.decode(credentials); - String[] usernameAndPassword = decodedString.split(":"); - String username = usernameAndPassword[0]; - String password = usernameAndPassword[1]; - - memberRepository.findByEmail(username) - .filter(it -> it.matchPassword(password)) - .orElseThrow(AuthenticationException::new); - - List members = memberRepository.findAll(); - return ResponseEntity.ok(members); - } catch (Exception e) { - throw new AuthenticationException(); - } + public ResponseEntity> list() { + List members = memberRepository.findAll(); + return ResponseEntity.ok(members); } } From aac34c6c6bb93dc0f0aa15f766faa02baee8c2ed Mon Sep 17 00:00:00 2001 From: juno-junho Date: Mon, 3 Feb 2025 22:27:36 +0900 Subject: [PATCH 02/24] =?UTF-8?q?refactor(Authentication):=20FormLoginInte?= =?UTF-8?q?rceptor=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/app/FormLoginInterceptor.java | 43 +++++++++++++++++++ src/main/java/nextstep/app/WebConfig.java | 2 +- .../java/nextstep/app/ui/LoginController.java | 37 ---------------- src/test/java/nextstep/app/FormLoginTest.java | 4 ++ ...ecurityAuthenticationApplicationTests.java | 13 ------ 5 files changed, 48 insertions(+), 51 deletions(-) create mode 100644 src/main/java/nextstep/app/FormLoginInterceptor.java delete mode 100644 src/main/java/nextstep/app/ui/LoginController.java delete mode 100644 src/test/java/nextstep/app/SecurityAuthenticationApplicationTests.java diff --git a/src/main/java/nextstep/app/FormLoginInterceptor.java b/src/main/java/nextstep/app/FormLoginInterceptor.java new file mode 100644 index 00000000..c1c71660 --- /dev/null +++ b/src/main/java/nextstep/app/FormLoginInterceptor.java @@ -0,0 +1,43 @@ +package nextstep.app; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.app.ui.AuthenticationException; +import org.springframework.web.servlet.HandlerInterceptor; + +public class FormLoginInterceptor implements HandlerInterceptor { + + private static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + private final MemberRepository memberRepository; + + public FormLoginInterceptor(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + try { + String username = request.getParameter("username"); + String password = request.getParameter("password"); + + Member member = memberRepository.findByEmail(username) + .filter(it -> it.matchPassword(password)) + .orElseThrow(AuthenticationException::new); + + addMemberToSession(request, member); + }catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + return false; + } + + private void addMemberToSession(HttpServletRequest request, Member member) { + HttpSession session = request.getSession(); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member); + } + +} diff --git a/src/main/java/nextstep/app/WebConfig.java b/src/main/java/nextstep/app/WebConfig.java index d4435e98..58fedfa3 100644 --- a/src/main/java/nextstep/app/WebConfig.java +++ b/src/main/java/nextstep/app/WebConfig.java @@ -16,8 +16,8 @@ public WebConfig(MemberRepository memberRepository) { @Override public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new FormLoginInterceptor(memberRepository)).addPathPatterns("/login"); registry.addInterceptor(new BasicAuthInterceptor(memberRepository)).addPathPatterns("/members"); -// registry.addInterceptor(new FormLoginInterceptor()).addPathPatterns("/login"); } } 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 cc0fb95d..00000000 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ /dev/null @@ -1,37 +0,0 @@ -package nextstep.app.ui; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; -import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Map; - -@RestController -public class LoginController { - public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - - private final MemberRepository memberRepository; - - public LoginController(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } - - @PostMapping("/login") - public ResponseEntity login(HttpServletRequest request, HttpSession session) { - Map parameterMap = request.getParameterMap(); - String username = parameterMap.get("username")[0]; - String password = parameterMap.get("password")[0]; - - Member member = memberRepository.findByEmail(username) - .filter(it -> it.matchPassword(password)) - .orElseThrow(AuthenticationException::new); - - session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member); - - return ResponseEntity.ok().build(); - } -} diff --git a/src/test/java/nextstep/app/FormLoginTest.java b/src/test/java/nextstep/app/FormLoginTest.java index ea9ccf87..9c156371 100644 --- a/src/test/java/nextstep/app/FormLoginTest.java +++ b/src/test/java/nextstep/app/FormLoginTest.java @@ -15,6 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @@ -42,6 +43,7 @@ void login_success() throws Exception { .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); + loginResponse.andDo(print()); loginResponse.andExpect(status().isOk()); HttpSession session = loginResponse.andReturn().getRequest().getSession(); @@ -58,6 +60,7 @@ void login_fail_with_no_user() throws Exception { .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); + response.andDo(print()); response.andExpect(status().isUnauthorized()); } @@ -70,6 +73,7 @@ void login_fail_with_invalid_password() throws Exception { .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); + response.andDo(print()); response.andExpect(status().isUnauthorized()); } } diff --git a/src/test/java/nextstep/app/SecurityAuthenticationApplicationTests.java b/src/test/java/nextstep/app/SecurityAuthenticationApplicationTests.java deleted file mode 100644 index 5b47bde8..00000000 --- a/src/test/java/nextstep/app/SecurityAuthenticationApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package nextstep.app; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class SecurityAuthenticationApplicationTests { - - @Test - void contextLoads() { - } - -} From 9b051147e284c5c53ea2c35ea24f2fdd4f96054f Mon Sep 17 00:00:00 2001 From: juno-junho Date: Mon, 3 Feb 2025 22:57:21 +0900 Subject: [PATCH 03/24] =?UTF-8?q?refactor(Authentication):=20FormLoginInte?= =?UTF-8?q?rceptor=20app=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/app/WebConfig.java | 23 --------- .../java/nextstep/app/config/WebConfig.java | 47 +++++++++++++++++++ .../AuthenticationException.java | 2 +- .../FormLoginInterceptor.java | 24 +++++----- .../nextstep/security/UserDetailService.java | 7 +++ .../java/nextstep/security/UserDetails.java | 9 ++++ 6 files changed, 75 insertions(+), 37 deletions(-) delete mode 100644 src/main/java/nextstep/app/WebConfig.java create mode 100644 src/main/java/nextstep/app/config/WebConfig.java rename src/main/java/nextstep/{app/ui => security}/AuthenticationException.java (89%) rename src/main/java/nextstep/{app => security}/FormLoginInterceptor.java (58%) create mode 100644 src/main/java/nextstep/security/UserDetailService.java create mode 100644 src/main/java/nextstep/security/UserDetails.java diff --git a/src/main/java/nextstep/app/WebConfig.java b/src/main/java/nextstep/app/WebConfig.java deleted file mode 100644 index 58fedfa3..00000000 --- a/src/main/java/nextstep/app/WebConfig.java +++ /dev/null @@ -1,23 +0,0 @@ -package nextstep.app; - -import nextstep.app.domain.MemberRepository; -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 MemberRepository memberRepository; - - public WebConfig(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new FormLoginInterceptor(memberRepository)).addPathPatterns("/login"); - registry.addInterceptor(new BasicAuthInterceptor(memberRepository)).addPathPatterns("/members"); - } - -} diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java new file mode 100644 index 00000000..f73ed509 --- /dev/null +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -0,0 +1,47 @@ +package nextstep.app.config; + +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.security.FormLoginInterceptor; +import nextstep.security.UserDetailService; +import nextstep.security.UserDetails; +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; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final MemberRepository memberRepository; + + public WebConfig(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new FormLoginInterceptor(userDetailService())).addPathPatterns("/login"); +// registry.addInterceptor(new BasicAuthInterceptor(userDetailService)).addPathPatterns("/members"); + } + + @Bean + public UserDetailService userDetailService() { + return username -> { + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new IllegalArgumentException("해당하는 사용자를 찾을 수 없습니다.")); + 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/ui/AuthenticationException.java b/src/main/java/nextstep/security/AuthenticationException.java similarity index 89% rename from src/main/java/nextstep/app/ui/AuthenticationException.java rename to src/main/java/nextstep/security/AuthenticationException.java index 61aca430..2e29e958 100644 --- a/src/main/java/nextstep/app/ui/AuthenticationException.java +++ b/src/main/java/nextstep/security/AuthenticationException.java @@ -1,4 +1,4 @@ -package nextstep.app.ui; +package nextstep.security; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; diff --git a/src/main/java/nextstep/app/FormLoginInterceptor.java b/src/main/java/nextstep/security/FormLoginInterceptor.java similarity index 58% rename from src/main/java/nextstep/app/FormLoginInterceptor.java rename to src/main/java/nextstep/security/FormLoginInterceptor.java index c1c71660..56bb0315 100644 --- a/src/main/java/nextstep/app/FormLoginInterceptor.java +++ b/src/main/java/nextstep/security/FormLoginInterceptor.java @@ -1,21 +1,18 @@ -package nextstep.app; +package nextstep.security; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; -import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; -import nextstep.app.ui.AuthenticationException; import org.springframework.web.servlet.HandlerInterceptor; public class FormLoginInterceptor implements HandlerInterceptor { private static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - private final MemberRepository memberRepository; + private final UserDetailService userDetailService; - public FormLoginInterceptor(MemberRepository memberRepository) { - this.memberRepository = memberRepository; + public FormLoginInterceptor(UserDetailService userDetailService) { + this.userDetailService = userDetailService; } @Override @@ -24,20 +21,21 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons String username = request.getParameter("username"); String password = request.getParameter("password"); - Member member = memberRepository.findByEmail(username) - .filter(it -> it.matchPassword(password)) - .orElseThrow(AuthenticationException::new); + UserDetails userDetail = userDetailService.getUserByUsername(username); + if (!userDetail.getPassword().equals(password)) { + throw new AuthenticationException(); + } - addMemberToSession(request, member); + addMemberToSession(request, userDetail); }catch (Exception e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); } return false; } - private void addMemberToSession(HttpServletRequest request, Member member) { + private void addMemberToSession(HttpServletRequest request, UserDetails userDetail) { HttpSession session = request.getSession(); - session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetail); } } diff --git a/src/main/java/nextstep/security/UserDetailService.java b/src/main/java/nextstep/security/UserDetailService.java new file mode 100644 index 00000000..7de7b4dd --- /dev/null +++ b/src/main/java/nextstep/security/UserDetailService.java @@ -0,0 +1,7 @@ +package nextstep.security; + +public interface UserDetailService { + + UserDetails getUserByUsername(String username); + +} diff --git a/src/main/java/nextstep/security/UserDetails.java b/src/main/java/nextstep/security/UserDetails.java new file mode 100644 index 00000000..29331ea1 --- /dev/null +++ b/src/main/java/nextstep/security/UserDetails.java @@ -0,0 +1,9 @@ +package nextstep.security; + +public interface UserDetails { + + String getUsername(); + + String getPassword(); + +} From c6015ebe5b2cd6b86c4e2e89ef60ba5d67210ab2 Mon Sep 17 00:00:00 2001 From: juno-junho Date: Mon, 3 Feb 2025 23:00:22 +0900 Subject: [PATCH 04/24] =?UTF-8?q?refactor(Authentication):=20BasicAuthInte?= =?UTF-8?q?rceptor=20app=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/config/WebConfig.java | 3 ++- .../util => security}/Base64Convertor.java | 2 +- .../BasicAuthInterceptor.java | 18 ++++++++---------- src/test/java/nextstep/app/BasicAuthTest.java | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) rename src/main/java/nextstep/{app/util => security}/Base64Convertor.java (91%) rename src/main/java/nextstep/{app => security}/BasicAuthInterceptor.java (71%) diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index f73ed509..cce38eca 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -2,6 +2,7 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; +import nextstep.security.BasicAuthInterceptor; import nextstep.security.FormLoginInterceptor; import nextstep.security.UserDetailService; import nextstep.security.UserDetails; @@ -22,7 +23,7 @@ public WebConfig(MemberRepository memberRepository) { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new FormLoginInterceptor(userDetailService())).addPathPatterns("/login"); -// registry.addInterceptor(new BasicAuthInterceptor(userDetailService)).addPathPatterns("/members"); + registry.addInterceptor(new BasicAuthInterceptor(userDetailService())).addPathPatterns("/members"); } @Bean diff --git a/src/main/java/nextstep/app/util/Base64Convertor.java b/src/main/java/nextstep/security/Base64Convertor.java similarity index 91% rename from src/main/java/nextstep/app/util/Base64Convertor.java rename to src/main/java/nextstep/security/Base64Convertor.java index 12eb1831..7930f84b 100644 --- a/src/main/java/nextstep/app/util/Base64Convertor.java +++ b/src/main/java/nextstep/security/Base64Convertor.java @@ -1,4 +1,4 @@ -package nextstep.app.util; +package nextstep.security; import java.util.Base64; diff --git a/src/main/java/nextstep/app/BasicAuthInterceptor.java b/src/main/java/nextstep/security/BasicAuthInterceptor.java similarity index 71% rename from src/main/java/nextstep/app/BasicAuthInterceptor.java rename to src/main/java/nextstep/security/BasicAuthInterceptor.java index 88382913..c3a6d698 100644 --- a/src/main/java/nextstep/app/BasicAuthInterceptor.java +++ b/src/main/java/nextstep/security/BasicAuthInterceptor.java @@ -1,18 +1,15 @@ -package nextstep.app; +package nextstep.security; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.app.domain.MemberRepository; -import nextstep.app.ui.AuthenticationException; -import nextstep.app.util.Base64Convertor; import org.springframework.web.servlet.HandlerInterceptor; public class BasicAuthInterceptor implements HandlerInterceptor { - private final MemberRepository memberRepository; + private final UserDetailService userDetailService; - public BasicAuthInterceptor(MemberRepository memberRepository) { - this.memberRepository = memberRepository; + public BasicAuthInterceptor(UserDetailService userDetailService) { + this.userDetailService = userDetailService; } @Override @@ -28,9 +25,10 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons String username = usernameAndPassword[0]; String password = usernameAndPassword[1]; - memberRepository.findByEmail(username) - .filter(it -> it.matchPassword(password)) - .orElseThrow(AuthenticationException::new); + UserDetails userDetail = userDetailService.getUserByUsername(username); + if (!userDetail.getPassword().equals(password)) { + throw new AuthenticationException(); + } return true; } catch (Exception e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); diff --git a/src/test/java/nextstep/app/BasicAuthTest.java b/src/test/java/nextstep/app/BasicAuthTest.java index 7a4acdfe..8426e38f 100644 --- a/src/test/java/nextstep/app/BasicAuthTest.java +++ b/src/test/java/nextstep/app/BasicAuthTest.java @@ -2,7 +2,7 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; -import nextstep.app.util.Base64Convertor; +import nextstep.security.Base64Convertor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; From d4a45960e38f35bc95acf78b074565b1ebc0f61b Mon Sep 17 00:00:00 2001 From: juno-junho Date: Mon, 3 Feb 2025 23:03:42 +0900 Subject: [PATCH 05/24] =?UTF-8?q?refactor(Authentication):=20security=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=84=B8=EB=B6=84=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/app/config/WebConfig.java | 4 ++-- .../{ => interceptor}/BasicAuthInterceptor.java | 6 +++++- .../{ => interceptor}/FormLoginInterceptor.java | 5 ++++- .../nextstep/security/{ => util}/Base64Convertor.java | 10 ++++++++-- src/test/java/nextstep/app/BasicAuthTest.java | 2 +- 5 files changed, 20 insertions(+), 7 deletions(-) rename src/main/java/nextstep/security/{ => interceptor}/BasicAuthInterceptor.java (88%) rename src/main/java/nextstep/security/{ => interceptor}/FormLoginInterceptor.java (89%) rename src/main/java/nextstep/security/{ => util}/Base64Convertor.java (64%) diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index cce38eca..6f65a608 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -2,8 +2,8 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; -import nextstep.security.BasicAuthInterceptor; -import nextstep.security.FormLoginInterceptor; +import nextstep.security.interceptor.BasicAuthInterceptor; +import nextstep.security.interceptor.FormLoginInterceptor; import nextstep.security.UserDetailService; import nextstep.security.UserDetails; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/nextstep/security/BasicAuthInterceptor.java b/src/main/java/nextstep/security/interceptor/BasicAuthInterceptor.java similarity index 88% rename from src/main/java/nextstep/security/BasicAuthInterceptor.java rename to src/main/java/nextstep/security/interceptor/BasicAuthInterceptor.java index c3a6d698..b9225f9b 100644 --- a/src/main/java/nextstep/security/BasicAuthInterceptor.java +++ b/src/main/java/nextstep/security/interceptor/BasicAuthInterceptor.java @@ -1,7 +1,11 @@ -package nextstep.security; +package nextstep.security.interceptor; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.AuthenticationException; +import nextstep.security.UserDetailService; +import nextstep.security.UserDetails; +import nextstep.security.util.Base64Convertor; import org.springframework.web.servlet.HandlerInterceptor; public class BasicAuthInterceptor implements HandlerInterceptor { diff --git a/src/main/java/nextstep/security/FormLoginInterceptor.java b/src/main/java/nextstep/security/interceptor/FormLoginInterceptor.java similarity index 89% rename from src/main/java/nextstep/security/FormLoginInterceptor.java rename to src/main/java/nextstep/security/interceptor/FormLoginInterceptor.java index 56bb0315..29337763 100644 --- a/src/main/java/nextstep/security/FormLoginInterceptor.java +++ b/src/main/java/nextstep/security/interceptor/FormLoginInterceptor.java @@ -1,8 +1,11 @@ -package nextstep.security; +package nextstep.security.interceptor; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; +import nextstep.security.AuthenticationException; +import nextstep.security.UserDetailService; +import nextstep.security.UserDetails; import org.springframework.web.servlet.HandlerInterceptor; public class FormLoginInterceptor implements HandlerInterceptor { diff --git a/src/main/java/nextstep/security/Base64Convertor.java b/src/main/java/nextstep/security/util/Base64Convertor.java similarity index 64% rename from src/main/java/nextstep/security/Base64Convertor.java rename to src/main/java/nextstep/security/util/Base64Convertor.java index 7930f84b..1c1f1e38 100644 --- a/src/main/java/nextstep/security/Base64Convertor.java +++ b/src/main/java/nextstep/security/util/Base64Convertor.java @@ -1,8 +1,13 @@ -package nextstep.security; +package nextstep.security.util; import java.util.Base64; -public class Base64Convertor { +public final class Base64Convertor { + + private Base64Convertor() { + throw new AssertionError(); + } + public static String encode(String value) { return Base64.getEncoder().encodeToString(value.getBytes()); } @@ -10,4 +15,5 @@ public static String encode(String value) { public static String decode(String value) { return new String(Base64.getDecoder().decode(value)); } + } diff --git a/src/test/java/nextstep/app/BasicAuthTest.java b/src/test/java/nextstep/app/BasicAuthTest.java index 8426e38f..b5340f18 100644 --- a/src/test/java/nextstep/app/BasicAuthTest.java +++ b/src/test/java/nextstep/app/BasicAuthTest.java @@ -2,7 +2,7 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; -import nextstep.security.Base64Convertor; +import nextstep.security.util.Base64Convertor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; From fb784b09975eae8c9a3e39cad76a67ce0503a9d7 Mon Sep 17 00:00:00 2001 From: juno-junho Date: Tue, 4 Feb 2025 23:36:39 +0900 Subject: [PATCH 06/24] =?UTF-8?q?feat(Authentication):=20BasicAuth=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20interceptor=EC=97=90=EC=84=9C=20filter?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/config/WebConfig.java | 19 ++++++ .../security/filter/BasicAuthFilter.java | 67 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/main/java/nextstep/security/filter/BasicAuthFilter.java diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index 6f65a608..4b9ce98f 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -2,10 +2,13 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; +import nextstep.security.filter.BasicAuthFilter; +import nextstep.security.filter.UsernamePasswordAuthFilter; import nextstep.security.interceptor.BasicAuthInterceptor; import nextstep.security.interceptor.FormLoginInterceptor; import nextstep.security.UserDetailService; import nextstep.security.UserDetails; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; @@ -26,6 +29,22 @@ public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new BasicAuthInterceptor(userDetailService())).addPathPatterns("/members"); } + @Bean + public FilterRegistrationBean usernamePasswordAuthenticationFilterRegister() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new UsernamePasswordAuthFilter(userDetailService())); + registrationBean.addUrlPatterns("/login"); + registrationBean.setOrder(1); + return registrationBean; + } + + @Bean + public FilterRegistrationBean basicAuthenticationFilterRegister() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new BasicAuthFilter(userDetailService())); + registrationBean.addUrlPatterns("/members"); + registrationBean.setOrder(2); + return registrationBean; + } + @Bean public UserDetailService userDetailService() { return username -> { diff --git a/src/main/java/nextstep/security/filter/BasicAuthFilter.java b/src/main/java/nextstep/security/filter/BasicAuthFilter.java new file mode 100644 index 00000000..3148cbf7 --- /dev/null +++ b/src/main/java/nextstep/security/filter/BasicAuthFilter.java @@ -0,0 +1,67 @@ +package nextstep.security.filter; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.AuthenticationException; +import nextstep.security.UserDetailService; +import nextstep.security.UserDetails; +import nextstep.security.util.Base64Convertor; + +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + +public class BasicAuthFilter implements Filter { + + private final UserDetailService userDetailsService; + + public BasicAuthFilter(UserDetailService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException { + boolean isHttpServlet = servletRequest instanceof HttpServletRequest && servletResponse instanceof HttpServletResponse; + if (isHttpServlet) { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + try { + checkAuthentication(request); + filterChain.doFilter(request, response); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + return; + } + + throw new ServletException("BasicAuthFilter only supports HTTP requests"); + } + + private void checkAuthentication(HttpServletRequest request) { + String authorizationHeader = request.getHeader(AUTHORIZATION); + String authType = authorizationHeader.split(" ")[0]; + String credentials = authorizationHeader.split(" ")[1]; + String decodedString = Base64Convertor.decode(credentials); + checkAuthType(authType); + + String[] usernameAndPassword = decodedString.split(":"); + String username = usernameAndPassword[0]; + String password = usernameAndPassword[1]; + + UserDetails userDetail = userDetailsService.getUserByUsername(username); + if (!userDetail.getPassword().equals(password)) { + throw new AuthenticationException(); + } + } + + private void checkAuthType(String authType) { + if (!authType.equalsIgnoreCase(HttpServletRequest.BASIC_AUTH)) { + throw new AuthenticationException(); + } + } + +} From cab8986295fd66877b70b3217b53570b4db733b3 Mon Sep 17 00:00:00 2001 From: juno-junho Date: Wed, 5 Feb 2025 00:06:45 +0900 Subject: [PATCH 07/24] =?UTF-8?q?feat(Authentication):=20FormLogin=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20interceptor=EC=97=90?= =?UTF-8?q?=EC=84=9C=20filter=EB=A1=9C=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/config/WebConfig.java | 29 ++++--- .../security/filter/BasicAuthFilter.java | 1 - .../filter/UsernamePasswordAuthFilter.java | 75 +++++++++++++++++++ 3 files changed, 88 insertions(+), 17 deletions(-) create mode 100644 src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index 4b9ce98f..8b4ece1e 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -2,16 +2,13 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; -import nextstep.security.filter.BasicAuthFilter; -import nextstep.security.filter.UsernamePasswordAuthFilter; -import nextstep.security.interceptor.BasicAuthInterceptor; -import nextstep.security.interceptor.FormLoginInterceptor; import nextstep.security.UserDetailService; import nextstep.security.UserDetails; +import nextstep.security.filter.BasicAuthFilter; +import nextstep.security.filter.UsernamePasswordAuthFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; 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; @Configuration @@ -23,24 +20,24 @@ public WebConfig(MemberRepository memberRepository) { this.memberRepository = memberRepository; } - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new FormLoginInterceptor(userDetailService())).addPathPatterns("/login"); - registry.addInterceptor(new BasicAuthInterceptor(userDetailService())).addPathPatterns("/members"); - } +// @Override +// public void addInterceptors(InterceptorRegistry registry) { +// registry.addInterceptor(new FormLoginInterceptor(userDetailService())).addPathPatterns("/login"); +// registry.addInterceptor(new BasicAuthInterceptor(userDetailService())).addPathPatterns("/members"); +// } @Bean - public FilterRegistrationBean usernamePasswordAuthenticationFilterRegister() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new UsernamePasswordAuthFilter(userDetailService())); - registrationBean.addUrlPatterns("/login"); + public FilterRegistrationBean basicAuthFilterRegister() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new BasicAuthFilter(userDetailService())); + registrationBean.addUrlPatterns("/member"); registrationBean.setOrder(1); return registrationBean; } @Bean - public FilterRegistrationBean basicAuthenticationFilterRegister() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new BasicAuthFilter(userDetailService())); - registrationBean.addUrlPatterns("/members"); + public FilterRegistrationBean usernamePasswordAuthFilterRegister() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new UsernamePasswordAuthFilter(userDetailService())); + registrationBean.addUrlPatterns("/login", "/login/*"); registrationBean.setOrder(2); return registrationBean; } diff --git a/src/main/java/nextstep/security/filter/BasicAuthFilter.java b/src/main/java/nextstep/security/filter/BasicAuthFilter.java index 3148cbf7..f92186e1 100644 --- a/src/main/java/nextstep/security/filter/BasicAuthFilter.java +++ b/src/main/java/nextstep/security/filter/BasicAuthFilter.java @@ -37,7 +37,6 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo } return; } - throw new ServletException("BasicAuthFilter only supports HTTP requests"); } diff --git a/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java b/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java new file mode 100644 index 00000000..d2e9a571 --- /dev/null +++ b/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java @@ -0,0 +1,75 @@ +package nextstep.security.filter; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import nextstep.security.AuthenticationException; +import nextstep.security.UserDetailService; +import nextstep.security.UserDetails; + +import java.io.IOException; +import java.util.List; + +public class UsernamePasswordAuthFilter implements Filter { + + private static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + private static final List targetURIList = List.of("/login"); + + private final UserDetailService userDetailService; + + public UsernamePasswordAuthFilter(UserDetailService userDetailService) { + this.userDetailService = userDetailService; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + boolean isHttpServlet = servletRequest instanceof HttpServletRequest && servletResponse instanceof HttpServletResponse; + if (isHttpServlet) { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + boolean isNotUsernamePasswordAuthTarget = checkIfAuthTarget(request); + if (isNotUsernamePasswordAuthTarget) { + filterChain.doFilter(request, response); + return; + } + processLogin(request, response); + return; + } + throw new ServletException("UsernamePasswordAuthFilter only supports HTTP requests"); + } + + private boolean checkIfAuthTarget(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + return targetURIList.stream() + .filter(requestURI::startsWith) + .findAny() + .isEmpty(); + } + + private void processLogin(HttpServletRequest request, HttpServletResponse response) { + try { + String username = request.getParameter("username"); + String password = request.getParameter("password"); + + UserDetails userDetail = userDetailService.getUserByUsername(username); + if (!userDetail.getPassword().equals(password)) { + throw new AuthenticationException(); + } + addMemberToSession(request, userDetail); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } + + private void addMemberToSession(HttpServletRequest request, UserDetails userDetail) { + HttpSession session = request.getSession(); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetail); + } + +} From 9ddb2d8d85ce91f72fc8b43c13994207af7e590c Mon Sep 17 00:00:00 2001 From: juno-junho Date: Fri, 7 Feb 2025 23:08:19 +0900 Subject: [PATCH 08/24] =?UTF-8?q?feat(Authentication):=20DelegatingFilterP?= =?UTF-8?q?roxy=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/DefaultSecurityFilterChain.java | 24 +++++++++ .../security/config/FilterChainProxy.java | 50 +++++++++++++++++++ .../security/config/SecurityConfig.java | 31 ++++++++++++ .../security/config/SecurityFilterChain.java | 14 ++++++ 4 files changed, 119 insertions(+) create mode 100644 src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java create mode 100644 src/main/java/nextstep/security/config/FilterChainProxy.java create mode 100644 src/main/java/nextstep/security/config/SecurityConfig.java create mode 100644 src/main/java/nextstep/security/config/SecurityFilterChain.java 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..b9bab9ef --- /dev/null +++ b/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java @@ -0,0 +1,24 @@ +package nextstep.security.config; + +import jakarta.servlet.Filter; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.List; + +public class DefaultSecurityFilterChain implements SecurityFilterChain { + + + public DefaultSecurityFilterChain(List of) { + } + + @Override + public List getFilters() { + return List.of(); + } + + @Override + public boolean matches(HttpServletRequest request) { + return false; + } + +} 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..81e26328 --- /dev/null +++ b/src/main/java/nextstep/security/config/FilterChainProxy.java @@ -0,0 +1,50 @@ +package nextstep.security.config; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; +import java.util.List; + +public class FilterChainProxy extends GenericFilterBean { + + private final List securityFilterChains; + + public FilterChainProxy(List securityFilterChains) { + this.securityFilterChains = securityFilterChains; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + + } + + private static class VirtualFilterChain implements FilterChain { + + private final List filters; + private final FilterChain originalChain; + private int currentPosition = 0; + + public VirtualFilterChain(List filters, FilterChain originalChain) { + this.filters = filters; + this.originalChain = originalChain; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + if (currentPosition == filters.size()) { + originalChain.doFilter(request, response); + } else { + Filter nextFilter = filters.get(currentPosition++); + nextFilter.doFilter(request, response, this); + } + } + + } + + +} diff --git a/src/main/java/nextstep/security/config/SecurityConfig.java b/src/main/java/nextstep/security/config/SecurityConfig.java new file mode 100644 index 00000000..bcd05ffb --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityConfig.java @@ -0,0 +1,31 @@ +package nextstep.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.DelegatingFilterProxy; + +import java.util.List; + +@Configuration +public class SecurityConfig { + + @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( + // ... + ) + ); + } + +} 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..8857b94d --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityFilterChain.java @@ -0,0 +1,14 @@ +package nextstep.security.config; + +import jakarta.servlet.Filter; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.List; + +public interface SecurityFilterChain { + + List getFilters(); + + boolean matches(HttpServletRequest request); + +} From 1b974e2e305f61645f6418bb75f41a8682d7d36f Mon Sep 17 00:00:00 2001 From: juno-junho Date: Sat, 8 Feb 2025 01:30:57 +0900 Subject: [PATCH 09/24] =?UTF-8?q?feat(Authentication):=20FilterChainProxy?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/config/FilterChainProxy.java | 57 +++++++++++++++---- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/src/main/java/nextstep/security/config/FilterChainProxy.java b/src/main/java/nextstep/security/config/FilterChainProxy.java index 81e26328..dfcfadd9 100644 --- a/src/main/java/nextstep/security/config/FilterChainProxy.java +++ b/src/main/java/nextstep/security/config/FilterChainProxy.java @@ -5,6 +5,8 @@ import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.filter.GenericFilterBean; import java.io.IOException; @@ -12,39 +14,70 @@ public class FilterChainProxy extends GenericFilterBean { - private final List securityFilterChains; + private final List filterChains; - public FilterChainProxy(List securityFilterChains) { - this.securityFilterChains = securityFilterChains; + private VirtualFilterChainDecorator virtualFilterChainDecorator = new VirtualFilterChainDecorator(); + + public FilterChainProxy(List filterChains) { + this.filterChains = filterChains; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + List filters = getFilters(request); + + if (filters == null || filters.isEmpty()) { + filterChain.doFilter(request, response); + return; + } + this.virtualFilterChainDecorator.decorate(filterChain, filters).doFilter(request, response); + } + + private List getFilters(HttpServletRequest request) { + return this.filterChains.stream() + .filter(chain -> chain.matches(request)) + .findAny() + .map(SecurityFilterChain::getFilters) + .orElse(null); + } + + public static final class VirtualFilterChainDecorator { + + public FilterChain decorate(FilterChain original, List filters) { + return new VirtualFilterChain(original, filters); + } } private static class VirtualFilterChain implements FilterChain { - private final List filters; private final FilterChain originalChain; + private final List additionalFilters; + private final int size; + private int currentPosition = 0; - public VirtualFilterChain(List filters, FilterChain originalChain) { - this.filters = filters; + public 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 (currentPosition == filters.size()) { - originalChain.doFilter(request, response); - } else { - Filter nextFilter = filters.get(currentPosition++); - nextFilter.doFilter(request, response, this); + if (this.currentPosition == this.size) { + this.originalChain.doFilter(request, response); + return; } + this.currentPosition++; + Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1); + + nextFilter.doFilter(request, response, this); } } - } From 6345383720a2a74f855b6790c9b079fd8993eefe Mon Sep 17 00:00:00 2001 From: juno-junho Date: Sat, 8 Feb 2025 02:49:15 +0900 Subject: [PATCH 10/24] =?UTF-8?q?feat(Authentication):=20DefaultSecurityFi?= =?UTF-8?q?lterChain=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/DefaultSecurityFilterChain.java | 23 +++++++++++++++---- .../security/config/RequestMatcher.java | 11 +++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 src/main/java/nextstep/security/config/RequestMatcher.java diff --git a/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java b/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java index b9bab9ef..f023dc2d 100644 --- a/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java +++ b/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java @@ -7,18 +7,31 @@ public class DefaultSecurityFilterChain implements SecurityFilterChain { + private final RequestMatcher requestMatcher; - public DefaultSecurityFilterChain(List of) { + private final List filters; + + public DefaultSecurityFilterChain(List filters) { + this(null, filters); } - @Override - public List getFilters() { - return List.of(); + public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List filters) { + this.requestMatcher = requestMatcher; + this.filters = filters; } @Override public boolean matches(HttpServletRequest request) { - return false; + return true; + } + + @Override + public List getFilters() { + return this.filters; + } + + public RequestMatcher getRequestMatcher() { + return requestMatcher; } } diff --git a/src/main/java/nextstep/security/config/RequestMatcher.java b/src/main/java/nextstep/security/config/RequestMatcher.java new file mode 100644 index 00000000..8f4056db --- /dev/null +++ b/src/main/java/nextstep/security/config/RequestMatcher.java @@ -0,0 +1,11 @@ +package nextstep.security.config; + + +import jakarta.servlet.http.HttpServletRequest; + +public interface RequestMatcher { + + boolean matches(HttpServletRequest request); + + +} From b0bf522c29cfdae94ceca2b295f8e737ef658b1b Mon Sep 17 00:00:00 2001 From: juno-junho Date: Sat, 8 Feb 2025 02:49:45 +0900 Subject: [PATCH 11/24] =?UTF-8?q?feat(Authentication):=20web=20config=20->?= =?UTF-8?q?=20security=20=EC=84=A4=EC=A0=95=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/config/WebConfig.java | 33 +++++++++---------- .../security/config/SecurityConfig.java | 33 +++++++++++++++---- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index 8b4ece1e..03ffa439 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -4,9 +4,6 @@ import nextstep.app.domain.MemberRepository; import nextstep.security.UserDetailService; import nextstep.security.UserDetails; -import nextstep.security.filter.BasicAuthFilter; -import nextstep.security.filter.UsernamePasswordAuthFilter; -import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -26,21 +23,21 @@ public WebConfig(MemberRepository memberRepository) { // registry.addInterceptor(new BasicAuthInterceptor(userDetailService())).addPathPatterns("/members"); // } - @Bean - public FilterRegistrationBean basicAuthFilterRegister() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new BasicAuthFilter(userDetailService())); - registrationBean.addUrlPatterns("/member"); - registrationBean.setOrder(1); - return registrationBean; - } - - @Bean - public FilterRegistrationBean usernamePasswordAuthFilterRegister() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new UsernamePasswordAuthFilter(userDetailService())); - registrationBean.addUrlPatterns("/login", "/login/*"); - registrationBean.setOrder(2); - return registrationBean; - } +// @Bean +// public FilterRegistrationBean basicAuthFilterRegister() { +// FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new BasicAuthFilter(userDetailService())); +// registrationBean.addUrlPatterns("/member"); +// registrationBean.setOrder(1); +// return registrationBean; +// } +// +// @Bean +// public FilterRegistrationBean usernamePasswordAuthFilterRegister() { +// FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new UsernamePasswordAuthFilter(userDetailService())); +// registrationBean.addUrlPatterns("/login", "/login/*"); +// registrationBean.setOrder(2); +// return registrationBean; +// } @Bean public UserDetailService userDetailService() { diff --git a/src/main/java/nextstep/security/config/SecurityConfig.java b/src/main/java/nextstep/security/config/SecurityConfig.java index bcd05ffb..c0ef3d14 100644 --- a/src/main/java/nextstep/security/config/SecurityConfig.java +++ b/src/main/java/nextstep/security/config/SecurityConfig.java @@ -1,7 +1,13 @@ package nextstep.security.config; +import jakarta.servlet.Filter; +import nextstep.security.UserDetailService; +import nextstep.security.filter.BasicAuthFilter; +import nextstep.security.filter.UsernamePasswordAuthFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; import org.springframework.web.filter.DelegatingFilterProxy; import java.util.List; @@ -9,23 +15,38 @@ @Configuration public class SecurityConfig { + private final UserDetailService userDetailsService; + + public SecurityConfig(UserDetailService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Bean + public FilterRegistrationBean delegatingFilterProxyFilterRegister() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new DelegatingFilterProxy("filterChainProxy")); + registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); + return registrationBean; + } + @Bean public DelegatingFilterProxy delegatingFilterProxy() { - return new DelegatingFilterProxy(filterChainProxy(List.of(securityFilterChain()))); + return new DelegatingFilterProxy(filterChainProxy()); } @Bean - public FilterChainProxy filterChainProxy(List securityFilterChains) { + public FilterChainProxy filterChainProxy() { + List securityFilterChains = List.of(securityFilterChain()); return new FilterChainProxy(securityFilterChains); } @Bean public SecurityFilterChain securityFilterChain() { - return new DefaultSecurityFilterChain( - List.of( - // ... - ) + List securityFilters = List.of( + new BasicAuthFilter(userDetailsService), + new UsernamePasswordAuthFilter(userDetailsService) ); + return new DefaultSecurityFilterChain(securityFilters); } } From 03d0cbd93241767485b54fef8b5436d371288482 Mon Sep 17 00:00:00 2001 From: juno-junho Date: Sat, 8 Feb 2025 03:00:01 +0900 Subject: [PATCH 12/24] =?UTF-8?q?fix(Authentication):=20web=20config?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20bean=20=EC=84=A4=EC=A0=95=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20(1=EB=8B=A8=EA=B3=84=20=EB=81=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/config/WebConfig.java | 49 +++++++++-------- .../security/config/SecurityConfig.java | 52 ------------------- .../security/filter/BasicAuthFilter.java | 23 +++++++- 3 files changed, 50 insertions(+), 74 deletions(-) delete mode 100644 src/main/java/nextstep/security/config/SecurityConfig.java diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index 03ffa439..aba8bdb8 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -1,13 +1,22 @@ package nextstep.app.config; +import jakarta.servlet.Filter; import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import nextstep.security.UserDetailService; import nextstep.security.UserDetails; +import nextstep.security.config.DefaultSecurityFilterChain; +import nextstep.security.config.FilterChainProxy; +import nextstep.security.config.SecurityFilterChain; +import nextstep.security.filter.BasicAuthFilter; +import nextstep.security.filter.UsernamePasswordAuthFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.DelegatingFilterProxy; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.List; + @Configuration public class WebConfig implements WebMvcConfigurer { @@ -17,27 +26,25 @@ public WebConfig(MemberRepository memberRepository) { this.memberRepository = memberRepository; } -// @Override -// public void addInterceptors(InterceptorRegistry registry) { -// registry.addInterceptor(new FormLoginInterceptor(userDetailService())).addPathPatterns("/login"); -// registry.addInterceptor(new BasicAuthInterceptor(userDetailService())).addPathPatterns("/members"); -// } - -// @Bean -// public FilterRegistrationBean basicAuthFilterRegister() { -// FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new BasicAuthFilter(userDetailService())); -// registrationBean.addUrlPatterns("/member"); -// registrationBean.setOrder(1); -// return registrationBean; -// } -// -// @Bean -// public FilterRegistrationBean usernamePasswordAuthFilterRegister() { -// FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new UsernamePasswordAuthFilter(userDetailService())); -// registrationBean.addUrlPatterns("/login", "/login/*"); -// registrationBean.setOrder(2); -// return registrationBean; -// } + @Bean + public DelegatingFilterProxy delegatingFilterProxy() { + return new DelegatingFilterProxy(filterChainProxy()); + } + + @Bean + public FilterChainProxy filterChainProxy() { + List securityFilterChains = List.of(securityFilterChain()); + return new FilterChainProxy(securityFilterChains); + } + + @Bean + public SecurityFilterChain securityFilterChain() { + List securityFilters = List.of( + new BasicAuthFilter(userDetailService()), + new UsernamePasswordAuthFilter(userDetailService()) + ); + return new DefaultSecurityFilterChain(securityFilters); + } @Bean public UserDetailService userDetailService() { diff --git a/src/main/java/nextstep/security/config/SecurityConfig.java b/src/main/java/nextstep/security/config/SecurityConfig.java deleted file mode 100644 index c0ef3d14..00000000 --- a/src/main/java/nextstep/security/config/SecurityConfig.java +++ /dev/null @@ -1,52 +0,0 @@ -package nextstep.security.config; - -import jakarta.servlet.Filter; -import nextstep.security.UserDetailService; -import nextstep.security.filter.BasicAuthFilter; -import nextstep.security.filter.UsernamePasswordAuthFilter; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; -import org.springframework.web.filter.DelegatingFilterProxy; - -import java.util.List; - -@Configuration -public class SecurityConfig { - - private final UserDetailService userDetailsService; - - public SecurityConfig(UserDetailService userDetailsService) { - this.userDetailsService = userDetailsService; - } - - @Bean - public FilterRegistrationBean delegatingFilterProxyFilterRegister() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new DelegatingFilterProxy("filterChainProxy")); - registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); - return registrationBean; - } - - @Bean - public DelegatingFilterProxy delegatingFilterProxy() { - return new DelegatingFilterProxy(filterChainProxy()); - } - - @Bean - public FilterChainProxy filterChainProxy() { - List securityFilterChains = List.of(securityFilterChain()); - return new FilterChainProxy(securityFilterChains); - } - - @Bean - public SecurityFilterChain securityFilterChain() { - List securityFilters = List.of( - new BasicAuthFilter(userDetailsService), - new UsernamePasswordAuthFilter(userDetailsService) - ); - return new DefaultSecurityFilterChain(securityFilters); - } - -} diff --git a/src/main/java/nextstep/security/filter/BasicAuthFilter.java b/src/main/java/nextstep/security/filter/BasicAuthFilter.java index f92186e1..78a27d86 100644 --- a/src/main/java/nextstep/security/filter/BasicAuthFilter.java +++ b/src/main/java/nextstep/security/filter/BasicAuthFilter.java @@ -12,10 +12,14 @@ import nextstep.security.UserDetails; import nextstep.security.util.Base64Convertor; +import java.util.List; + import static org.springframework.http.HttpHeaders.AUTHORIZATION; public class BasicAuthFilter implements Filter { + private static final List TARGET_URL = List.of("/members"); + private final UserDetailService userDetailsService; public BasicAuthFilter(UserDetailService userDetailsService) { @@ -30,6 +34,12 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo HttpServletResponse response = (HttpServletResponse) servletResponse; try { + boolean notTarget = isNotTarget(request); + if (notTarget) { + filterChain.doFilter(request, response); + return; + } + checkAuthentication(request); filterChain.doFilter(request, response); } catch (Exception e) { @@ -40,8 +50,19 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo throw new ServletException("BasicAuthFilter only supports HTTP requests"); } + private boolean isNotTarget(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + return TARGET_URL.stream() + .filter(requestURI::startsWith) + .findAny() + .isEmpty(); + } + private void checkAuthentication(HttpServletRequest request) { String authorizationHeader = request.getHeader(AUTHORIZATION); + if (authorizationHeader == null) { + return; + } String authType = authorizationHeader.split(" ")[0]; String credentials = authorizationHeader.split(" ")[1]; String decodedString = Base64Convertor.decode(credentials); @@ -58,7 +79,7 @@ private void checkAuthentication(HttpServletRequest request) { } private void checkAuthType(String authType) { - if (!authType.equalsIgnoreCase(HttpServletRequest.BASIC_AUTH)) { + if (!HttpServletRequest.BASIC_AUTH.equalsIgnoreCase(authType)) { throw new AuthenticationException(); } } From 2bce0cf9877b6ad87a62bb7a30747e0ce1e1603a Mon Sep 17 00:00:00 2001 From: juno-junho Date: Sat, 8 Feb 2025 23:15:30 +0900 Subject: [PATCH 13/24] =?UTF-8?q?feat(Authentication):=20Authentication=20?= =?UTF-8?q?=EC=99=80=20UsernamePasswordAuthenticationToken=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/AbstractAuthenticationToken.java | 32 ++++++++ .../security/config/Authentication.java | 81 +++++++++++++++++++ .../UsernamePasswordAuthenticationToken.java | 42 ++++++++++ 3 files changed, 155 insertions(+) create mode 100644 src/main/java/nextstep/security/config/AbstractAuthenticationToken.java create mode 100644 src/main/java/nextstep/security/config/Authentication.java create mode 100644 src/main/java/nextstep/security/config/UsernamePasswordAuthenticationToken.java diff --git a/src/main/java/nextstep/security/config/AbstractAuthenticationToken.java b/src/main/java/nextstep/security/config/AbstractAuthenticationToken.java new file mode 100644 index 00000000..ac8cfd41 --- /dev/null +++ b/src/main/java/nextstep/security/config/AbstractAuthenticationToken.java @@ -0,0 +1,32 @@ +package nextstep.security.config; + +import nextstep.security.UserDetails; + +import java.security.Principal; + +// Implementations which use this class should be immutable. +public abstract class AbstractAuthenticationToken implements Authentication{ + + private Object details; + + @Override + public String getName() { + if (this.getPrincipal() instanceof UserDetails userDetails) { + return userDetails.getUsername(); + } + if (this.getPrincipal() instanceof Principal principal) { + return principal.getName(); + } + return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString(); + } + + public void setDetails(Object details) { + this.details = details; + } + + @Override + public Object getDetails() { + return this.details; + } + +} diff --git a/src/main/java/nextstep/security/config/Authentication.java b/src/main/java/nextstep/security/config/Authentication.java new file mode 100644 index 00000000..ad134914 --- /dev/null +++ b/src/main/java/nextstep/security/config/Authentication.java @@ -0,0 +1,81 @@ +package nextstep.security.config; + + +import java.io.Serializable; +import java.security.Principal; + +/** + * 일단 요청이 AuthenticationManager의 authenticate(Authentication) 메서드에 의해서 진행된다면 + * 인증 요청이나 authenticated principaldㅔ 대한 토큰을 나타낸다. + + * 일단 request가 authenticated 되면, 이 Authentication 객체는 SecurityContextHolder의 SecurityContext의 threadlocal에 저장된다. + * spring security의 authentication 메커니즘을 사용하지 않고 아래와 같이 Authentication 인스턴스를 생성해서 명시적으로 사용가능하다 + *
+ * SecurityContext context = SecurityContextHolder.createEmptyContext();
+ * context.setAuthentication(anAuthentication);
+ * SecurityContextHolder.setContext(context);
+ * 
+ * + * Authentication 객체가 authenticated 프로퍼티 값이 true로 지정되지 않는한, + * 만나는 security interceptor마다 인증된다. + + * 대부분의 경우에 framework가 security context와 Authentication 객체를 를 투명하게 관리해줄것이다. + **/ +public interface Authentication extends Principal, Serializable { + + /** + * principal이 올바른지 확인하는 crendentials. + * 주로 password이나 AuthenticationManager와 관련된 어느것이든 가능하다. + * caller가 이 credentials를 채운다. + * + * @return the credentials that prove the identity of the principal + */ + Object getCredentials(); + + /** + * 인증 요청에 대한 추가적인 details를 저장한다. + * IP 주소나 인증서 일련번호 등 + * + * @return 인증 요청에 대한 추가적인 details. 사용하지 않으면 null + */ + Object getDetails(); + + /** + * + * 인증되는 principal(주체)의 신원. + * username / password로 인증 요청의 경우, username이 된다. + * AuthenticationManager implementation 은 더많은 정보를 가지고 있는 Authentication를 principal로 반환한다. + * UserDetails 객체를 principal로 사용 + * @return 인증의 대상인 Principal이나 인증된 Principal + */ + Object getPrincipal(); + + /** + * AbstractSecurityInterceptor가 인증 토큰을 AuthenticationManager에게 제시해야 하는지 여부를 나타내는 데 사용된다. + * 일반적으로 AuthenticationManager (AuthenticationProvider중 하나) 는 성공적인 인증 후 불변 인증 토큰을 반환하며, + * 그 경우 토큰이 안전하게 이 method에 true로 반환할 수 있다. + * true를 반환하는 것은 performance를 높이고 AuthenticationManager를 매 요청마다 호출하는것은 더이상 불필요하게 된다. + * + * 보안적인 이유로 이 인터페이스 구현체는 불변이거나 처음 생성때부터 변하지 않는 프로퍼티를 보장하는 방법이 있지 않은한 true를 반환하는 것에 매우 주의해야 한다. + * + * @return true if the token has been authenticated and the + * AbstractSecurityInterceptor does not need to present the token to the + * AuthenticationManager again for re-authentication. + */ + boolean isAuthenticated(); + + /** + *

+ * Implementations should always allow this method to be called with a + * false parameter, as this is used by various classes to specify the + * authentication token should not be trusted. If an implementation wishes to reject + * an invocation with a true parameter (which would indicate the + * authentication token is trusted - a potential security risk) the implementation + * should throw an {@link IllegalArgumentException}. + * @param isAuthenticated 는 토큰을 신뢰해야하는 경우 일때 true를 반환한다. + * 토큰을 신뢰하지 말아야하는 경우 false를 반환한다. + * @throws IllegalArgumentException | authentication token을 신뢰하도록 만드는 시도가 실패했을경우 IllegalArgumentException 발생 + */ + void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; + +} diff --git a/src/main/java/nextstep/security/config/UsernamePasswordAuthenticationToken.java b/src/main/java/nextstep/security/config/UsernamePasswordAuthenticationToken.java new file mode 100644 index 00000000..85ba7873 --- /dev/null +++ b/src/main/java/nextstep/security/config/UsernamePasswordAuthenticationToken.java @@ -0,0 +1,42 @@ +package nextstep.security.config; + +import nextstep.security.UserDetails; + +public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { + + private final String principal; + private final String credentials; + private boolean authenticated = false; + + public UsernamePasswordAuthenticationToken(String principal, String credentials, UserDetails userDetails) { + this.principal = principal; + this.credentials = credentials; + setDetails(userDetails); + } + + @Override + public Object getCredentials() { + return this.credentials; + } + + @Override + public Object getDetails() { + return super.getDetails(); + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + @Override + public boolean isAuthenticated() { + return this.authenticated; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + this.authenticated = isAuthenticated; + } + +} From 1a2d400687ab262a01ff6b51c1129b9e2257dadc Mon Sep 17 00:00:00 2001 From: juno-junho Date: Sun, 9 Feb 2025 00:14:24 +0900 Subject: [PATCH 14/24] =?UTF-8?q?feat(Authentication):=20AuthenticationMan?= =?UTF-8?q?ager=20=EA=B5=AC=ED=98=84=EC=B2=B4=EC=9D=B8=20ProviderManager?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20&=20=EA=B4=80=EB=A0=A8=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/AuthenticationException.java | 10 +++++ .../security/ProviderNotFoundException.java | 9 ++++ .../config/AuthenticationManager.java | 7 +++ .../config/AuthenticationProvider.java | 29 ++++++++++++ .../security/config/ProviderManager.java | 44 +++++++++++++++++++ 5 files changed, 99 insertions(+) create mode 100644 src/main/java/nextstep/security/ProviderNotFoundException.java create mode 100644 src/main/java/nextstep/security/config/AuthenticationManager.java create mode 100644 src/main/java/nextstep/security/config/AuthenticationProvider.java create mode 100644 src/main/java/nextstep/security/config/ProviderManager.java diff --git a/src/main/java/nextstep/security/AuthenticationException.java b/src/main/java/nextstep/security/AuthenticationException.java index 2e29e958..f45eb78b 100644 --- a/src/main/java/nextstep/security/AuthenticationException.java +++ b/src/main/java/nextstep/security/AuthenticationException.java @@ -5,4 +5,14 @@ @ResponseStatus(code = HttpStatus.UNAUTHORIZED) public class AuthenticationException extends RuntimeException { + + public AuthenticationException() { + super(); + } + + public AuthenticationException(String message) { + super(message); + + } + } diff --git a/src/main/java/nextstep/security/ProviderNotFoundException.java b/src/main/java/nextstep/security/ProviderNotFoundException.java new file mode 100644 index 00000000..c4f93244 --- /dev/null +++ b/src/main/java/nextstep/security/ProviderNotFoundException.java @@ -0,0 +1,9 @@ +package nextstep.security; + +public class ProviderNotFoundException extends AuthenticationException { + + public ProviderNotFoundException(String message) { + super(message); + } + +} diff --git a/src/main/java/nextstep/security/config/AuthenticationManager.java b/src/main/java/nextstep/security/config/AuthenticationManager.java new file mode 100644 index 00000000..65c598d2 --- /dev/null +++ b/src/main/java/nextstep/security/config/AuthenticationManager.java @@ -0,0 +1,7 @@ +package nextstep.security.config; + +public interface AuthenticationManager { + + Authentication authenticate(Authentication authentication); + +} diff --git a/src/main/java/nextstep/security/config/AuthenticationProvider.java b/src/main/java/nextstep/security/config/AuthenticationProvider.java new file mode 100644 index 00000000..3a306d8d --- /dev/null +++ b/src/main/java/nextstep/security/config/AuthenticationProvider.java @@ -0,0 +1,29 @@ +package nextstep.security.config; + +import nextstep.security.AuthenticationException; + +public interface AuthenticationProvider { + + /** + * AuthenticationManager의 authenticate 메서드와 같은 contract으로 인증 수행 + + * @param authentication : 인증 요청 객체 + * @return credential을 포함한 완전히 authenticated 객체를 반환. + * AuthenticationProvider가 받은 Authentication 객체에 대한 인증을 지원하지 않으면 지원null 을 반환 할 수 있음. + * 그러면 Authentication을 지원하는 그 다음 AuthenticationProvider가 시도된다. + */ + Authentication authenticate(Authentication authentication) throws AuthenticationException; + + /** + * + * AuthenticationProvider가 Authentication를 support 하면 true 반환 + * true를 반환하는것이 AuthenticationProvider가 인증할 수 있을것이라고 보장하는 것이 아님. 그냥 더 자세히 평가를 지원한다는 의미. + * 다른 AuthenticationProvider를 시도해봐야한다는 의미로 AuthenticationProvider는 authenticate()결과를 null로 반환 + * 인증 가능한 AuthenticationProvider 선택은 런타임에 ProviderManager에 의해서 이루어짐 + * + * @param authentication + * @return true if the implementation can more closely evaluate the Authentication class presented + */ + boolean supports(Class authentication); + +} diff --git a/src/main/java/nextstep/security/config/ProviderManager.java b/src/main/java/nextstep/security/config/ProviderManager.java new file mode 100644 index 00000000..e38c47e9 --- /dev/null +++ b/src/main/java/nextstep/security/config/ProviderManager.java @@ -0,0 +1,44 @@ +package nextstep.security.config; + +import nextstep.security.ProviderNotFoundException; + +import java.util.Collections; +import java.util.List; + +public class ProviderManager implements AuthenticationManager { + + private List providers = Collections.emptyList(); + + public ProviderManager(List providers) { + this.providers = providers; + } + + @Override + public Authentication authenticate(Authentication authentication) { + Class authenticationClassToTest = authentication.getClass(); + Authentication result = null; + for (AuthenticationProvider provider : this.providers) { + if (!provider.supports(authenticationClassToTest)) { + continue; + } + result = provider.authenticate(authentication); + if (result != null) { // 인증된 객제가 있다면 + copyDetails(authentication, result); + break; + } + } + if (result == null) { + throw new ProviderNotFoundException("지원하는 provider가 없습니다!"); + } + return result; + } + + // 인증된 토큰에 detail 값이 없다면 detail 넣어주기 + private void copyDetails(Authentication source, Authentication dest) { + if ((dest instanceof AbstractAuthenticationToken token) && (dest.getDetails() == null)) { + token.setDetails(source.getDetails()); + } + } + +} + From bbc61a0452af9646fe3ccacb88d69c5fa8efdc2e Mon Sep 17 00:00:00 2001 From: juno-junho Date: Sun, 9 Feb 2025 00:50:44 +0900 Subject: [PATCH 15/24] =?UTF-8?q?feat(Authentication):=20AuthenticationPro?= =?UTF-8?q?vider=20=EA=B5=AC=ED=98=84=EC=B2=B4=EC=9D=B8=20DaoAuthenticatio?= =?UTF-8?q?nProvider=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/DaoAuthenticationProvider.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/main/java/nextstep/security/config/DaoAuthenticationProvider.java diff --git a/src/main/java/nextstep/security/config/DaoAuthenticationProvider.java b/src/main/java/nextstep/security/config/DaoAuthenticationProvider.java new file mode 100644 index 00000000..83d69de6 --- /dev/null +++ b/src/main/java/nextstep/security/config/DaoAuthenticationProvider.java @@ -0,0 +1,42 @@ +package nextstep.security.config; + +import nextstep.security.AuthenticationException; +import nextstep.security.UserDetailService; +import nextstep.security.UserDetails; + +public class DaoAuthenticationProvider implements AuthenticationProvider{ + + private final UserDetailService userDetailService; + private final PasswordEncoder passwordEncoder; + + public DaoAuthenticationProvider(UserDetailService userDetailService, PasswordEncoder passwordEncoder) { + this.userDetailService = userDetailService; + this.passwordEncoder = passwordEncoder; + } + + public DaoAuthenticationProvider(UserDetailService userDetailService) { + this.userDetailService = userDetailService; + this.passwordEncoder = null; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (!supports(authentication.getClass())) { + return null; + } + String username = (String) authentication.getPrincipal(); + String password = (String) authentication.getCredentials(); + + UserDetails user = userDetailService.getUserByUsername(username); + if (!password.equals(user.getPassword())) { + throw new AuthenticationException("Passwords do not match"); + } + return new UsernamePasswordAuthenticationToken(username, password, user); + } + + @Override + public boolean supports(Class authentication) { // todo 왜 instanceOf가 아닌 이렇게 클래스로 비교를 하는걸까? + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); + } + +} From cf518f4893244b454384f1a4bbfcaaa4dd647e61 Mon Sep 17 00:00:00 2001 From: juno-junho Date: Sun, 9 Feb 2025 01:34:04 +0900 Subject: [PATCH 16/24] =?UTF-8?q?feat(Authentication):=20UsernamePasswordA?= =?UTF-8?q?uthFilter=20=EC=9D=B8=EC=A6=9D=EB=A1=9C=EC=A7=81=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/config/WebConfig.java | 17 +++++++++++++++- .../security/config/PasswordEncoder.java | 14 +++++++++++++ .../UsernamePasswordAuthenticationToken.java | 5 +++++ .../filter/UsernamePasswordAuthFilter.java | 20 +++++++++++-------- 4 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 src/main/java/nextstep/security/config/PasswordEncoder.java diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index aba8bdb8..975120cf 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -5,8 +5,12 @@ import nextstep.app.domain.MemberRepository; import nextstep.security.UserDetailService; import nextstep.security.UserDetails; +import nextstep.security.config.AuthenticationManager; +import nextstep.security.config.AuthenticationProvider; +import nextstep.security.config.DaoAuthenticationProvider; import nextstep.security.config.DefaultSecurityFilterChain; import nextstep.security.config.FilterChainProxy; +import nextstep.security.config.ProviderManager; import nextstep.security.config.SecurityFilterChain; import nextstep.security.filter.BasicAuthFilter; import nextstep.security.filter.UsernamePasswordAuthFilter; @@ -41,11 +45,22 @@ public FilterChainProxy filterChainProxy() { public SecurityFilterChain securityFilterChain() { List securityFilters = List.of( new BasicAuthFilter(userDetailService()), - new UsernamePasswordAuthFilter(userDetailService()) + new UsernamePasswordAuthFilter(authenticationManager()) ); return new DefaultSecurityFilterChain(securityFilters); } + @Bean + public AuthenticationManager authenticationManager() { + List providers = List.of(daoAuthenticationProvider()); + return new ProviderManager(providers); + } + + @Bean + public DaoAuthenticationProvider daoAuthenticationProvider() { + return new DaoAuthenticationProvider(userDetailService()); + } + @Bean public UserDetailService userDetailService() { return username -> { diff --git a/src/main/java/nextstep/security/config/PasswordEncoder.java b/src/main/java/nextstep/security/config/PasswordEncoder.java new file mode 100644 index 00000000..73031645 --- /dev/null +++ b/src/main/java/nextstep/security/config/PasswordEncoder.java @@ -0,0 +1,14 @@ +package nextstep.security.config; + +public interface PasswordEncoder { + + String encode(CharSequence rawPassword); + + /** + * @param rawPassword the raw password to encode and match + * @param encodedPassword the encoded password from storage to compare with + * @return true if the raw password, after encoding, matches the encoded password from storage + */ + boolean matches(CharSequence rawPassword, String encodedPassword); + +} diff --git a/src/main/java/nextstep/security/config/UsernamePasswordAuthenticationToken.java b/src/main/java/nextstep/security/config/UsernamePasswordAuthenticationToken.java index 85ba7873..e979b830 100644 --- a/src/main/java/nextstep/security/config/UsernamePasswordAuthenticationToken.java +++ b/src/main/java/nextstep/security/config/UsernamePasswordAuthenticationToken.java @@ -8,6 +8,11 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT private final String credentials; private boolean authenticated = false; + public UsernamePasswordAuthenticationToken(String principal, String credentials) { + this.principal = principal; + this.credentials = credentials; + } + public UsernamePasswordAuthenticationToken(String principal, String credentials, UserDetails userDetails) { this.principal = principal; this.credentials = credentials; diff --git a/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java b/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java index d2e9a571..c91976b6 100644 --- a/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java +++ b/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java @@ -8,9 +8,10 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; -import nextstep.security.AuthenticationException; -import nextstep.security.UserDetailService; import nextstep.security.UserDetails; +import nextstep.security.config.Authentication; +import nextstep.security.config.AuthenticationManager; +import nextstep.security.config.UsernamePasswordAuthenticationToken; import java.io.IOException; import java.util.List; @@ -20,10 +21,10 @@ public class UsernamePasswordAuthFilter implements Filter { private static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; private static final List targetURIList = List.of("/login"); - private final UserDetailService userDetailService; + private final AuthenticationManager authenticationManager; - public UsernamePasswordAuthFilter(UserDetailService userDetailService) { - this.userDetailService = userDetailService; + public UsernamePasswordAuthFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; } @Override @@ -56,11 +57,14 @@ private void processLogin(HttpServletRequest request, HttpServletResponse respon try { String username = request.getParameter("username"); String password = request.getParameter("password"); + Authentication authentication = new UsernamePasswordAuthenticationToken(username, password); - UserDetails userDetail = userDetailService.getUserByUsername(username); - if (!userDetail.getPassword().equals(password)) { - throw new AuthenticationException(); + Authentication authenticationResult = this.authenticationManager.authenticate(authentication); + if (authenticationResult == null) { + throw new ServletException("AuthenticationManager should not return null"); } + + UserDetails userDetail = (UserDetails)authenticationResult.getDetails(); addMemberToSession(request, userDetail); } catch (Exception e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); From 62cf60af7e0d8f0ce8618bda35689018672cb914 Mon Sep 17 00:00:00 2001 From: juno-junho Date: Mon, 10 Feb 2025 23:48:00 +0900 Subject: [PATCH 17/24] =?UTF-8?q?feat(Authentication):=20SecurityContext?= =?UTF-8?q?=20=EB=B0=8F=20SecurityContextHolder=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/config/SecurityContext.java | 9 ++++++ .../config/SecurityContextHolder.java | 28 +++++++++++++++++++ .../security/config/SecurityContextImpl.java | 17 +++++++++++ 3 files changed, 54 insertions(+) create mode 100644 src/main/java/nextstep/security/config/SecurityContext.java create mode 100644 src/main/java/nextstep/security/config/SecurityContextHolder.java create mode 100644 src/main/java/nextstep/security/config/SecurityContextImpl.java diff --git a/src/main/java/nextstep/security/config/SecurityContext.java b/src/main/java/nextstep/security/config/SecurityContext.java new file mode 100644 index 00000000..6af69ae7 --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityContext.java @@ -0,0 +1,9 @@ +package nextstep.security.config; + +public interface SecurityContext { + + Authentication getAuthentication(); + + void setAuthentication(Authentication authentication); + +} diff --git a/src/main/java/nextstep/security/config/SecurityContextHolder.java b/src/main/java/nextstep/security/config/SecurityContextHolder.java new file mode 100644 index 00000000..34fff8b7 --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityContextHolder.java @@ -0,0 +1,28 @@ +package nextstep.security.config; + +public class SecurityContextHolder { + + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + + // 현재 스레드의 SecurityContext 반환 + public static SecurityContext getContext() { + SecurityContext securityContext = contextHolder.get(); + if (securityContext == null) { + securityContext = new SecurityContextImpl(); + contextHolder.set(securityContext); + } + return securityContext; + } + + // 현재 스레드의 SecurityContext 설정 + public static void setContext(SecurityContext securityContext) { + contextHolder.set(securityContext); + } + + //현재 스레드의 SecurityContext 초기화. + public static void clearContext() { + contextHolder.remove(); + } + + +} diff --git a/src/main/java/nextstep/security/config/SecurityContextImpl.java b/src/main/java/nextstep/security/config/SecurityContextImpl.java new file mode 100644 index 00000000..e43dc181 --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityContextImpl.java @@ -0,0 +1,17 @@ +package nextstep.security.config; + +public class SecurityContextImpl implements SecurityContext{ + + private Authentication authentication; + + @Override + public Authentication getAuthentication() { + return this.authentication; + } + + @Override + public void setAuthentication(Authentication authentication) { + this.authentication = authentication; + } + +} From 0e11e9906d1646cc37db531458c7d68325f4152e Mon Sep 17 00:00:00 2001 From: juno-junho Date: Tue, 11 Feb 2025 00:29:40 +0900 Subject: [PATCH 18/24] =?UTF-8?q?feat(Authentication):=20BasicAuthenticati?= =?UTF-8?q?onFilter=EC=97=90=EC=84=9C=20SecurityContextHolder=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/config/WebConfig.java | 13 ++++- .../config/BasicAuthenticationProvider.java | 42 ++++++++++++++ .../config/BasicAuthenticationToken.java | 56 +++++++++++++++++++ .../security/config/ProviderManager.java | 1 + .../security/filter/BasicAuthFilter.java | 50 ++++++++--------- src/test/java/nextstep/app/BasicAuthTest.java | 12 ++++ 6 files changed, 146 insertions(+), 28 deletions(-) create mode 100644 src/main/java/nextstep/security/config/BasicAuthenticationProvider.java create mode 100644 src/main/java/nextstep/security/config/BasicAuthenticationToken.java diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index 975120cf..7ec0ae15 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -7,6 +7,7 @@ import nextstep.security.UserDetails; import nextstep.security.config.AuthenticationManager; import nextstep.security.config.AuthenticationProvider; +import nextstep.security.config.BasicAuthenticationProvider; import nextstep.security.config.DaoAuthenticationProvider; import nextstep.security.config.DefaultSecurityFilterChain; import nextstep.security.config.FilterChainProxy; @@ -44,7 +45,7 @@ public FilterChainProxy filterChainProxy() { @Bean public SecurityFilterChain securityFilterChain() { List securityFilters = List.of( - new BasicAuthFilter(userDetailService()), + new BasicAuthFilter(authenticationManager()), new UsernamePasswordAuthFilter(authenticationManager()) ); return new DefaultSecurityFilterChain(securityFilters); @@ -52,10 +53,18 @@ public SecurityFilterChain securityFilterChain() { @Bean public AuthenticationManager authenticationManager() { - List providers = List.of(daoAuthenticationProvider()); + List providers = List.of( + daoAuthenticationProvider(), + basicAuthenticationProvider() + ); return new ProviderManager(providers); } + @Bean + public BasicAuthenticationProvider basicAuthenticationProvider() { + return new BasicAuthenticationProvider(userDetailService()); + } + @Bean public DaoAuthenticationProvider daoAuthenticationProvider() { return new DaoAuthenticationProvider(userDetailService()); diff --git a/src/main/java/nextstep/security/config/BasicAuthenticationProvider.java b/src/main/java/nextstep/security/config/BasicAuthenticationProvider.java new file mode 100644 index 00000000..af3a1ae5 --- /dev/null +++ b/src/main/java/nextstep/security/config/BasicAuthenticationProvider.java @@ -0,0 +1,42 @@ +package nextstep.security.config; + +import nextstep.security.AuthenticationException; +import nextstep.security.UserDetailService; +import nextstep.security.UserDetails; + +public class BasicAuthenticationProvider implements AuthenticationProvider { + + private final UserDetailService userDetailService; + private final PasswordEncoder passwordEncoder; + + public BasicAuthenticationProvider(UserDetailService userDetailService, PasswordEncoder passwordEncoder) { + this.userDetailService = userDetailService; + this.passwordEncoder = passwordEncoder; + } + + public BasicAuthenticationProvider(UserDetailService userDetailService) { + this.userDetailService = userDetailService; + this.passwordEncoder = null; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (!supports(authentication.getClass())) { + return null; + } + String username = (String) authentication.getPrincipal(); + String password = (String) authentication.getCredentials(); + + UserDetails userDetail = userDetailService.getUserByUsername(username); + if (!userDetail.getPassword().equals(password)) { + throw new AuthenticationException(); + } + return new BasicAuthenticationToken(username, password); + } + + @Override + public boolean supports(Class authentication) { + return BasicAuthenticationToken.class.isAssignableFrom(authentication); + } + +} diff --git a/src/main/java/nextstep/security/config/BasicAuthenticationToken.java b/src/main/java/nextstep/security/config/BasicAuthenticationToken.java new file mode 100644 index 00000000..ccedf0e4 --- /dev/null +++ b/src/main/java/nextstep/security/config/BasicAuthenticationToken.java @@ -0,0 +1,56 @@ +package nextstep.security.config; + +import jakarta.servlet.http.HttpServletRequest; +import nextstep.security.AuthenticationException; +import nextstep.security.util.Base64Convertor; + +public class BasicAuthenticationToken extends AbstractAuthenticationToken { + + private final String principal; + private final String credentials; + private boolean authenticated = false; + + + public BasicAuthenticationToken(String authorizationHeader) { + String authType = authorizationHeader.split(" ")[0]; + String credentials = authorizationHeader.split(" ")[1]; + String decodedString = Base64Convertor.decode(credentials); + + checkAuthType(authType); + String[] usernameAndPassword = decodedString.split(":"); + this.principal = usernameAndPassword[0]; + this.credentials = usernameAndPassword[1]; + } + + public BasicAuthenticationToken(String principal, String credentials) { + this.principal = principal; + this.credentials = credentials; + } + + private void checkAuthType(String authType) { + if (!HttpServletRequest.BASIC_AUTH.equalsIgnoreCase(authType)) { + throw new AuthenticationException(); + } + } + + @Override + public Object getCredentials() { + return this.credentials; + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + @Override + public boolean isAuthenticated() { + return this.authenticated; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + this.authenticated = isAuthenticated; + } + +} diff --git a/src/main/java/nextstep/security/config/ProviderManager.java b/src/main/java/nextstep/security/config/ProviderManager.java index e38c47e9..f8129306 100644 --- a/src/main/java/nextstep/security/config/ProviderManager.java +++ b/src/main/java/nextstep/security/config/ProviderManager.java @@ -24,6 +24,7 @@ public Authentication authenticate(Authentication authentication) { result = provider.authenticate(authentication); if (result != null) { // 인증된 객제가 있다면 copyDetails(authentication, result); + result.setAuthenticated(true); // todo 책임 확인 한번 필요 break; } } diff --git a/src/main/java/nextstep/security/filter/BasicAuthFilter.java b/src/main/java/nextstep/security/filter/BasicAuthFilter.java index 78a27d86..da614a56 100644 --- a/src/main/java/nextstep/security/filter/BasicAuthFilter.java +++ b/src/main/java/nextstep/security/filter/BasicAuthFilter.java @@ -9,8 +9,10 @@ import jakarta.servlet.http.HttpServletResponse; import nextstep.security.AuthenticationException; import nextstep.security.UserDetailService; -import nextstep.security.UserDetails; -import nextstep.security.util.Base64Convertor; +import nextstep.security.config.Authentication; +import nextstep.security.config.AuthenticationManager; +import nextstep.security.config.BasicAuthenticationToken; +import nextstep.security.config.SecurityContextHolder; import java.util.List; @@ -20,10 +22,10 @@ public class BasicAuthFilter implements Filter { private static final List TARGET_URL = List.of("/members"); - private final UserDetailService userDetailsService; + private final AuthenticationManager authenticationManager; - public BasicAuthFilter(UserDetailService userDetailsService) { - this.userDetailsService = userDetailsService; + public BasicAuthFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; } @Override @@ -39,7 +41,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo filterChain.doFilter(request, response); return; } - + // 타겟일 경우 checkAuthentication(request); filterChain.doFilter(request, response); } catch (Exception e) { @@ -59,29 +61,25 @@ private boolean isNotTarget(HttpServletRequest request) { } private void checkAuthentication(HttpServletRequest request) { - String authorizationHeader = request.getHeader(AUTHORIZATION); - if (authorizationHeader == null) { - return; - } - String authType = authorizationHeader.split(" ")[0]; - String credentials = authorizationHeader.split(" ")[1]; - String decodedString = Base64Convertor.decode(credentials); - checkAuthType(authType); - - String[] usernameAndPassword = decodedString.split(":"); - String username = usernameAndPassword[0]; - String password = usernameAndPassword[1]; - - UserDetails userDetail = userDetailsService.getUserByUsername(username); - if (!userDetail.getPassword().equals(password)) { - throw new AuthenticationException(); + try { + String authorizationHeader = getAuthorizationHeader(request); + if (authorizationHeader == null) throw new AuthenticationException("Missing authorization header"); + + Authentication authentication = authenticationManager.authenticate(new BasicAuthenticationToken(authorizationHeader)); + if (!authentication.isAuthenticated()) { + throw new AuthenticationException(); + } + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (AuthenticationException e) { + SecurityContextHolder.clearContext(); + throw e; } + } - private void checkAuthType(String authType) { - if (!HttpServletRequest.BASIC_AUTH.equalsIgnoreCase(authType)) { - throw new AuthenticationException(); - } + private static String getAuthorizationHeader(HttpServletRequest request) { + return request.getHeader(AUTHORIZATION); } + } diff --git a/src/test/java/nextstep/app/BasicAuthTest.java b/src/test/java/nextstep/app/BasicAuthTest.java index b5340f18..3a9cb363 100644 --- a/src/test/java/nextstep/app/BasicAuthTest.java +++ b/src/test/java/nextstep/app/BasicAuthTest.java @@ -63,4 +63,16 @@ void members_fail() throws Exception { loginResponse.andDo(print()); loginResponse.andExpect(status().isUnauthorized()); } + + @Test + @DisplayName("Authentication 헤더가 없으면 예외가 발생한다.") + void throwException_when_authentication_header_doesnt_exist() throws Exception { + + ResultActions loginResponse = mockMvc.perform(get("/members") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)); + + loginResponse.andDo(print()); + loginResponse.andExpect(status().isUnauthorized()); + } + } From 29f2ff2ddf5009cadd58f604d2a36c567c20759c Mon Sep 17 00:00:00 2001 From: juno-junho Date: Tue, 11 Feb 2025 00:36:18 +0900 Subject: [PATCH 19/24] =?UTF-8?q?feat(Authentication):=20=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4=20=EC=84=B8=EC=85=98=20=EB=B0=A9=EC=8B=9D=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=8A=A4=EB=A0=88=EB=93=9C=20=EB=A1=9C=EC=BB=AC=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EA=B4=80=EB=A6=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/SecurityContextHolder.java | 3 +++ .../filter/UsernamePasswordAuthFilter.java | 20 +++++++++---------- src/test/java/nextstep/app/FormLoginTest.java | 13 +++++++++--- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/main/java/nextstep/security/config/SecurityContextHolder.java b/src/main/java/nextstep/security/config/SecurityContextHolder.java index 34fff8b7..8896635b 100644 --- a/src/main/java/nextstep/security/config/SecurityContextHolder.java +++ b/src/main/java/nextstep/security/config/SecurityContextHolder.java @@ -24,5 +24,8 @@ public static void clearContext() { contextHolder.remove(); } + public static SecurityContext createEmptyContext() { + return new SecurityContextImpl(); + } } diff --git a/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java b/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java index c91976b6..48d1f1c7 100644 --- a/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java +++ b/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java @@ -7,18 +7,17 @@ import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpSession; -import nextstep.security.UserDetails; import nextstep.security.config.Authentication; import nextstep.security.config.AuthenticationManager; +import nextstep.security.config.SecurityContext; +import nextstep.security.config.SecurityContextHolder; import nextstep.security.config.UsernamePasswordAuthenticationToken; import java.io.IOException; import java.util.List; public class UsernamePasswordAuthFilter implements Filter { - - private static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + private static final List targetURIList = List.of("/login"); private final AuthenticationManager authenticationManager; @@ -60,20 +59,19 @@ private void processLogin(HttpServletRequest request, HttpServletResponse respon Authentication authentication = new UsernamePasswordAuthenticationToken(username, password); Authentication authenticationResult = this.authenticationManager.authenticate(authentication); - if (authenticationResult == null) { + if (authenticationResult == null || !authenticationResult.isAuthenticated()) { throw new ServletException("AuthenticationManager should not return null"); } - - UserDetails userDetail = (UserDetails)authenticationResult.getDetails(); - addMemberToSession(request, userDetail); + addMemberToSession(authenticationResult); } catch (Exception e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); } } - private void addMemberToSession(HttpServletRequest request, UserDetails userDetail) { - HttpSession session = request.getSession(); - session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetail); + private void addMemberToSession(Authentication authentication) { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(authentication); + SecurityContextHolder.setContext(securityContext); } } diff --git a/src/test/java/nextstep/app/FormLoginTest.java b/src/test/java/nextstep/app/FormLoginTest.java index 9c156371..8f14d0f5 100644 --- a/src/test/java/nextstep/app/FormLoginTest.java +++ b/src/test/java/nextstep/app/FormLoginTest.java @@ -3,6 +3,9 @@ import jakarta.servlet.http.HttpSession; import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; +import nextstep.security.config.Authentication; +import nextstep.security.config.SecurityContext; +import nextstep.security.config.SecurityContextHolder; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -21,6 +24,7 @@ @SpringBootTest @AutoConfigureMockMvc class FormLoginTest { + private final Member TEST_MEMBER = new Member("a@a.com", "password", "a", ""); @Autowired @@ -46,9 +50,11 @@ void login_success() throws Exception { loginResponse.andDo(print()); loginResponse.andExpect(status().isOk()); - HttpSession session = loginResponse.andReturn().getRequest().getSession(); - assertThat(session).isNotNull(); - assertThat(session.getAttribute("SPRING_SECURITY_CONTEXT")).isNotNull(); + SecurityContext securityContext = SecurityContextHolder.getContext(); + Authentication authentication = securityContext.getAuthentication(); + assertThat(authentication).isNotNull(); + assertThat(authentication.getPrincipal()).isEqualTo(TEST_MEMBER.getEmail()); + assertThat(authentication.getCredentials()).isEqualTo(TEST_MEMBER.getPassword()); } @DisplayName("로그인 실패 - 사용자 없음") @@ -76,4 +82,5 @@ void login_fail_with_invalid_password() throws Exception { response.andDo(print()); response.andExpect(status().isUnauthorized()); } + } From 872ef46e6b0f9891ac139124534954bd09972a98 Mon Sep 17 00:00:00 2001 From: juno-junho Date: Tue, 11 Feb 2025 00:36:49 +0900 Subject: [PATCH 20/24] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20import=EB=AC=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/security/filter/BasicAuthFilter.java | 1 - src/test/java/nextstep/app/FormLoginTest.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/nextstep/security/filter/BasicAuthFilter.java b/src/main/java/nextstep/security/filter/BasicAuthFilter.java index da614a56..45ae41d2 100644 --- a/src/main/java/nextstep/security/filter/BasicAuthFilter.java +++ b/src/main/java/nextstep/security/filter/BasicAuthFilter.java @@ -8,7 +8,6 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import nextstep.security.AuthenticationException; -import nextstep.security.UserDetailService; import nextstep.security.config.Authentication; import nextstep.security.config.AuthenticationManager; import nextstep.security.config.BasicAuthenticationToken; diff --git a/src/test/java/nextstep/app/FormLoginTest.java b/src/test/java/nextstep/app/FormLoginTest.java index 8f14d0f5..3a90c455 100644 --- a/src/test/java/nextstep/app/FormLoginTest.java +++ b/src/test/java/nextstep/app/FormLoginTest.java @@ -1,6 +1,5 @@ package nextstep.app; -import jakarta.servlet.http.HttpSession; import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import nextstep.security.config.Authentication; From 86ba902e8fbc74ebcf555c3ee11ae15145eb34c2 Mon Sep 17 00:00:00 2001 From: juno-junho Date: Tue, 11 Feb 2025 23:47:33 +0900 Subject: [PATCH 21/24] =?UTF-8?q?feat(authenticaion):=20SecurityContextRep?= =?UTF-8?q?ository=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4?= =?UTF-8?q?=EB=A5=BC=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20HttpSessionS?= =?UTF-8?q?ecurityContextRepository=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HttpSessionSecurityContextRepository.java | 56 +++++++++++++++++++ .../security/config/SecurityContext.java | 4 ++ .../config/SecurityContextRepository.java | 30 ++++++++++ 3 files changed, 90 insertions(+) create mode 100644 src/main/java/nextstep/security/config/HttpSessionSecurityContextRepository.java create mode 100644 src/main/java/nextstep/security/config/SecurityContextRepository.java diff --git a/src/main/java/nextstep/security/config/HttpSessionSecurityContextRepository.java b/src/main/java/nextstep/security/config/HttpSessionSecurityContextRepository.java new file mode 100644 index 00000000..f84e3822 --- /dev/null +++ b/src/main/java/nextstep/security/config/HttpSessionSecurityContextRepository.java @@ -0,0 +1,56 @@ +package nextstep.security.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +public class HttpSessionSecurityContextRepository implements SecurityContextRepository { + + private static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + /** + * 현 request의 security context를 가지고와서 반환한다. + * session이 null이면 context 객체는 null을 반환하거나 session에 저장되어 있는 context 객체는 SecurityContext의 인스턴스가 아니다. + * 새로운 context 객체가 생성되고 리턴된다. + * + * @return + */ + /** + * HttpServletReqeust의 getSession(boolean create): + * false: current seesion이 없으면 null 반환 / true: current session 없으면 새로 만든다. + *

+ * 세션이 잘 유지되기 위해서는 getSession을 response가 커밋되기 전에 호출해야한다. + * cookie를 사용해 세션의 정합성을 보장할때, 새로운 세션을 response가 커밋되고 나서 만든다면 IllegalStateException 발생한다. + * getSession() == getSession(true) + */ + @Override + public SecurityContext loadContext(HttpServletRequest request) { + HttpSession session = request.getSession(false); // false: current seesion이 없으면 null 반환 / true: current session 없으면 새로 만든다. + SecurityContext securityContext = readSecurityContextFromSession(session); + if (securityContext == null) { // 세션에 올바른 security context가 없다면 새로 만든다. + securityContext = SecurityContextHolder.createEmptyContext(); + } + return securityContext; + } + + private SecurityContext readSecurityContextFromSession(HttpSession httpSession) { + if (httpSession == null) { + return null; + } + Object contextFromSession = httpSession.getAttribute(SPRING_SECURITY_CONTEXT_KEY); + if (contextFromSession == null) { + return null; + } + if (!(contextFromSession instanceof SecurityContext)) { + return null; + } + return (SecurityContext) contextFromSession; + } + + @Override + public 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/config/SecurityContext.java b/src/main/java/nextstep/security/config/SecurityContext.java index 6af69ae7..52bf24b5 100644 --- a/src/main/java/nextstep/security/config/SecurityContext.java +++ b/src/main/java/nextstep/security/config/SecurityContext.java @@ -2,6 +2,10 @@ public interface SecurityContext { + /** + * 최근에 인증된 principal을 얻거나 authentication request token을 얻는다. + * @return Authentication이나 null을 반환. 인증 정보가 없다면 + */ Authentication getAuthentication(); void setAuthentication(Authentication authentication); diff --git a/src/main/java/nextstep/security/config/SecurityContextRepository.java b/src/main/java/nextstep/security/config/SecurityContextRepository.java new file mode 100644 index 00000000..b7753ce2 --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityContextRepository.java @@ -0,0 +1,30 @@ +package nextstep.security.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 요청들 사이에 SecurityContext를 영속시키는데 사용되는 전략. + * + * SecurityContextPersistenceFilter에 의해 사용된다. + * 현재 실행되는 스레드에 사용되는 context를 획득하고 + * 요청이 끝나고 thread-local 스토리지에서 일단 저장되면 저장하는데 사용된다. + * + * persistence 매커니즘이 구현에 따라 달라지지만, HttpSession을 사용해 context를 저장하는데 사용한다. + */ +public interface SecurityContextRepository { + + /** + * 공급된 요청에서 security context를 획득한다. + * 인증 안된 사용자에게는 빈 컨텍스트 impl이 반환된다. + * null을 반환해서는 안된다. + * + */ + SecurityContext loadContext(HttpServletRequest request); + + /** + * stores the security context on completion of a request. + * request에 맞는 context가 찾아지면 true 아니면 false를 반환함 + */ + void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response); +} From 55ad4923991b14ff5fe34f90a17fb868b3e05e99 Mon Sep 17 00:00:00 2001 From: juno-junho Date: Wed, 12 Feb 2025 00:35:14 +0900 Subject: [PATCH 22/24] =?UTF-8?q?feat(authenticaion):=20SecurityContextHol?= =?UTF-8?q?derFilter=20=EC=9E=91=EC=84=B1=20=EB=B0=8F=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=20=EC=B2=B4=EC=9D=B8=EC=97=90=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/config/WebConfig.java | 2 + .../config/SecurityContextHolderFilter.java | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 src/main/java/nextstep/security/config/SecurityContextHolderFilter.java diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index 7ec0ae15..f0180138 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -12,6 +12,7 @@ import nextstep.security.config.DefaultSecurityFilterChain; import nextstep.security.config.FilterChainProxy; import nextstep.security.config.ProviderManager; +import nextstep.security.config.SecurityContextHolderFilter; import nextstep.security.config.SecurityFilterChain; import nextstep.security.filter.BasicAuthFilter; import nextstep.security.filter.UsernamePasswordAuthFilter; @@ -45,6 +46,7 @@ public FilterChainProxy filterChainProxy() { @Bean public SecurityFilterChain securityFilterChain() { List securityFilters = List.of( + new SecurityContextHolderFilter(), new BasicAuthFilter(authenticationManager()), new UsernamePasswordAuthFilter(authenticationManager()) ); diff --git a/src/main/java/nextstep/security/config/SecurityContextHolderFilter.java b/src/main/java/nextstep/security/config/SecurityContextHolderFilter.java new file mode 100644 index 00000000..6731de91 --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityContextHolderFilter.java @@ -0,0 +1,46 @@ +package nextstep.security.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; + +/** + * SecurityContextHolderFilter는 요청 시작 시 SecurityContextRepository로부터 인증 정보를 SecurityContextHolder로 이동시킴 + * 요청 종료 후 SecurityContextHolder를 정리. + * 요청 시작 시: + * SecurityContextRepository.loadContext() 호출. + * 로드한 인증 정보를 SecurityContextHolder에 설정. + * 요청 종료 시: + * SecurityContextHolder.getContext()를 가져와 SecurityContextRepository.saveContext() 호출. + * SecurityContextHolder.clearContext()로 정리. + */ +public class SecurityContextHolderFilter extends GenericFilterBean { + + private final SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository(); + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { + boolean isHttpServlet = servletRequest instanceof HttpServletRequest && servletResponse instanceof HttpServletResponse; + + if (isHttpServlet) { + HttpServletRequest request = (HttpServletRequest) servletRequest; + SecurityContext securityContext = securityContextRepository.loadContext(request); + try { + SecurityContextHolder.setContext(securityContext); + chain.doFilter(servletRequest, servletResponse); + } finally { + SecurityContextHolder.clearContext(); + } + return; + } + throw new ServletException("SecurityContextHolderFilter only supports HTTP requests"); + + } + +} From 0ced606a473ef98260e9f86c5ccf505fea1411e6 Mon Sep 17 00:00:00 2001 From: juno-junho Date: Wed, 12 Feb 2025 00:36:13 +0900 Subject: [PATCH 23/24] =?UTF-8?q?feat(authenticaion):=20HttpSessionSecurit?= =?UTF-8?q?yContextRepository=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/filter/BasicAuthFilter.java | 25 +++++++------- .../filter/UsernamePasswordAuthFilter.java | 9 +++-- src/test/java/nextstep/app/FormLoginTest.java | 33 ++++++++++++++----- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/main/java/nextstep/security/filter/BasicAuthFilter.java b/src/main/java/nextstep/security/filter/BasicAuthFilter.java index 45ae41d2..4ff2c096 100644 --- a/src/main/java/nextstep/security/filter/BasicAuthFilter.java +++ b/src/main/java/nextstep/security/filter/BasicAuthFilter.java @@ -11,6 +11,7 @@ import nextstep.security.config.Authentication; import nextstep.security.config.AuthenticationManager; import nextstep.security.config.BasicAuthenticationToken; +import nextstep.security.config.SecurityContext; import nextstep.security.config.SecurityContextHolder; import java.util.List; @@ -60,23 +61,23 @@ private boolean isNotTarget(HttpServletRequest request) { } private void checkAuthentication(HttpServletRequest request) { - try { - String authorizationHeader = getAuthorizationHeader(request); - if (authorizationHeader == null) throw new AuthenticationException("Missing authorization header"); + String authorizationHeader = getAuthorizationHeader(request); + if (authorizationHeader == null) throw new AuthenticationException("Missing authorization header"); - Authentication authentication = authenticationManager.authenticate(new BasicAuthenticationToken(authorizationHeader)); - if (!authentication.isAuthenticated()) { - throw new AuthenticationException(); - } - SecurityContextHolder.getContext().setAuthentication(authentication); - } catch (AuthenticationException e) { - SecurityContextHolder.clearContext(); - throw e; + Authentication authentication = authenticationManager.authenticate(new BasicAuthenticationToken(authorizationHeader)); + if (!authentication.isAuthenticated()) { + throw new AuthenticationException(); } + saveToSecurityContext(authentication); + } + private void saveToSecurityContext(Authentication authentication) { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(authentication); + SecurityContextHolder.setContext(securityContext); } - private static String getAuthorizationHeader(HttpServletRequest request) { + private String getAuthorizationHeader(HttpServletRequest request) { return request.getHeader(AUTHORIZATION); } diff --git a/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java b/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java index 48d1f1c7..949e1aff 100644 --- a/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java +++ b/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java @@ -9,8 +9,10 @@ import jakarta.servlet.http.HttpServletResponse; import nextstep.security.config.Authentication; import nextstep.security.config.AuthenticationManager; +import nextstep.security.config.HttpSessionSecurityContextRepository; import nextstep.security.config.SecurityContext; import nextstep.security.config.SecurityContextHolder; +import nextstep.security.config.SecurityContextRepository; import nextstep.security.config.UsernamePasswordAuthenticationToken; import java.io.IOException; @@ -21,9 +23,11 @@ public class UsernamePasswordAuthFilter implements Filter { private static final List targetURIList = List.of("/login"); private final AuthenticationManager authenticationManager; + private final SecurityContextRepository securityContextRepository; public UsernamePasswordAuthFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; + this.securityContextRepository = new HttpSessionSecurityContextRepository(); } @Override @@ -62,16 +66,17 @@ private void processLogin(HttpServletRequest request, HttpServletResponse respon if (authenticationResult == null || !authenticationResult.isAuthenticated()) { throw new ServletException("AuthenticationManager should not return null"); } - addMemberToSession(authenticationResult); + addMemberToSession(authenticationResult, request, response); } catch (Exception e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); } } - private void addMemberToSession(Authentication authentication) { + private void addMemberToSession(Authentication authentication, HttpServletRequest request, HttpServletResponse response) { SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); securityContext.setAuthentication(authentication); SecurityContextHolder.setContext(securityContext); + securityContextRepository.saveContext(securityContext, request, response); } } diff --git a/src/test/java/nextstep/app/FormLoginTest.java b/src/test/java/nextstep/app/FormLoginTest.java index 3a90c455..70183a6e 100644 --- a/src/test/java/nextstep/app/FormLoginTest.java +++ b/src/test/java/nextstep/app/FormLoginTest.java @@ -1,10 +1,8 @@ package nextstep.app; +import jakarta.servlet.http.HttpSession; import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; -import nextstep.security.config.Authentication; -import nextstep.security.config.SecurityContext; -import nextstep.security.config.SecurityContextHolder; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -49,11 +47,9 @@ void login_success() throws Exception { loginResponse.andDo(print()); loginResponse.andExpect(status().isOk()); - SecurityContext securityContext = SecurityContextHolder.getContext(); - Authentication authentication = securityContext.getAuthentication(); - assertThat(authentication).isNotNull(); - assertThat(authentication.getPrincipal()).isEqualTo(TEST_MEMBER.getEmail()); - assertThat(authentication.getCredentials()).isEqualTo(TEST_MEMBER.getPassword()); + HttpSession session = loginResponse.andReturn().getRequest().getSession(); + assertThat(session).isNotNull(); + assertThat(session.getAttribute("SPRING_SECURITY_CONTEXT")).isNotNull(); } @DisplayName("로그인 실패 - 사용자 없음") @@ -82,4 +78,25 @@ void login_fail_with_invalid_password() throws Exception { response.andExpect(status().isUnauthorized()); } +// @DisplayName("로그인 후 세션을 통해 회원 목록 조회") +// @Test +// void login_after_members() throws Exception { +// MockHttpSession session = new MockHttpSession(); +// ResultActions loginResponse = mockMvc.perform(post("/login") +// .param("username", TEST_MEMBER.getEmail()) +// .param("password", TEST_MEMBER.getPassword()) +// .session(session) +// .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) +// ).andDo(print()); +// +// loginResponse.andExpect(status().isOk()); +// +// ResultActions membersResponse = mockMvc.perform(get("/members") +// .session(session) +// .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) +// ); +// +// membersResponse.andExpect(status().isOk()); +// } + } From 53f13e19ae7a0d2c8cd05c50a1c727dff926007e Mon Sep 17 00:00:00 2001 From: juno-junho Date: Wed, 12 Feb 2025 00:36:31 +0900 Subject: [PATCH 24/24] =?UTF-8?q?feat(authenticaion):=20HttpSessionSecurit?= =?UTF-8?q?yContextRepository=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/security/config/SecurityContextRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/nextstep/security/config/SecurityContextRepository.java b/src/main/java/nextstep/security/config/SecurityContextRepository.java index b7753ce2..ddb5f926 100644 --- a/src/main/java/nextstep/security/config/SecurityContextRepository.java +++ b/src/main/java/nextstep/security/config/SecurityContextRepository.java @@ -5,11 +5,11 @@ /** * 요청들 사이에 SecurityContext를 영속시키는데 사용되는 전략. - * + * SecurityContextPersistenceFilter에 의해 사용된다. * 현재 실행되는 스레드에 사용되는 context를 획득하고 * 요청이 끝나고 thread-local 스토리지에서 일단 저장되면 저장하는데 사용된다. - * + * persistence 매커니즘이 구현에 따라 달라지지만, HttpSession을 사용해 context를 저장하는데 사용한다. */ public interface SecurityContextRepository {