From eed21f68ed70fd449566bbdec11a399210b94649 Mon Sep 17 00:00:00 2001 From: Jealyang Date: Fri, 1 Nov 2024 18:51:00 +0900 Subject: [PATCH 01/17] =?UTF-8?q?=EB=A1=AC=EB=B3=B5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.gradle b/build.gradle index 99766160..d8069ccb 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,14 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' +// implementation 'org.springframework.boot:spring-boot-starter-security' +// implementation 'org.springframework.boot:spring-boot-starter-data-jpa' +// runtimeOnly 'com.h2database:h2' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' +// testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { From 477637d2267380b6d18f013c77cb953fd76b3896 Mon Sep 17 00:00:00 2001 From: Jealyang Date: Fri, 1 Nov 2024 18:51:12 +0900 Subject: [PATCH 02/17] =?UTF-8?q?README=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/README.md b/README.md index 1e7ba652..dcb0f9df 100644 --- a/README.md +++ b/README.md @@ -1 +1,48 @@ # spring-security-authentication + + +아이디와 비밀번호를 기반으로 로그인 기능을 구현하고
+Basic 인증을 사용하여 사용자를 식별할 수 있도록 스프링 프레임워크를 사용하여 웹 앱으로 구현한다. + +- ``Spring security``가 아니라 ``Session``과 ``Interceptor`` 를 기반으로 구현 + +--- +# 구현 요구 사항 + +1. 아이디와 비밀번호 기반 로그인 구현 + + 로그인 요청 시 사용자가 입력한 아이디와 패스워드를 확인하여 인증한다. + + 로그인 성공 시 ``Session`` 을 사용하여 인증 정보를 저장한다. + + +2. Basic 인증 구현 + + 사용자 목록 조회 기능 이용 시 + + ``Member``로 등록되어 있는 사용자만 가능하도록 한다. + + 이를 위해 Basic 인증을 사용하여 사용자를 식별한다. + + 요청의 **Authorization 헤더**에서 Basic 인증 정보를 추출하여 인증을 처리한다. + + 인증 성공 시 ``Session``을 사용하여 인증 정보를 저장한다. + + +3. 인터셉터 분리 + + ``HandlerInterceptor``를 사용하여 인증 관련 로직을 ``Controller``에서 분리한다. + + 앞서 구현한 두 인증 방식(아이디 비밀번호 로그인 방식과 Basic 인증 방식) 모두 인터셉터에서 처리되도록 구현한다. + + 가급적이면 하나의 인터셉터는 하나의 작업만 수행하도록 설계한다. + + +4. 인증 로직과 서비스 로직 간의 패키지 분리 + + **서비스 코드와 인증 코드를 명확히 분리**하여 관리하도록 한다. + + 서비스 관련 코드는 ``app`` 패키지에 위치시키고, 인증 관련 코드는 ``security`` 패키지에 위치시킨다. + + 리팩터링 과정에서 패키지 간의 양방향 참조가 발생한다면 단방향 참조로 리팩터링한다. + + ``app`` 패키지는 ``security`` 패키지에 의존할 수 있지만, ``security`` 패키지는 ``app`` 패키지에 의존하지 않도록 한다. + + 인증 관련 작업은 ``security`` 패키지에서 전담하도록 설계하여, 서비스 로직이 인증 세부 사항에 의존하지 않게 만든다. + + +
+ +--- +# API 정의 +### 로그인 + - /login [POST] 아이디와 비밀번호를 확인하여 인증. (인증 후 Session에 인증 정보 저장) + +### 사용자 조회 + - /member [GET] 사용자 목록 조회. (단, Member만 인가) + From ccacf2f87f9947aa86afaf7fad4c0b97979e44e4 Mon Sep 17 00:00:00 2001 From: Jealyang Date: Fri, 1 Nov 2024 18:57:50 +0900 Subject: [PATCH 03/17] =?UTF-8?q?=EC=9D=B8=ED=84=B0=EC=85=89=ED=84=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/config/WebConfig.java | 16 ++++++++++++++++ .../security/BasicAuthenticationInterceptor.java | 13 +++++++++++++ .../java/nextstep/security/LoginInterceptor.java | 13 +++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 src/main/java/nextstep/config/WebConfig.java create mode 100644 src/main/java/nextstep/security/BasicAuthenticationInterceptor.java create mode 100644 src/main/java/nextstep/security/LoginInterceptor.java diff --git a/src/main/java/nextstep/config/WebConfig.java b/src/main/java/nextstep/config/WebConfig.java new file mode 100644 index 00000000..036ad992 --- /dev/null +++ b/src/main/java/nextstep/config/WebConfig.java @@ -0,0 +1,16 @@ +package nextstep.config; + +import nextstep.security.BasicAuthenticationInterceptor; +import nextstep.security.LoginInterceptor; +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 { + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new LoginInterceptor()); + registry.addInterceptor(new BasicAuthenticationInterceptor()).addPathPatterns("/members"); + } +} diff --git a/src/main/java/nextstep/security/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/BasicAuthenticationInterceptor.java new file mode 100644 index 00000000..b4b54e56 --- /dev/null +++ b/src/main/java/nextstep/security/BasicAuthenticationInterceptor.java @@ -0,0 +1,13 @@ +package nextstep.security; + +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class BasicAuthenticationInterceptor implements HandlerInterceptor { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + return HandlerInterceptor.super.preHandle(request, response, handler); + } +} diff --git a/src/main/java/nextstep/security/LoginInterceptor.java b/src/main/java/nextstep/security/LoginInterceptor.java new file mode 100644 index 00000000..4e1f607a --- /dev/null +++ b/src/main/java/nextstep/security/LoginInterceptor.java @@ -0,0 +1,13 @@ +package nextstep.security; + +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class LoginInterceptor implements HandlerInterceptor { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + return HandlerInterceptor.super.preHandle(request, response, handler); + } +} From 763f9508e5eb480ec2a385a295fb401f9f38a0e1 Mon Sep 17 00:00:00 2001 From: Jealyang Date: Fri, 1 Nov 2024 22:13:19 +0900 Subject: [PATCH 04/17] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=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 --- build.gradle | 3 +++ .../java/nextstep/app/ui/LoginController.java | 18 ++++++++++++++---- .../nextstep/security/LoginInterceptor.java | 2 +- src/test/java/nextstep/app/LoginTest.java | 3 ++- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index d8069ccb..b1e5d784 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,9 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' // testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + //테스트에서 lombok 사용 + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' } tasks.named('test') { diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 0ea94f1b..4638490a 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,5 +1,7 @@ package nextstep.app.ui; +import lombok.RequiredArgsConstructor; +import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -9,19 +11,27 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; +import java.util.Map; @RestController +@RequiredArgsConstructor 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) { + String email = request.getParameter("username"); + String password = request.getParameter("password"); + + Member member = memberRepository.findByEmail(email).orElseThrow(AuthenticationException::new); + if (email.equals(member.getEmail()) && password.equals(member.getPassword())) { + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member); //session name, object + } else { + throw new AuthenticationException(); + } + return ResponseEntity.ok().build(); } diff --git a/src/main/java/nextstep/security/LoginInterceptor.java b/src/main/java/nextstep/security/LoginInterceptor.java index 4e1f607a..86af0e47 100644 --- a/src/main/java/nextstep/security/LoginInterceptor.java +++ b/src/main/java/nextstep/security/LoginInterceptor.java @@ -10,4 +10,4 @@ public class LoginInterceptor implements HandlerInterceptor { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return HandlerInterceptor.super.preHandle(request, response, handler); } -} +} \ No newline at end of file diff --git a/src/test/java/nextstep/app/LoginTest.java b/src/test/java/nextstep/app/LoginTest.java index 717bcc8a..e83007d6 100644 --- a/src/test/java/nextstep/app/LoginTest.java +++ b/src/test/java/nextstep/app/LoginTest.java @@ -1,5 +1,6 @@ package nextstep.app; +import lombok.extern.slf4j.Slf4j; import nextstep.app.domain.Member; import nextstep.app.infrastructure.InmemoryMemberRepository; import org.junit.jupiter.api.DisplayName; @@ -9,7 +10,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import javax.servlet.http.HttpSession; @@ -18,6 +18,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@Slf4j @SpringBootTest @AutoConfigureMockMvc class LoginTest { From 595c01552f5e5599b7d8444efaa5d23271b17eed Mon Sep 17 00:00:00 2001 From: Jealyang Date: Fri, 1 Nov 2024 22:15:44 +0900 Subject: [PATCH 05/17] polishing --- src/main/java/nextstep/app/ui/LoginController.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 4638490a..4e089312 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -25,13 +25,15 @@ public ResponseEntity login(HttpServletRequest request, HttpSession sessio String email = request.getParameter("username"); String password = request.getParameter("password"); + //로그인 실패 시 예외 발생 Member member = memberRepository.findByEmail(email).orElseThrow(AuthenticationException::new); - if (email.equals(member.getEmail()) && password.equals(member.getPassword())) { - session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member); //session name, object - } else { + if (!email.equals(member.getEmail()) || !password.equals(member.getPassword())) { throw new AuthenticationException(); } - + + //로그인 성공 + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member); //session name, object + return ResponseEntity.ok().build(); } From 5398b89bea7816177af29a63226bfd45a5980011 Mon Sep 17 00:00:00 2001 From: Jealyang Date: Sat, 2 Nov 2024 23:30:02 +0900 Subject: [PATCH 06/17] =?UTF-8?q?README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dcb0f9df..9b2d4c5b 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,15 @@ Basic 인증을 사용하여 사용자를 식별할 수 있도록 스프링 프 + **서비스 코드와 인증 코드를 명확히 분리**하여 관리하도록 한다. + 서비스 관련 코드는 ``app`` 패키지에 위치시키고, 인증 관련 코드는 ``security`` 패키지에 위치시킨다. + 리팩터링 과정에서 패키지 간의 양방향 참조가 발생한다면 단방향 참조로 리팩터링한다. - + ``app`` 패키지는 ``security`` 패키지에 의존할 수 있지만, ``security`` 패키지는 ``app`` 패키지에 의존하지 않도록 한다. + + ``app`` 패키지는 ``security`` 패키지에 의존할 수 있지만, **``security`` 패키지는 ``app`` 패키지에 의존하지 않도록** 한다. + 인증 관련 작업은 ``security`` 패키지에서 전담하도록 설계하여, 서비스 로직이 인증 세부 사항에 의존하지 않게 만든다. + + ``` + 패키지 간 의존성을 최소화하고, 변경에 강한 구조를 만드는 목적. + security 패키지를 독립적이고 재사용 가능하게 설계하려면, 직접적인 의존성을 피하기 위해 인터페이스를 구현하게 한다. (DIP) + ``` + +
From 840b844d7356e191fe5a44609e7e80bd179b9b8f Mon Sep 17 00:00:00 2001 From: Jealyang Date: Sat, 2 Nov 2024 23:57:54 +0900 Subject: [PATCH 07/17] =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80,=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/{ => app}/config/WebConfig.java | 9 +++--- .../nextstep/app/service/LoginService.java | 27 ++++++++++++++++ .../java/nextstep/app/ui/LoginController.java | 12 +++---- .../nextstep/app/ui/MemberController.java | 6 ++-- .../nextstep/security/LoginInterceptor.java | 13 -------- .../authentication/AuthenticationManager.java | 8 +++++ .../security/context/SecurityContext.java | 14 +++++++++ .../context/SecurityContextHolder.java | 20 ++++++++++++ .../security/core/Authentication.java | 18 +++++++++++ .../core/userdetails/UserDetails.java | 11 +++++++ .../core/userdetails/UserDetailsService.java | 5 +++ .../BasicAuthenticationInterceptor.java | 5 ++- ...namePasswordAuthenticationInterceptor.java | 31 +++++++++++++++++++ src/test/java/nextstep/app/MemberTest.java | 2 +- 14 files changed, 152 insertions(+), 29 deletions(-) rename src/main/java/nextstep/{ => app}/config/WebConfig.java (63%) create mode 100644 src/main/java/nextstep/app/service/LoginService.java delete mode 100644 src/main/java/nextstep/security/LoginInterceptor.java create mode 100644 src/main/java/nextstep/security/authentication/AuthenticationManager.java create mode 100644 src/main/java/nextstep/security/context/SecurityContext.java create mode 100644 src/main/java/nextstep/security/context/SecurityContextHolder.java create mode 100644 src/main/java/nextstep/security/core/Authentication.java create mode 100644 src/main/java/nextstep/security/core/userdetails/UserDetails.java create mode 100644 src/main/java/nextstep/security/core/userdetails/UserDetailsService.java rename src/main/java/nextstep/security/{ => web/authentication}/BasicAuthenticationInterceptor.java (79%) create mode 100644 src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java diff --git a/src/main/java/nextstep/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java similarity index 63% rename from src/main/java/nextstep/config/WebConfig.java rename to src/main/java/nextstep/app/config/WebConfig.java index 036ad992..b2f22353 100644 --- a/src/main/java/nextstep/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -1,7 +1,7 @@ -package nextstep.config; +package nextstep.app.config; -import nextstep.security.BasicAuthenticationInterceptor; -import nextstep.security.LoginInterceptor; +import nextstep.security.web.authentication.BasicAuthenticationInterceptor; +import nextstep.security.web.authentication.UsernamePasswordAuthenticationInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -10,7 +10,8 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new LoginInterceptor()); + registry.addInterceptor(new UsernamePasswordAuthenticationInterceptor()); registry.addInterceptor(new BasicAuthenticationInterceptor()).addPathPatterns("/members"); } + } diff --git a/src/main/java/nextstep/app/service/LoginService.java b/src/main/java/nextstep/app/service/LoginService.java new file mode 100644 index 00000000..05b94641 --- /dev/null +++ b/src/main/java/nextstep/app/service/LoginService.java @@ -0,0 +1,27 @@ +package nextstep.app.service; + +import lombok.RequiredArgsConstructor; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.app.ui.AuthenticationException; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpSession; + +import static nextstep.app.ui.LoginController.SPRING_SECURITY_CONTEXT_KEY; + +@Service +@RequiredArgsConstructor +public class LoginService { + + private final MemberRepository memberRepository; + + public Member login(String email, String password) { + //로그인 실패 시 예외 발생 + Member member = memberRepository.findByEmail(email).orElseThrow(AuthenticationException::new); + if (!email.equals(member.getEmail()) || !password.equals(member.getPassword())) { + throw new AuthenticationException(); + } + return member; + } +} diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 4e089312..1215e626 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; +import nextstep.app.service.LoginService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -18,7 +19,7 @@ public class LoginController { public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - private final MemberRepository memberRepository; + private final LoginService loginService; @PostMapping("/login") public ResponseEntity login(HttpServletRequest request, HttpSession session) { @@ -26,13 +27,12 @@ public ResponseEntity login(HttpServletRequest request, HttpSession sessio String password = request.getParameter("password"); //로그인 실패 시 예외 발생 - Member member = memberRepository.findByEmail(email).orElseThrow(AuthenticationException::new); - if (!email.equals(member.getEmail()) || !password.equals(member.getPassword())) { - throw new AuthenticationException(); - } + Member loginMember = loginService.login(email, password); //로그인 성공 - session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member); //session name, object + if (loginMember != null) { + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, loginMember); //session name, object + } return ResponseEntity.ok().build(); } diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index c8cc74d6..299b140c 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -1,5 +1,6 @@ package nextstep.app.ui; +import lombok.RequiredArgsConstructor; import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import org.springframework.http.ResponseEntity; @@ -9,14 +10,11 @@ import java.util.List; @RestController +@RequiredArgsConstructor public class MemberController { private final MemberRepository memberRepository; - public MemberController(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } - @GetMapping("/members") public ResponseEntity> list() { List members = memberRepository.findAll(); diff --git a/src/main/java/nextstep/security/LoginInterceptor.java b/src/main/java/nextstep/security/LoginInterceptor.java deleted file mode 100644 index 86af0e47..00000000 --- a/src/main/java/nextstep/security/LoginInterceptor.java +++ /dev/null @@ -1,13 +0,0 @@ -package nextstep.security; - -import org.springframework.web.servlet.HandlerInterceptor; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class LoginInterceptor implements HandlerInterceptor { - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - return HandlerInterceptor.super.preHandle(request, response, handler); - } -} \ No newline at end of file diff --git a/src/main/java/nextstep/security/authentication/AuthenticationManager.java b/src/main/java/nextstep/security/authentication/AuthenticationManager.java new file mode 100644 index 00000000..71a174dc --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationManager.java @@ -0,0 +1,8 @@ +package nextstep.security.authentication; + +import nextstep.security.core.Authentication; + +public interface AuthenticationManager { + Authentication authenticate(Authentication authentication); + //Authentication authenticate(Authentication authentication) throws AuthenticationException; +} diff --git a/src/main/java/nextstep/security/context/SecurityContext.java b/src/main/java/nextstep/security/context/SecurityContext.java new file mode 100644 index 00000000..6aaf143e --- /dev/null +++ b/src/main/java/nextstep/security/context/SecurityContext.java @@ -0,0 +1,14 @@ +package nextstep.security.context; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import nextstep.security.core.Authentication; + +@AllArgsConstructor +@Getter +@Setter +public class SecurityContext { + private Authentication authentication; +} diff --git a/src/main/java/nextstep/security/context/SecurityContextHolder.java b/src/main/java/nextstep/security/context/SecurityContextHolder.java new file mode 100644 index 00000000..a65d9f6b --- /dev/null +++ b/src/main/java/nextstep/security/context/SecurityContextHolder.java @@ -0,0 +1,20 @@ +package nextstep.security.context; + +import java.util.function.Supplier; + +public class SecurityContextHolder { + private static final ThreadLocal> contextHolder = new ThreadLocal<>(); + + public SecurityContext getContext() { + return contextHolder.get().get(); + } + + public void setContext(SecurityContext context) { + contextHolder.set(() -> context); + } + + public void clearContext() { + contextHolder.remove(); + } +} + diff --git a/src/main/java/nextstep/security/core/Authentication.java b/src/main/java/nextstep/security/core/Authentication.java new file mode 100644 index 00000000..dff3129e --- /dev/null +++ b/src/main/java/nextstep/security/core/Authentication.java @@ -0,0 +1,18 @@ +package nextstep.security.core; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; + + +@Getter +@Setter +@AllArgsConstructor +public class Authentication { + private Object principal; + private Object credentials; + private final ArrayList authorities = new ArrayList<>(); + private boolean authenticated = false; +} diff --git a/src/main/java/nextstep/security/core/userdetails/UserDetails.java b/src/main/java/nextstep/security/core/userdetails/UserDetails.java new file mode 100644 index 00000000..766431c8 --- /dev/null +++ b/src/main/java/nextstep/security/core/userdetails/UserDetails.java @@ -0,0 +1,11 @@ +package nextstep.security.core.userdetails; + +import java.util.Collection; + +public interface UserDetails { + + Collection getAuthorities(); + String getPassword(); + String getUsername(); + +} diff --git a/src/main/java/nextstep/security/core/userdetails/UserDetailsService.java b/src/main/java/nextstep/security/core/userdetails/UserDetailsService.java new file mode 100644 index 00000000..b1f7923f --- /dev/null +++ b/src/main/java/nextstep/security/core/userdetails/UserDetailsService.java @@ -0,0 +1,5 @@ +package nextstep.security.core.userdetails; + +public interface UserDetailsService { + UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; +} diff --git a/src/main/java/nextstep/security/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/web/authentication/BasicAuthenticationInterceptor.java similarity index 79% rename from src/main/java/nextstep/security/BasicAuthenticationInterceptor.java rename to src/main/java/nextstep/security/web/authentication/BasicAuthenticationInterceptor.java index b4b54e56..1be7601c 100644 --- a/src/main/java/nextstep/security/BasicAuthenticationInterceptor.java +++ b/src/main/java/nextstep/security/web/authentication/BasicAuthenticationInterceptor.java @@ -1,4 +1,4 @@ -package nextstep.security; +package nextstep.security.web.authentication; import org.springframework.web.servlet.HandlerInterceptor; @@ -8,6 +8,9 @@ public class BasicAuthenticationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String authorization = request.getHeader("Authorization"); + + return HandlerInterceptor.super.preHandle(request, response, handler); } } diff --git a/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java b/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java new file mode 100644 index 00000000..44447565 --- /dev/null +++ b/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java @@ -0,0 +1,31 @@ +package nextstep.security.web.authentication; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Component +@RequiredArgsConstructor +public class UsernamePasswordAuthenticationInterceptor implements HandlerInterceptor { + + //app 패키지는 security 패키지에 의존할 수 있지만, security 패키지는 app 패키지에 의존하지 않도록 한다.. +// private final LoginService loginService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + /* String email = request.getParameter("username"); + String password = request.getParameter("password"); + + Member loginMember = loginService.login(email, password); + if (loginMember == null) { + return false; + } + + request.getSession().setAttribute("member", loginMember); + return true;*/ + return HandlerInterceptor.super.preHandle(request, response, handler); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/app/MemberTest.java b/src/test/java/nextstep/app/MemberTest.java index 58aba17b..5df7ef6b 100644 --- a/src/test/java/nextstep/app/MemberTest.java +++ b/src/test/java/nextstep/app/MemberTest.java @@ -42,7 +42,7 @@ void members() throws Exception { String encoded = Base64Utils.encodeToString(token.getBytes()); ResultActions loginResponse = mockMvc.perform(get("/members") - .header("Authorization", "Basic " + encoded) + .header("Authorization", "Basic " + encoded) //Basic YUBhLmNvbTpwYXNzd29yZA== .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); From 014c2ae3cfa4f93b5d30eedde2eb623189cde681 Mon Sep 17 00:00:00 2001 From: Jealyang Date: Sun, 3 Nov 2024 00:01:03 +0900 Subject: [PATCH 08/17] =?UTF-8?q?Exception=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/core/AuthenticationException.java | 11 +++++++++++ .../core/userdetails/UsernameNotFoundException.java | 12 ++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/main/java/nextstep/security/core/AuthenticationException.java create mode 100644 src/main/java/nextstep/security/core/userdetails/UsernameNotFoundException.java diff --git a/src/main/java/nextstep/security/core/AuthenticationException.java b/src/main/java/nextstep/security/core/AuthenticationException.java new file mode 100644 index 00000000..dba7fe33 --- /dev/null +++ b/src/main/java/nextstep/security/core/AuthenticationException.java @@ -0,0 +1,11 @@ +package nextstep.security.core; + +public abstract class AuthenticationException extends RuntimeException { + public AuthenticationException(String msg) { + super(msg); + } + public AuthenticationException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/src/main/java/nextstep/security/core/userdetails/UsernameNotFoundException.java b/src/main/java/nextstep/security/core/userdetails/UsernameNotFoundException.java new file mode 100644 index 00000000..130f730c --- /dev/null +++ b/src/main/java/nextstep/security/core/userdetails/UsernameNotFoundException.java @@ -0,0 +1,12 @@ +package nextstep.security.core.userdetails; + +import nextstep.security.core.AuthenticationException; + +public class UsernameNotFoundException extends AuthenticationException { + public UsernameNotFoundException(String msg) { + super(msg); + } + public UsernameNotFoundException(String msg, Throwable cause) { + super(msg, cause); + } +} From 146a158ce1dd0749cb1ee6c4234aeb4ff5b5d5fd Mon Sep 17 00:00:00 2001 From: Jealyang Date: Sun, 3 Nov 2024 15:00:30 +0900 Subject: [PATCH 09/17] =?UTF-8?q?=EC=95=84=EC=9D=B4=EB=94=94=20=ED=8C=A8?= =?UTF-8?q?=EC=8A=A4=EC=9B=8C=EB=93=9C=20=EA=B8=B0=EB=B0=98=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/config/AppConfig.java | 24 +++++++++ .../java/nextstep/app/config/WebConfig.java | 3 +- .../nextstep/app/service/LoginService.java | 27 ---------- .../nextstep/app/service/MemberService.java | 29 +++++++++++ .../java/nextstep/app/ui/LoginController.java | 28 ++--------- .../authentication/AuthenticationManager.java | 10 ++-- .../AuthenticationProvider.java | 49 +++++++++++++++++++ .../BadCredentialsException.java | 13 +++++ .../security/core/Authentication.java | 13 +++-- .../core/AuthenticationException.java | 2 +- .../core/authority/GrantedAuthority.java | 12 +++++ .../security/core/userdetails/User.java | 32 ++++++++++++ .../core/userdetails/UserDetails.java | 7 +-- ...namePasswordAuthenticationInterceptor.java | 37 +++++++++----- 14 files changed, 210 insertions(+), 76 deletions(-) create mode 100644 src/main/java/nextstep/app/config/AppConfig.java delete mode 100644 src/main/java/nextstep/app/service/LoginService.java create mode 100644 src/main/java/nextstep/app/service/MemberService.java create mode 100644 src/main/java/nextstep/security/authentication/AuthenticationProvider.java create mode 100644 src/main/java/nextstep/security/authentication/BadCredentialsException.java create mode 100644 src/main/java/nextstep/security/core/authority/GrantedAuthority.java create mode 100644 src/main/java/nextstep/security/core/userdetails/User.java diff --git a/src/main/java/nextstep/app/config/AppConfig.java b/src/main/java/nextstep/app/config/AppConfig.java new file mode 100644 index 00000000..910ab809 --- /dev/null +++ b/src/main/java/nextstep/app/config/AppConfig.java @@ -0,0 +1,24 @@ +package nextstep.app.config; + +import nextstep.app.domain.MemberRepository; +import nextstep.app.infrastructure.InmemoryMemberRepository; +import nextstep.app.service.MemberService; +import nextstep.security.core.userdetails.UserDetailsService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +//@Configuration +//@EnableAspectJAutoProxy +public class AppConfig { + + @Bean + UserDetailsService userDetailsService() { + return new MemberService(memberRepository()); + } + + @Bean + MemberRepository memberRepository() { + return new InmemoryMemberRepository(); + } +} diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index b2f22353..55a15602 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -1,6 +1,5 @@ package nextstep.app.config; -import nextstep.security.web.authentication.BasicAuthenticationInterceptor; import nextstep.security.web.authentication.UsernamePasswordAuthenticationInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; @@ -11,7 +10,7 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UsernamePasswordAuthenticationInterceptor()); - registry.addInterceptor(new BasicAuthenticationInterceptor()).addPathPatterns("/members"); +// registry.addInterceptor(new BasicAuthenticationInterceptor()).addPathPatterns("/members"); } } diff --git a/src/main/java/nextstep/app/service/LoginService.java b/src/main/java/nextstep/app/service/LoginService.java deleted file mode 100644 index 05b94641..00000000 --- a/src/main/java/nextstep/app/service/LoginService.java +++ /dev/null @@ -1,27 +0,0 @@ -package nextstep.app.service; - -import lombok.RequiredArgsConstructor; -import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; -import nextstep.app.ui.AuthenticationException; -import org.springframework.stereotype.Service; - -import javax.servlet.http.HttpSession; - -import static nextstep.app.ui.LoginController.SPRING_SECURITY_CONTEXT_KEY; - -@Service -@RequiredArgsConstructor -public class LoginService { - - private final MemberRepository memberRepository; - - public Member login(String email, String password) { - //로그인 실패 시 예외 발생 - Member member = memberRepository.findByEmail(email).orElseThrow(AuthenticationException::new); - if (!email.equals(member.getEmail()) || !password.equals(member.getPassword())) { - throw new AuthenticationException(); - } - return member; - } -} diff --git a/src/main/java/nextstep/app/service/MemberService.java b/src/main/java/nextstep/app/service/MemberService.java new file mode 100644 index 00000000..ec1959bb --- /dev/null +++ b/src/main/java/nextstep/app/service/MemberService.java @@ -0,0 +1,29 @@ +package nextstep.app.service; + +import lombok.RequiredArgsConstructor; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.security.core.userdetails.User; +import nextstep.security.core.userdetails.UserDetails; +import nextstep.security.core.userdetails.UserDetailsService; +import nextstep.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + + +@Service +@RequiredArgsConstructor +public class MemberService implements UserDetailsService { + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new UsernameNotFoundException(username)); + + return User.builder() + .username(member.getEmail()) + .password(member.getPassword()) + .roles("USER") + .build(); + } +} diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 1215e626..64a73f2a 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,9 +1,6 @@ package nextstep.app.ui; -import lombok.RequiredArgsConstructor; -import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; -import nextstep.app.service.LoginService; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -12,33 +9,14 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; -import java.util.Map; +@Slf4j @RestController -@RequiredArgsConstructor public class LoginController { - public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - - private final LoginService loginService; @PostMapping("/login") public ResponseEntity login(HttpServletRequest request, HttpSession session) { - String email = request.getParameter("username"); - String password = request.getParameter("password"); - - //로그인 실패 시 예외 발생 - Member loginMember = loginService.login(email, password); - - //로그인 성공 - if (loginMember != null) { - session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, loginMember); //session name, object - } - + log.info("Login request received"); return ResponseEntity.ok().build(); } - - @ExceptionHandler(AuthenticationException.class) - public ResponseEntity handleAuthenticationException() { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } } diff --git a/src/main/java/nextstep/security/authentication/AuthenticationManager.java b/src/main/java/nextstep/security/authentication/AuthenticationManager.java index 71a174dc..604b166e 100644 --- a/src/main/java/nextstep/security/authentication/AuthenticationManager.java +++ b/src/main/java/nextstep/security/authentication/AuthenticationManager.java @@ -1,8 +1,12 @@ package nextstep.security.authentication; import nextstep.security.core.Authentication; +import nextstep.security.core.AuthenticationException; -public interface AuthenticationManager { - Authentication authenticate(Authentication authentication); - //Authentication authenticate(Authentication authentication) throws AuthenticationException; +public class AuthenticationManager { + private final AuthenticationProvider authenticationProvider = new AuthenticationProvider(); + + public Authentication authenticate(Authentication authentication) throws AuthenticationException{ + return authenticationProvider.authenticate(authentication); + } } diff --git a/src/main/java/nextstep/security/authentication/AuthenticationProvider.java b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java new file mode 100644 index 00000000..cead6388 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java @@ -0,0 +1,49 @@ +package nextstep.security.authentication; + +import nextstep.app.infrastructure.InmemoryMemberRepository; +import nextstep.app.service.MemberService; +import nextstep.security.core.Authentication; +import nextstep.security.core.AuthenticationException; +import nextstep.security.core.userdetails.UserDetails; +import nextstep.security.core.userdetails.UserDetailsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Configurable; + +import java.util.Objects; + +//@Configurable +public class AuthenticationProvider { + +// @Autowired + private UserDetailsService userDetailsService = new MemberService(new InmemoryMemberRepository()); + + + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + // 사용자 정보 load + UserDetails loadedUser = retrieveUser((String) authentication.getPrincipal()); + + // 패스워드 체크 + authenticationChecks(authentication, loadedUser); + + // 인증 정보 생성 + return createSuccessAuthentication(loadedUser); + } + + private Authentication createSuccessAuthentication(UserDetails user) { + Authentication authentication = new Authentication(user, null, user.getAuthorities()); + authentication.setAuthenticated(true); + return authentication; + } + + private void authenticationChecks(Authentication authentication, UserDetails loadedUser) { + System.out.println("authentication = " + authentication.getCredentials()); + System.out.println("loadedUser = " + loadedUser.getPassword()); + if (!Objects.equals(authentication.getCredentials().toString(), loadedUser.getPassword())) { + throw new BadCredentialsException("Bad credentials"); + } + } + + private UserDetails retrieveUser(String username) { + return userDetailsService.loadUserByUsername(username); + } +} diff --git a/src/main/java/nextstep/security/authentication/BadCredentialsException.java b/src/main/java/nextstep/security/authentication/BadCredentialsException.java new file mode 100644 index 00000000..be46a7a0 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/BadCredentialsException.java @@ -0,0 +1,13 @@ +package nextstep.security.authentication; + +import nextstep.security.core.AuthenticationException; + +public class BadCredentialsException extends AuthenticationException { + public BadCredentialsException(String msg) { + super(msg); + } + + public BadCredentialsException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/src/main/java/nextstep/security/core/Authentication.java b/src/main/java/nextstep/security/core/Authentication.java index dff3129e..971ad1db 100644 --- a/src/main/java/nextstep/security/core/Authentication.java +++ b/src/main/java/nextstep/security/core/Authentication.java @@ -3,16 +3,23 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; +import nextstep.security.core.authority.GrantedAuthority; -import java.util.ArrayList; +import java.util.Collection; @Getter @Setter @AllArgsConstructor public class Authentication { - private Object principal; + private final Object principal; private Object credentials; - private final ArrayList authorities = new ArrayList<>(); + private final Collection authorities; private boolean authenticated = false; + + public Authentication(Object principal, Object credentials, Collection authorities) { + this.principal = principal; + this.credentials = credentials; + this.authorities = authorities; + } } diff --git a/src/main/java/nextstep/security/core/AuthenticationException.java b/src/main/java/nextstep/security/core/AuthenticationException.java index dba7fe33..b63b8b3d 100644 --- a/src/main/java/nextstep/security/core/AuthenticationException.java +++ b/src/main/java/nextstep/security/core/AuthenticationException.java @@ -1,6 +1,6 @@ package nextstep.security.core; -public abstract class AuthenticationException extends RuntimeException { +public class AuthenticationException extends RuntimeException { public AuthenticationException(String msg) { super(msg); } diff --git a/src/main/java/nextstep/security/core/authority/GrantedAuthority.java b/src/main/java/nextstep/security/core/authority/GrantedAuthority.java new file mode 100644 index 00000000..3469ae14 --- /dev/null +++ b/src/main/java/nextstep/security/core/authority/GrantedAuthority.java @@ -0,0 +1,12 @@ +package nextstep.security.core.authority; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@AllArgsConstructor +@Getter +@Setter +public class GrantedAuthority { + private final String role; +} diff --git a/src/main/java/nextstep/security/core/userdetails/User.java b/src/main/java/nextstep/security/core/userdetails/User.java new file mode 100644 index 00000000..bce321e2 --- /dev/null +++ b/src/main/java/nextstep/security/core/userdetails/User.java @@ -0,0 +1,32 @@ +package nextstep.security.core.userdetails; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import nextstep.security.core.authority.GrantedAuthority; +import org.springframework.util.Assert; + +import java.util.HashSet; +import java.util.Set; + +@Builder +@Getter +@Setter +public class User implements UserDetails { + private String password; + private final String username; + private final Set authorities; + + + public static class UserBuilder { + public UserBuilder roles(String... roles) { + this.authorities = new HashSet<>(); + for (String role : roles) { + Assert.isTrue(!role.startsWith("ROLE_"), + () -> role + " cannot start with ROLE_ (it is automatically added)"); + authorities.add(new GrantedAuthority("ROLE_" + role)); + } + return this; + } + } +} diff --git a/src/main/java/nextstep/security/core/userdetails/UserDetails.java b/src/main/java/nextstep/security/core/userdetails/UserDetails.java index 766431c8..dfe9c294 100644 --- a/src/main/java/nextstep/security/core/userdetails/UserDetails.java +++ b/src/main/java/nextstep/security/core/userdetails/UserDetails.java @@ -1,11 +1,12 @@ package nextstep.security.core.userdetails; +import nextstep.security.core.authority.GrantedAuthority; + import java.util.Collection; public interface UserDetails { - Collection getAuthorities(); - String getPassword(); String getUsername(); - + String getPassword(); + Collection getAuthorities(); } diff --git a/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java b/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java index 44447565..4b787c12 100644 --- a/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java +++ b/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java @@ -1,31 +1,44 @@ package nextstep.security.web.authentication; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; +import lombok.extern.slf4j.Slf4j; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.core.Authentication; +import nextstep.security.core.AuthenticationException; +import nextstep.security.core.userdetails.UserDetails; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -@Component -@RequiredArgsConstructor + +@Slf4j public class UsernamePasswordAuthenticationInterceptor implements HandlerInterceptor { + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + private final AuthenticationManager authenticationManager; - //app 패키지는 security 패키지에 의존할 수 있지만, security 패키지는 app 패키지에 의존하지 않도록 한다.. -// private final LoginService loginService; + public UsernamePasswordAuthenticationInterceptor() { + this.authenticationManager = new AuthenticationManager(); + } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - /* String email = request.getParameter("username"); - String password = request.getParameter("password"); - Member loginMember = loginService.login(email, password); - if (loginMember == null) { + // request parameter 로 전달된 정보를 인증 정보로 변환 + Authentication authentication = new Authentication(request.getParameter("username"), request.getParameter("password"), null); + + // 인증 + try { + authentication = authenticationManager.authenticate(authentication); //throw new AuthenticationException + } catch (AuthenticationException e) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); return false; } - request.getSession().setAttribute("member", loginMember); - return true;*/ - return HandlerInterceptor.super.preHandle(request, response, handler); + // 인증 정보를 세션에 저장 + UserDetails principal = (UserDetails) authentication.getPrincipal(); + request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, principal); + + return true; } } \ No newline at end of file From 7c0920ba7b2d840c83c76089650464748f38290a Mon Sep 17 00:00:00 2001 From: Jealyang Date: Sun, 3 Nov 2024 15:12:09 +0900 Subject: [PATCH 10/17] =?UTF-8?q?IOC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ApplicationContextProvider.java | 21 ++++++++++++++ .../nextstep/app/{config => }/WebConfig.java | 2 +- .../java/nextstep/app/config/AppConfig.java | 24 --------------- .../AuthenticationProvider.java | 29 +++++++++---------- ...namePasswordAuthenticationInterceptor.java | 1 - 5 files changed, 35 insertions(+), 42 deletions(-) create mode 100644 src/main/java/nextstep/app/ApplicationContextProvider.java rename src/main/java/nextstep/app/{config => }/WebConfig.java (95%) delete mode 100644 src/main/java/nextstep/app/config/AppConfig.java diff --git a/src/main/java/nextstep/app/ApplicationContextProvider.java b/src/main/java/nextstep/app/ApplicationContextProvider.java new file mode 100644 index 00000000..59e66474 --- /dev/null +++ b/src/main/java/nextstep/app/ApplicationContextProvider.java @@ -0,0 +1,21 @@ +package nextstep.app; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +@Component +public class ApplicationContextProvider implements ApplicationContextAware { + + private static ApplicationContext context; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + context = applicationContext; + } + + public static ApplicationContext getApplicationContext() { + return context; + } +} diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/WebConfig.java similarity index 95% rename from src/main/java/nextstep/app/config/WebConfig.java rename to src/main/java/nextstep/app/WebConfig.java index 55a15602..c217e2ef 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/WebConfig.java @@ -1,4 +1,4 @@ -package nextstep.app.config; +package nextstep.app; import nextstep.security.web.authentication.UsernamePasswordAuthenticationInterceptor; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/nextstep/app/config/AppConfig.java b/src/main/java/nextstep/app/config/AppConfig.java deleted file mode 100644 index 910ab809..00000000 --- a/src/main/java/nextstep/app/config/AppConfig.java +++ /dev/null @@ -1,24 +0,0 @@ -package nextstep.app.config; - -import nextstep.app.domain.MemberRepository; -import nextstep.app.infrastructure.InmemoryMemberRepository; -import nextstep.app.service.MemberService; -import nextstep.security.core.userdetails.UserDetailsService; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.EnableAspectJAutoProxy; - -//@Configuration -//@EnableAspectJAutoProxy -public class AppConfig { - - @Bean - UserDetailsService userDetailsService() { - return new MemberService(memberRepository()); - } - - @Bean - MemberRepository memberRepository() { - return new InmemoryMemberRepository(); - } -} diff --git a/src/main/java/nextstep/security/authentication/AuthenticationProvider.java b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java index cead6388..8e3cf4f2 100644 --- a/src/main/java/nextstep/security/authentication/AuthenticationProvider.java +++ b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java @@ -1,26 +1,24 @@ package nextstep.security.authentication; -import nextstep.app.infrastructure.InmemoryMemberRepository; -import nextstep.app.service.MemberService; +import nextstep.app.ApplicationContextProvider; import nextstep.security.core.Authentication; import nextstep.security.core.AuthenticationException; import nextstep.security.core.userdetails.UserDetails; import nextstep.security.core.userdetails.UserDetailsService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Configurable; import java.util.Objects; -//@Configurable public class AuthenticationProvider { -// @Autowired - private UserDetailsService userDetailsService = new MemberService(new InmemoryMemberRepository()); + private final UserDetailsService userDetailsService; + public AuthenticationProvider() { + this.userDetailsService = ApplicationContextProvider.getApplicationContext().getBean(UserDetailsService.class); + } public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 사용자 정보 load - UserDetails loadedUser = retrieveUser((String) authentication.getPrincipal()); + UserDetails loadedUser = retrieveUser(authentication.getPrincipal()); // 패스워드 체크 authenticationChecks(authentication, loadedUser); @@ -29,21 +27,20 @@ public Authentication authenticate(Authentication authentication) throws Authent return createSuccessAuthentication(loadedUser); } - private Authentication createSuccessAuthentication(UserDetails user) { - Authentication authentication = new Authentication(user, null, user.getAuthorities()); - authentication.setAuthenticated(true); - return authentication; + + private UserDetails retrieveUser(Object username) { + return userDetailsService.loadUserByUsername((String) username); } private void authenticationChecks(Authentication authentication, UserDetails loadedUser) { - System.out.println("authentication = " + authentication.getCredentials()); - System.out.println("loadedUser = " + loadedUser.getPassword()); if (!Objects.equals(authentication.getCredentials().toString(), loadedUser.getPassword())) { throw new BadCredentialsException("Bad credentials"); } } - private UserDetails retrieveUser(String username) { - return userDetailsService.loadUserByUsername(username); + private Authentication createSuccessAuthentication(UserDetails user) { + Authentication authentication = new Authentication(user, null, user.getAuthorities()); + authentication.setAuthenticated(true); + return authentication; } } diff --git a/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java b/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java index 4b787c12..2f42cb1c 100644 --- a/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java +++ b/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java @@ -1,6 +1,5 @@ package nextstep.security.web.authentication; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import nextstep.security.authentication.AuthenticationManager; import nextstep.security.core.Authentication; From 6010c3d8cada77adb4c8945846bfdba38348207f Mon Sep 17 00:00:00 2001 From: Jealyang Date: Sun, 3 Nov 2024 16:57:35 +0900 Subject: [PATCH 11/17] =?UTF-8?q?Basic=20=EC=9D=B8=EC=A6=9D=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 --- src/main/java/nextstep/app/WebConfig.java | 6 +- .../nextstep/app/service/MemberService.java | 2 +- .../security/context/SecurityContext.java | 2 + .../context/SecurityContextHolder.java | 24 ++++-- .../security/core/Authentication.java | 8 ++ .../BasicAuthenticationInterceptor.java | 86 ++++++++++++++++++- ...namePasswordAuthenticationInterceptor.java | 2 +- 7 files changed, 119 insertions(+), 11 deletions(-) diff --git a/src/main/java/nextstep/app/WebConfig.java b/src/main/java/nextstep/app/WebConfig.java index c217e2ef..6b2fe428 100644 --- a/src/main/java/nextstep/app/WebConfig.java +++ b/src/main/java/nextstep/app/WebConfig.java @@ -1,5 +1,6 @@ package nextstep.app; +import nextstep.security.web.authentication.BasicAuthenticationInterceptor; import nextstep.security.web.authentication.UsernamePasswordAuthenticationInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; @@ -9,8 +10,9 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new UsernamePasswordAuthenticationInterceptor()); -// registry.addInterceptor(new BasicAuthenticationInterceptor()).addPathPatterns("/members"); + registry.addInterceptor(new UsernamePasswordAuthenticationInterceptor()).addPathPatterns("/login"); + registry.addInterceptor(new BasicAuthenticationInterceptor()).addPathPatterns("/members"); + //해당 경로에 인가 인터셉터 추가 } } diff --git a/src/main/java/nextstep/app/service/MemberService.java b/src/main/java/nextstep/app/service/MemberService.java index ec1959bb..445dfd70 100644 --- a/src/main/java/nextstep/app/service/MemberService.java +++ b/src/main/java/nextstep/app/service/MemberService.java @@ -23,7 +23,7 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx return User.builder() .username(member.getEmail()) .password(member.getPassword()) - .roles("USER") + .roles("MEMBER") .build(); } } diff --git a/src/main/java/nextstep/security/context/SecurityContext.java b/src/main/java/nextstep/security/context/SecurityContext.java index 6aaf143e..8d63e6d4 100644 --- a/src/main/java/nextstep/security/context/SecurityContext.java +++ b/src/main/java/nextstep/security/context/SecurityContext.java @@ -3,10 +3,12 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import nextstep.security.core.Authentication; @AllArgsConstructor +@NoArgsConstructor @Getter @Setter public class SecurityContext { diff --git a/src/main/java/nextstep/security/context/SecurityContextHolder.java b/src/main/java/nextstep/security/context/SecurityContextHolder.java index a65d9f6b..2e163b23 100644 --- a/src/main/java/nextstep/security/context/SecurityContextHolder.java +++ b/src/main/java/nextstep/security/context/SecurityContextHolder.java @@ -1,20 +1,34 @@ package nextstep.security.context; +import org.springframework.util.Assert; + import java.util.function.Supplier; public class SecurityContextHolder { private static final ThreadLocal> contextHolder = new ThreadLocal<>(); - public SecurityContext getContext() { - return contextHolder.get().get(); + public static SecurityContext getContext() { + Supplier result = contextHolder.get(); + if (result == null) { + SecurityContext context = createEmptyContext(); + result = () -> context; + contextHolder.set(result); + } + + return result.get(); } - public void setContext(SecurityContext context) { - contextHolder.set(() -> context); + public static void setContext(SecurityContext context) { + Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); + contextHolder.set(() -> context); //map.set(this, value); } - public void clearContext() { + public static void clearContext() { contextHolder.remove(); } + + private static SecurityContext createEmptyContext() { + return new SecurityContext(); + } } diff --git a/src/main/java/nextstep/security/core/Authentication.java b/src/main/java/nextstep/security/core/Authentication.java index 971ad1db..e3d889f1 100644 --- a/src/main/java/nextstep/security/core/Authentication.java +++ b/src/main/java/nextstep/security/core/Authentication.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.Setter; import nextstep.security.core.authority.GrantedAuthority; +import nextstep.security.core.userdetails.UserDetails; import java.util.Collection; @@ -22,4 +23,11 @@ public Authentication(Object principal, Object credentials, Collection Member 로 등록되어있는 사용자만 가능하도록 한다 \ No newline at end of file diff --git a/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java b/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java index 2f42cb1c..dfd49fa2 100644 --- a/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java +++ b/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java @@ -28,7 +28,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons // 인증 try { - authentication = authenticationManager.authenticate(authentication); //throw new AuthenticationException + authentication = authenticationManager.authenticate(authentication); } catch (AuthenticationException e) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); return false; From a4f89826c6e334deabf27999cc46b02f470d09ba Mon Sep 17 00:00:00 2001 From: Jealyang Date: Sun, 3 Nov 2024 17:17:36 +0900 Subject: [PATCH 12/17] =?UTF-8?q?Authentication=20Interceptor=20=EC=83=81?= =?UTF-8?q?=EC=86=8D=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AuthenticationInterceptor.java | 45 +++++++++++++++++++ .../BasicAuthenticationInterceptor.java | 36 ++++----------- ...namePasswordAuthenticationInterceptor.java | 31 ++++--------- 3 files changed, 62 insertions(+), 50 deletions(-) create mode 100644 src/main/java/nextstep/security/web/authentication/AuthenticationInterceptor.java diff --git a/src/main/java/nextstep/security/web/authentication/AuthenticationInterceptor.java b/src/main/java/nextstep/security/web/authentication/AuthenticationInterceptor.java new file mode 100644 index 00000000..44d42ba7 --- /dev/null +++ b/src/main/java/nextstep/security/web/authentication/AuthenticationInterceptor.java @@ -0,0 +1,45 @@ +package nextstep.security.web.authentication; + +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.context.SecurityContext; +import nextstep.security.context.SecurityContextHolder; +import nextstep.security.core.Authentication; +import nextstep.security.core.AuthenticationException; +import nextstep.security.core.userdetails.UserDetails; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public abstract class AuthenticationInterceptor implements HandlerInterceptor { + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + private final AuthenticationManager authenticationManager = new AuthenticationManager(); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + Authentication authRequest = (Authentication) request.getAttribute("authRequest"); + Authentication authResponse; + + // 인증 + try { + authResponse = authenticationManager.authenticate(authRequest); + } catch (AuthenticationException e) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); + return false; + } + + //인증 정보 저장 + SecurityContextHolder.setContext(new SecurityContext(authResponse)); + + // 인증 정보를 세션에 저장 + UserDetails principal = (UserDetails) authResponse.getPrincipal(); + request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, principal); + return HandlerInterceptor.super.preHandle(request, response, handler); + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + // SecurityContext 초기화 + SecurityContextHolder.clearContext(); + } +} diff --git a/src/main/java/nextstep/security/web/authentication/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/web/authentication/BasicAuthenticationInterceptor.java index c183f079..be3dee59 100644 --- a/src/main/java/nextstep/security/web/authentication/BasicAuthenticationInterceptor.java +++ b/src/main/java/nextstep/security/web/authentication/BasicAuthenticationInterceptor.java @@ -1,33 +1,21 @@ package nextstep.security.web.authentication; -import nextstep.security.authentication.AuthenticationManager; import nextstep.security.authentication.BadCredentialsException; -import nextstep.security.context.SecurityContext; import nextstep.security.context.SecurityContextHolder; import nextstep.security.core.Authentication; -import nextstep.security.core.AuthenticationException; -import nextstep.security.core.userdetails.UserDetails; import org.springframework.util.StringUtils; -import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.nio.charset.StandardCharsets; import java.util.Base64; -public class BasicAuthenticationInterceptor implements HandlerInterceptor { - public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - private final AuthenticationManager authenticationManager; +public class BasicAuthenticationInterceptor extends AuthenticationInterceptor { - public BasicAuthenticationInterceptor() { - this.authenticationManager = new AuthenticationManager(); - } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - Authentication authRequest; - Authentication authResponse; // 인증 정보 추출 try { @@ -37,6 +25,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons return false; } + // 인증 정보가 없을 경우 로그인 페이지로 리다이렉트 if (authRequest == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; @@ -44,26 +33,17 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons //인증이 필요할 경우 인증 if (authenticationIsRequired((String) authRequest.getPrincipal())) { - - // 인증 - try { - authResponse = authenticationManager.authenticate(authRequest); - } catch (AuthenticationException e) { - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); - return false; - } - - //인증 정보 저장 - SecurityContextHolder.setContext(new SecurityContext(authResponse)); - - // 인증 정보를 세션에 저장 - UserDetails principal = (UserDetails) authResponse.getPrincipal(); - request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, principal); + request.setAttribute("authRequest", authRequest); + super.preHandle(request, response, handler); } return true; //다음 인터셉터로 } + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + super.afterCompletion(request, response, handler, ex); + } private Authentication convert(HttpServletRequest request) { String header = request.getHeader("Authorization"); diff --git a/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java b/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java index dfd49fa2..b9de89f1 100644 --- a/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java +++ b/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java @@ -1,43 +1,30 @@ package nextstep.security.web.authentication; import lombok.extern.slf4j.Slf4j; -import nextstep.security.authentication.AuthenticationManager; import nextstep.security.core.Authentication; -import nextstep.security.core.AuthenticationException; -import nextstep.security.core.userdetails.UserDetails; -import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Slf4j -public class UsernamePasswordAuthenticationInterceptor implements HandlerInterceptor { - public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - private final AuthenticationManager authenticationManager; - - public UsernamePasswordAuthenticationInterceptor() { - this.authenticationManager = new AuthenticationManager(); - } +public class UsernamePasswordAuthenticationInterceptor extends AuthenticationInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // request parameter 로 전달된 정보를 인증 정보로 변환 - Authentication authentication = new Authentication(request.getParameter("username"), request.getParameter("password"), null); + Authentication authRequest = new Authentication(request.getParameter("username"), request.getParameter("password"), null); // 인증 - try { - authentication = authenticationManager.authenticate(authentication); - } catch (AuthenticationException e) { - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); - return false; - } - - // 인증 정보를 세션에 저장 - UserDetails principal = (UserDetails) authentication.getPrincipal(); - request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, principal); + request.setAttribute("authRequest", authRequest); + super.preHandle(request, response, handler); return true; } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + super.afterCompletion(request, response, handler, ex); + } } \ No newline at end of file From ad3eb2247782997406852fb7c96a0a9f7cb15c34 Mon Sep 17 00:00:00 2001 From: Jealyang Date: Sun, 3 Nov 2024 17:27:27 +0900 Subject: [PATCH 13/17] polishing --- .../authentication/AuthenticationInterceptor.java | 12 +++++++----- .../BasicAuthenticationInterceptor.java | 5 ++--- .../UsernamePasswordAuthenticationInterceptor.java | 5 +---- .../web/authorization/AuthorizationInterceptor.java | 13 +++++++++++++ 4 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 src/main/java/nextstep/security/web/authorization/AuthorizationInterceptor.java diff --git a/src/main/java/nextstep/security/web/authentication/AuthenticationInterceptor.java b/src/main/java/nextstep/security/web/authentication/AuthenticationInterceptor.java index 44d42ba7..8bb123a1 100644 --- a/src/main/java/nextstep/security/web/authentication/AuthenticationInterceptor.java +++ b/src/main/java/nextstep/security/web/authentication/AuthenticationInterceptor.java @@ -20,21 +20,23 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons Authentication authRequest = (Authentication) request.getAttribute("authRequest"); Authentication authResponse; - // 인증 try { + // 인증 authResponse = authenticationManager.authenticate(authRequest); + + //인증 정보 저장 + SecurityContextHolder.setContext(new SecurityContext(authResponse)); + } catch (AuthenticationException e) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); return false; } - //인증 정보 저장 - SecurityContextHolder.setContext(new SecurityContext(authResponse)); - // 인증 정보를 세션에 저장 UserDetails principal = (UserDetails) authResponse.getPrincipal(); request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, principal); - return HandlerInterceptor.super.preHandle(request, response, handler); + + return true; } @Override diff --git a/src/main/java/nextstep/security/web/authentication/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/web/authentication/BasicAuthenticationInterceptor.java index be3dee59..3d1ca83e 100644 --- a/src/main/java/nextstep/security/web/authentication/BasicAuthenticationInterceptor.java +++ b/src/main/java/nextstep/security/web/authentication/BasicAuthenticationInterceptor.java @@ -16,7 +16,6 @@ public class BasicAuthenticationInterceptor extends AuthenticationInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Authentication authRequest; - // 인증 정보 추출 try { authRequest = convert(request); @@ -34,10 +33,10 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons //인증이 필요할 경우 인증 if (authenticationIsRequired((String) authRequest.getPrincipal())) { request.setAttribute("authRequest", authRequest); - super.preHandle(request, response, handler); + return super.preHandle(request, response, handler); } - return true; //다음 인터셉터로 + return true; } @Override diff --git a/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java b/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java index b9de89f1..9b8dc58a 100644 --- a/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java +++ b/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java @@ -12,15 +12,12 @@ public class UsernamePasswordAuthenticationInterceptor extends AuthenticationInt @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - // request parameter 로 전달된 정보를 인증 정보로 변환 Authentication authRequest = new Authentication(request.getParameter("username"), request.getParameter("password"), null); // 인증 request.setAttribute("authRequest", authRequest); - super.preHandle(request, response, handler); - - return true; + return super.preHandle(request, response, handler); } @Override diff --git a/src/main/java/nextstep/security/web/authorization/AuthorizationInterceptor.java b/src/main/java/nextstep/security/web/authorization/AuthorizationInterceptor.java new file mode 100644 index 00000000..e44bfcf6 --- /dev/null +++ b/src/main/java/nextstep/security/web/authorization/AuthorizationInterceptor.java @@ -0,0 +1,13 @@ +package nextstep.security.web.authorization; + +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class AuthorizationInterceptor implements HandlerInterceptor { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + return HandlerInterceptor.super.preHandle(request, response, handler); + } +} From 4d308a0515b3458c8132d251c02055019ee45ccd Mon Sep 17 00:00:00 2001 From: Jealyang Date: Sun, 3 Nov 2024 18:21:22 +0900 Subject: [PATCH 14/17] =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/app/WebConfig.java | 5 +-- ...nticationCredentialsNotFoundException.java | 9 +++++ .../authorization/AccessDeniedException.java | 12 +++++++ .../authorization/AuthorizationDecision.java | 10 ++++++ .../authorization/AuthorizationManager.java | 17 ++++++++++ .../AuthorizationInterceptor.java | 34 ++++++++++++++++++- 6 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 src/main/java/nextstep/security/authentication/AuthenticationCredentialsNotFoundException.java create mode 100644 src/main/java/nextstep/security/authorization/AccessDeniedException.java create mode 100644 src/main/java/nextstep/security/authorization/AuthorizationDecision.java create mode 100644 src/main/java/nextstep/security/authorization/AuthorizationManager.java diff --git a/src/main/java/nextstep/app/WebConfig.java b/src/main/java/nextstep/app/WebConfig.java index 6b2fe428..6d36b20e 100644 --- a/src/main/java/nextstep/app/WebConfig.java +++ b/src/main/java/nextstep/app/WebConfig.java @@ -2,6 +2,7 @@ import nextstep.security.web.authentication.BasicAuthenticationInterceptor; import nextstep.security.web.authentication.UsernamePasswordAuthenticationInterceptor; +import nextstep.security.web.authorization.AuthorizationInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -11,8 +12,8 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UsernamePasswordAuthenticationInterceptor()).addPathPatterns("/login"); + // /members 경로는 인증 후 인가 처리 registry.addInterceptor(new BasicAuthenticationInterceptor()).addPathPatterns("/members"); - //해당 경로에 인가 인터셉터 추가 + registry.addInterceptor(new AuthorizationInterceptor()).addPathPatterns("/members"); } - } diff --git a/src/main/java/nextstep/security/authentication/AuthenticationCredentialsNotFoundException.java b/src/main/java/nextstep/security/authentication/AuthenticationCredentialsNotFoundException.java new file mode 100644 index 00000000..0ea0aaeb --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationCredentialsNotFoundException.java @@ -0,0 +1,9 @@ +package nextstep.security.authentication; + +import nextstep.security.core.AuthenticationException; + +public class AuthenticationCredentialsNotFoundException extends AuthenticationException { + public AuthenticationCredentialsNotFoundException(String msg) { + super(msg); + } +} diff --git a/src/main/java/nextstep/security/authorization/AccessDeniedException.java b/src/main/java/nextstep/security/authorization/AccessDeniedException.java new file mode 100644 index 00000000..07a66488 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AccessDeniedException.java @@ -0,0 +1,12 @@ +package nextstep.security.authorization; + +/** + * Thrown if an Authentication object does not hold a required authority. + */ +public class AccessDeniedException extends RuntimeException { + + public AccessDeniedException(String msg) {super(msg);} + public AccessDeniedException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/src/main/java/nextstep/security/authorization/AuthorizationDecision.java b/src/main/java/nextstep/security/authorization/AuthorizationDecision.java new file mode 100644 index 00000000..f91966e8 --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AuthorizationDecision.java @@ -0,0 +1,10 @@ +package nextstep.security.authorization; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class AuthorizationDecision { + private final boolean granted; +} diff --git a/src/main/java/nextstep/security/authorization/AuthorizationManager.java b/src/main/java/nextstep/security/authorization/AuthorizationManager.java new file mode 100644 index 00000000..094be54b --- /dev/null +++ b/src/main/java/nextstep/security/authorization/AuthorizationManager.java @@ -0,0 +1,17 @@ +package nextstep.security.authorization; + +import nextstep.security.core.Authentication; + +import javax.servlet.http.HttpServletRequest; +import java.util.function.Supplier; + +public class AuthorizationManager { + public AuthorizationDecision check(Supplier authentication, HttpServletRequest request) { + boolean granted = isGranted(authentication.get()); + return new AuthorizationDecision(granted); + } + + private boolean isGranted(Authentication authentication) { + return authentication != null && authentication.isAuthenticated(); + } +} diff --git a/src/main/java/nextstep/security/web/authorization/AuthorizationInterceptor.java b/src/main/java/nextstep/security/web/authorization/AuthorizationInterceptor.java index e44bfcf6..82080891 100644 --- a/src/main/java/nextstep/security/web/authorization/AuthorizationInterceptor.java +++ b/src/main/java/nextstep/security/web/authorization/AuthorizationInterceptor.java @@ -1,13 +1,45 @@ package nextstep.security.web.authorization; +import nextstep.security.authentication.AuthenticationCredentialsNotFoundException; +import nextstep.security.authorization.AccessDeniedException; +import nextstep.security.authorization.AuthorizationDecision; +import nextstep.security.authorization.AuthorizationManager; +import nextstep.security.context.SecurityContextHolder; +import nextstep.security.core.Authentication; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class AuthorizationInterceptor implements HandlerInterceptor { + + private final AuthorizationManager authorizationManager = new AuthorizationManager(); + @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - return HandlerInterceptor.super.preHandle(request, response, handler); + + try { + // 인가 : AuthorizationManger 에게 Authentication 전달하여 권한 확인 + AuthorizationDecision decision = authorizationManager.check(this::getAuthentication, request); + + // 인가 권한이 안맞다면 예외 발생 + if (decision != null && !decision.isGranted()) { + throw new AccessDeniedException("Unauthorized"); + } + + } catch (AuthenticationCredentialsNotFoundException | AccessDeniedException e) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); + return false; + } + + return true; + } + + private Authentication getAuthentication() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + throw new AuthenticationCredentialsNotFoundException("Unauthenticated"); + } + return authentication; } } From 60b7df168dc68100cdf125863be9f70f915beac7 Mon Sep 17 00:00:00 2001 From: Jealyang Date: Sun, 3 Nov 2024 18:22:18 +0900 Subject: [PATCH 15/17] polishing --- .../security/authorization/AccessDeniedException.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/nextstep/security/authorization/AccessDeniedException.java b/src/main/java/nextstep/security/authorization/AccessDeniedException.java index 07a66488..9b48f514 100644 --- a/src/main/java/nextstep/security/authorization/AccessDeniedException.java +++ b/src/main/java/nextstep/security/authorization/AccessDeniedException.java @@ -5,8 +5,8 @@ */ public class AccessDeniedException extends RuntimeException { - public AccessDeniedException(String msg) {super(msg);} - public AccessDeniedException(String msg, Throwable cause) { - super(msg, cause); + public AccessDeniedException(String msg) { + super(msg); } + } From 518d17ab611c05b524ad4dc93c57f336bd16c8f3 Mon Sep 17 00:00:00 2001 From: Jealyang Date: Sun, 3 Nov 2024 19:04:16 +0900 Subject: [PATCH 16/17] =?UTF-8?q?README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9b2d4c5b..393378e3 100644 --- a/README.md +++ b/README.md @@ -4,28 +4,35 @@ 아이디와 비밀번호를 기반으로 로그인 기능을 구현하고
Basic 인증을 사용하여 사용자를 식별할 수 있도록 스프링 프레임워크를 사용하여 웹 앱으로 구현한다. -- ``Spring security``가 아니라 ``Session``과 ``Interceptor`` 를 기반으로 구현 +- ``Spring security``의 내부 구조를 분석해 직접 구현 (단, ``Filter`` 대신 ``Interceptor`` 활용) --- # 구현 요구 사항 -1. 아이디와 비밀번호 기반 로그인 구현 +1. 아이디와 비밀번호 기반 로그인 인증 구현 + 로그인 요청 시 사용자가 입력한 아이디와 패스워드를 확인하여 인증한다. + 로그인 성공 시 ``Session`` 을 사용하여 인증 정보를 저장한다. 2. Basic 인증 구현 - + 사용자 목록 조회 기능 이용 시 - + ``Member``로 등록되어 있는 사용자만 가능하도록 한다. - + 이를 위해 Basic 인증을 사용하여 사용자를 식별한다. - + 요청의 **Authorization 헤더**에서 Basic 인증 정보를 추출하여 인증을 처리한다. - + 인증 성공 시 ``Session``을 사용하여 인증 정보를 저장한다. + + 사용자 목록 조회 기능 (인증 진행 후 인가 진행) + - 인증 : Basic 인증을 사용하여 사용자를 식별한다. + + 이를 위해 요청의 **Authorization 헤더**에서 Basic 인증 정보를 추출 후 decode 하여 인증을 처리한다. + + 인증 성공 시 ``Session``을 사용하여 인증 정보를 저장한다. + + (다만, 인가를 위한 인증 및 권한 정보는 ThreadLocal 에 저장하여 활용) + - 인가 : ``Member``로 등록되어 있는, 인증된 사용자만 가능하도록 한다. + + 인증 ``Interceptor``가 통과되면 인가 ``Interceptor`` 진행 + + ThreadLocal 에서 조회하여 인증 정보가 있으면 인가 3. 인터셉터 분리 + ``HandlerInterceptor``를 사용하여 인증 관련 로직을 ``Controller``에서 분리한다. + 앞서 구현한 두 인증 방식(아이디 비밀번호 로그인 방식과 Basic 인증 방식) 모두 인터셉터에서 처리되도록 구현한다. + 가급적이면 하나의 인터셉터는 하나의 작업만 수행하도록 설계한다. + + 아이디/패스워드 기반 Authentication ``Interceptor`` + + Basic 인증 기반 Authentication ``Interceptor`` + + Authorization ``Interceptor`` + 4. 인증 로직과 서비스 로직 간의 패키지 분리 @@ -51,5 +58,5 @@ Basic 인증을 사용하여 사용자를 식별할 수 있도록 스프링 프 - /login [POST] 아이디와 비밀번호를 확인하여 인증. (인증 후 Session에 인증 정보 저장) ### 사용자 조회 - - /member [GET] 사용자 목록 조회. (단, Member만 인가) + - /member [GET] 사용자 목록 조회. (단, 인증 성공 후 인증 정보가 있을 경우만 인가) From ee76f5be62e180f74db61d8f13b2554e2cfcf1ff Mon Sep 17 00:00:00 2001 From: Jealyang Date: Sun, 3 Nov 2024 19:30:47 +0900 Subject: [PATCH 17/17] =?UTF-8?q?app,=20security=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EA=B0=84=20=EC=96=91=EB=B0=A9=ED=96=A5=20=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=EB=A5=BC=20=ED=95=B4=EA=B2=B0=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20ApplicationContextProvider=20=EC=9D=98=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9C=84=EC=B9=98=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95(Util=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/SecurityAuthenticationApplication.java | 4 ++-- .../security/authentication/AuthenticationProvider.java | 2 +- .../nextstep/{app => util}/ApplicationContextProvider.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/main/java/nextstep/{app => util}/ApplicationContextProvider.java (96%) diff --git a/src/main/java/nextstep/app/SecurityAuthenticationApplication.java b/src/main/java/nextstep/app/SecurityAuthenticationApplication.java index 0f8eb47d..9a8b666d 100644 --- a/src/main/java/nextstep/app/SecurityAuthenticationApplication.java +++ b/src/main/java/nextstep/app/SecurityAuthenticationApplication.java @@ -2,12 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; @SpringBootApplication +@ComponentScan(basePackages = {"nextstep.app", "nextstep.util"}) public class SecurityAuthenticationApplication { - public static void main(String[] args) { SpringApplication.run(SecurityAuthenticationApplication.class, args); } - } diff --git a/src/main/java/nextstep/security/authentication/AuthenticationProvider.java b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java index 8e3cf4f2..03b0eac5 100644 --- a/src/main/java/nextstep/security/authentication/AuthenticationProvider.java +++ b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java @@ -1,6 +1,6 @@ package nextstep.security.authentication; -import nextstep.app.ApplicationContextProvider; +import nextstep.util.ApplicationContextProvider; import nextstep.security.core.Authentication; import nextstep.security.core.AuthenticationException; import nextstep.security.core.userdetails.UserDetails; diff --git a/src/main/java/nextstep/app/ApplicationContextProvider.java b/src/main/java/nextstep/util/ApplicationContextProvider.java similarity index 96% rename from src/main/java/nextstep/app/ApplicationContextProvider.java rename to src/main/java/nextstep/util/ApplicationContextProvider.java index 59e66474..299aa72d 100644 --- a/src/main/java/nextstep/app/ApplicationContextProvider.java +++ b/src/main/java/nextstep/util/ApplicationContextProvider.java @@ -1,4 +1,4 @@ -package nextstep.app; +package nextstep.util; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext;