From c35f54d645611ae279124f5b1902d0ff7cb6d59d Mon Sep 17 00:00:00 2001 From: "daehee.kim" Date: Wed, 30 Oct 2024 17:00:53 +0900 Subject: [PATCH 01/11] initial commit --- README.md | 11 +++++++ build.gradle | 2 ++ .../SecurityAuthenticationApplication.java | 2 +- .../BasicAuthenticationInterceptor.java | 33 +++++++++++++++++++ .../app/config/WebMvcConfiguration.java | 18 ++++++++++ .../service/BasicAuthenticationService.java | 13 ++++++++ .../nextstep/{app => }/domain/Member.java | 2 +- .../{app => }/domain/MemberRepository.java | 2 +- .../InmemoryMemberRepository.java | 6 ++-- .../{app => }/ui/AuthenticationException.java | 2 +- .../{app => }/ui/LoginController.java | 4 +-- .../{app => }/ui/MemberController.java | 6 ++-- src/main/resources/application.properties | 1 - src/main/resources/application.yml | 10 ++++++ src/test/java/nextstep/app/LoginTest.java | 5 ++- src/test/java/nextstep/app/MemberTest.java | 6 ++-- 16 files changed, 104 insertions(+), 19 deletions(-) rename src/main/java/nextstep/{app => }/SecurityAuthenticationApplication.java (93%) create mode 100644 src/main/java/nextstep/app/config/BasicAuthenticationInterceptor.java create mode 100644 src/main/java/nextstep/app/config/WebMvcConfiguration.java create mode 100644 src/main/java/nextstep/app/service/BasicAuthenticationService.java rename src/main/java/nextstep/{app => }/domain/Member.java (95%) rename src/main/java/nextstep/{app => }/domain/MemberRepository.java (87%) rename src/main/java/nextstep/{app => }/infrastructure/InmemoryMemberRepository.java (89%) rename src/main/java/nextstep/{app => }/ui/AuthenticationException.java (72%) rename src/main/java/nextstep/{app => }/ui/LoginController.java (93%) rename src/main/java/nextstep/{app => }/ui/MemberController.java (85%) delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml diff --git a/README.md b/README.md index 1e7ba652..205c7d8c 100644 --- a/README.md +++ b/README.md @@ -1 +1,12 @@ # spring-security-authentication + +- change properties to yml (personal preference) + + +- refactoring packages by hexagonal architecture, I referred to the following URL +https://remonsinnema.com/tag/hexagonal-architecture/ + + +- Authorization would be implemented like: +As interceptor should check request header to + diff --git a/build.gradle b/build.gradle index 99766160..bdf350f4 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,8 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.projectlombok:lombok' + implementation 'org.apache.commons:commons-lang3' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/src/main/java/nextstep/app/SecurityAuthenticationApplication.java b/src/main/java/nextstep/SecurityAuthenticationApplication.java similarity index 93% rename from src/main/java/nextstep/app/SecurityAuthenticationApplication.java rename to src/main/java/nextstep/SecurityAuthenticationApplication.java index 0f8eb47d..1ecd05fe 100644 --- a/src/main/java/nextstep/app/SecurityAuthenticationApplication.java +++ b/src/main/java/nextstep/SecurityAuthenticationApplication.java @@ -1,4 +1,4 @@ -package nextstep.app; +package nextstep; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/src/main/java/nextstep/app/config/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/app/config/BasicAuthenticationInterceptor.java new file mode 100644 index 00000000..7425e3bf --- /dev/null +++ b/src/main/java/nextstep/app/config/BasicAuthenticationInterceptor.java @@ -0,0 +1,33 @@ +package nextstep.app.config; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import lombok.RequiredArgsConstructor; +import nextstep.app.service.BasicAuthenticationService; +import nextstep.domain.Member; +import nextstep.domain.MemberRepository; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +@RequiredArgsConstructor +public class BasicAuthenticationInterceptor implements HandlerInterceptor { + + private final MemberRepository memberRepository; + private final BasicAuthenticationService basicAuthenticationService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + + if (StringUtils.isNotEmpty(authorizationHeader) && authorizationHeader.startsWith("Basic ")) { + Member decodedMember = basicAuthenticationService.decodeAuthorization(authorizationHeader); + return memberRepository.findByEmail(decodedMember.getEmail()).isEmpty(); + } + return false; + } + +} diff --git a/src/main/java/nextstep/app/config/WebMvcConfiguration.java b/src/main/java/nextstep/app/config/WebMvcConfiguration.java new file mode 100644 index 00000000..d75d6bf0 --- /dev/null +++ b/src/main/java/nextstep/app/config/WebMvcConfiguration.java @@ -0,0 +1,18 @@ +package nextstep.app.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class WebMvcConfiguration implements WebMvcConfigurer { + + private final BasicAuthenticationInterceptor basicAuthenticationInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(basicAuthenticationInterceptor); + } +} diff --git a/src/main/java/nextstep/app/service/BasicAuthenticationService.java b/src/main/java/nextstep/app/service/BasicAuthenticationService.java new file mode 100644 index 00000000..6875f7e0 --- /dev/null +++ b/src/main/java/nextstep/app/service/BasicAuthenticationService.java @@ -0,0 +1,13 @@ +package nextstep.app.service; + +import nextstep.domain.Member; +import org.springframework.stereotype.Service; + +@Service +public class BasicAuthenticationService { + + public Member decodeAuthorization(String authKey) { + + } + +} \ No newline at end of file diff --git a/src/main/java/nextstep/app/domain/Member.java b/src/main/java/nextstep/domain/Member.java similarity index 95% rename from src/main/java/nextstep/app/domain/Member.java rename to src/main/java/nextstep/domain/Member.java index 6cafa9c7..0f35ad3b 100644 --- a/src/main/java/nextstep/app/domain/Member.java +++ b/src/main/java/nextstep/domain/Member.java @@ -1,4 +1,4 @@ -package nextstep.app.domain; +package nextstep.domain; public class Member { private final String email; diff --git a/src/main/java/nextstep/app/domain/MemberRepository.java b/src/main/java/nextstep/domain/MemberRepository.java similarity index 87% rename from src/main/java/nextstep/app/domain/MemberRepository.java rename to src/main/java/nextstep/domain/MemberRepository.java index 2eb5cdbb..19bbaf48 100644 --- a/src/main/java/nextstep/app/domain/MemberRepository.java +++ b/src/main/java/nextstep/domain/MemberRepository.java @@ -1,4 +1,4 @@ -package nextstep.app.domain; +package nextstep.domain; import java.util.List; import java.util.Optional; diff --git a/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java b/src/main/java/nextstep/infrastructure/InmemoryMemberRepository.java similarity index 89% rename from src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java rename to src/main/java/nextstep/infrastructure/InmemoryMemberRepository.java index 5a6062cf..45f46169 100644 --- a/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java +++ b/src/main/java/nextstep/infrastructure/InmemoryMemberRepository.java @@ -1,7 +1,7 @@ -package nextstep.app.infrastructure; +package nextstep.infrastructure; -import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; +import nextstep.domain.Member; +import nextstep.domain.MemberRepository; import org.springframework.stereotype.Repository; import java.util.HashMap; diff --git a/src/main/java/nextstep/app/ui/AuthenticationException.java b/src/main/java/nextstep/ui/AuthenticationException.java similarity index 72% rename from src/main/java/nextstep/app/ui/AuthenticationException.java rename to src/main/java/nextstep/ui/AuthenticationException.java index f809b6e4..145a0934 100644 --- a/src/main/java/nextstep/app/ui/AuthenticationException.java +++ b/src/main/java/nextstep/ui/AuthenticationException.java @@ -1,4 +1,4 @@ -package nextstep.app.ui; +package nextstep.ui; public class AuthenticationException extends RuntimeException { } diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/ui/LoginController.java similarity index 93% rename from src/main/java/nextstep/app/ui/LoginController.java rename to src/main/java/nextstep/ui/LoginController.java index 0ea94f1b..f54e27bc 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/ui/LoginController.java @@ -1,6 +1,6 @@ -package nextstep.app.ui; +package nextstep.ui; -import nextstep.app.domain.MemberRepository; +import nextstep.domain.MemberRepository; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/ui/MemberController.java similarity index 85% rename from src/main/java/nextstep/app/ui/MemberController.java rename to src/main/java/nextstep/ui/MemberController.java index c8cc74d6..9cb553d6 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/ui/MemberController.java @@ -1,7 +1,7 @@ -package nextstep.app.ui; +package nextstep.ui; -import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; +import nextstep.domain.Member; +import nextstep.domain.MemberRepository; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b137891..00000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 00000000..c80abf6a --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,10 @@ +spring: + application: + name: spring-security-authentication + +logging: + level: + root: INFO + +server: + port: 8080 \ 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..37a3c56a 100644 --- a/src/test/java/nextstep/app/LoginTest.java +++ b/src/test/java/nextstep/app/LoginTest.java @@ -1,7 +1,7 @@ package nextstep.app; -import nextstep.app.domain.Member; -import nextstep.app.infrastructure.InmemoryMemberRepository; +import nextstep.domain.Member; +import nextstep.infrastructure.InmemoryMemberRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -9,7 +9,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; diff --git a/src/test/java/nextstep/app/MemberTest.java b/src/test/java/nextstep/app/MemberTest.java index 58aba17b..67dd42af 100644 --- a/src/test/java/nextstep/app/MemberTest.java +++ b/src/test/java/nextstep/app/MemberTest.java @@ -1,8 +1,8 @@ package nextstep.app; -import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; -import nextstep.app.infrastructure.InmemoryMemberRepository; +import nextstep.domain.Member; +import nextstep.domain.MemberRepository; +import nextstep.infrastructure.InmemoryMemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; From 9fd1554ef72c093b6f6b2856c24ad32388d273af Mon Sep 17 00:00:00 2001 From: "daehee.kim" Date: Mon, 4 Nov 2024 01:16:01 +0900 Subject: [PATCH 02/11] adjusted interceptor, satisfied all tests --- README.md | 19 +++--- build.gradle | 10 ++- .../SecurityAuthenticationApplication.java | 2 +- .../BasicAuthenticationInterceptor.java | 33 ---------- .../WebMvcConfiguration.java | 8 ++- .../BasicAuthenticationInterceptor.java | 46 ++++++++++++++ .../MemberAuthorizationInterceptor.java | 48 ++++++++++++++ .../nextstep/{ => app}/domain/Member.java | 26 +++----- .../{ => app}/domain/MemberRepository.java | 4 +- .../InmemoryMemberRepository.java | 18 ++++-- .../service/BasicAuthenticationService.java | 13 ---- .../auth/BasicAuthenticationService.java | 32 ++++++++++ .../{ => app}/ui/AuthenticationException.java | 2 +- .../{ => app}/ui/LoginController.java | 15 +---- .../{ => app}/ui/MemberController.java | 16 +++-- .../java/nextstep/app/utils/Constants.java | 8 +++ .../app/BasicAuthenticationServiceTest.java | 62 +++++++++++++++++++ src/test/java/nextstep/app/LoginTest.java | 12 ++-- src/test/java/nextstep/app/MemberTest.java | 7 ++- 19 files changed, 271 insertions(+), 110 deletions(-) rename src/main/java/nextstep/{ => app}/SecurityAuthenticationApplication.java (93%) delete mode 100644 src/main/java/nextstep/app/config/BasicAuthenticationInterceptor.java rename src/main/java/nextstep/app/{config => configuration}/WebMvcConfiguration.java (53%) create mode 100644 src/main/java/nextstep/app/configuration/interceptor/BasicAuthenticationInterceptor.java create mode 100644 src/main/java/nextstep/app/configuration/interceptor/MemberAuthorizationInterceptor.java rename src/main/java/nextstep/{ => app}/domain/Member.java (54%) rename src/main/java/nextstep/{ => app}/domain/MemberRepository.java (65%) rename src/main/java/nextstep/{ => app}/infrastructure/InmemoryMemberRepository.java (71%) delete mode 100644 src/main/java/nextstep/app/service/BasicAuthenticationService.java create mode 100644 src/main/java/nextstep/app/service/auth/BasicAuthenticationService.java rename src/main/java/nextstep/{ => app}/ui/AuthenticationException.java (72%) rename src/main/java/nextstep/{ => app}/ui/LoginController.java (54%) rename src/main/java/nextstep/{ => app}/ui/MemberController.java (69%) create mode 100644 src/main/java/nextstep/app/utils/Constants.java create mode 100644 src/test/java/nextstep/app/BasicAuthenticationServiceTest.java diff --git a/README.md b/README.md index 205c7d8c..336d92da 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,17 @@ # spring-security-authentication -- change properties to yml (personal preference) +1. LoginController 를 통한 인증 관리 + - /login URL 에 `MemberAuthorizationInterceptor` 를 적용 + - `MemberAuthorizationInterceptor` 는 세션에 이미 저장된 인증값이 있는 경우 바이패스 됨 + - 세션 인증값이 없는 경우 주어진 request parameter 로 repository 에서 `Member` 를 조회 + - 조회 결과가 있는 경우 세션 업데이트, 인증 처리 함 -- refactoring packages by hexagonal architecture, I referred to the following URL -https://remonsinnema.com/tag/hexagonal-architecture/ - - -- Authorization would be implemented like: -As interceptor should check request header to +2. MemberController 를 통한 인가 관리 +- /member URL 에 `BasicAuthenticationInterceptor` 를 적용 +- `BasicAuthenticationInterceptor` 는 토큰을 받아 해당 토큰이 유효한지 여부를 판단 +- 이 때 토큰을 decode 하기 위해 `BasicAuthenticationService` 에서 Base64 기준 토큰 분해 및 `Member` 객체에 담아서 리턴 +- `BasicAuthenticationInterceptor` 는 토큰을 분해해서 얻은 Member 의 email 을 조회하여 인가 여부를 결정 +- 토큰값이 유효하지 않은 경우 `InvalidTokenExcpetion` 발생 +- 그 외 Interceptor 에서 무효 처리 (단순 false 리턴) diff --git a/build.gradle b/build.gradle index bdf350f4..085c66b8 100644 --- a/build.gradle +++ b/build.gradle @@ -12,10 +12,18 @@ repositories { mavenCentral() } +ext { + lombokVersion = '1.18.30' +} + dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.projectlombok:lombok' implementation 'org.apache.commons:commons-lang3' + + compileOnly group: 'org.projectlombok', name: 'lombok', version : lombokVersion + annotationProcessor group: 'org.projectlombok', name: 'lombok', version : lombokVersion + testAnnotationProcessor group: 'org.projectlombok', name: 'lombok', version : lombokVersion + testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/src/main/java/nextstep/SecurityAuthenticationApplication.java b/src/main/java/nextstep/app/SecurityAuthenticationApplication.java similarity index 93% rename from src/main/java/nextstep/SecurityAuthenticationApplication.java rename to src/main/java/nextstep/app/SecurityAuthenticationApplication.java index 1ecd05fe..0f8eb47d 100644 --- a/src/main/java/nextstep/SecurityAuthenticationApplication.java +++ b/src/main/java/nextstep/app/SecurityAuthenticationApplication.java @@ -1,4 +1,4 @@ -package nextstep; +package nextstep.app; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/src/main/java/nextstep/app/config/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/app/config/BasicAuthenticationInterceptor.java deleted file mode 100644 index 7425e3bf..00000000 --- a/src/main/java/nextstep/app/config/BasicAuthenticationInterceptor.java +++ /dev/null @@ -1,33 +0,0 @@ -package nextstep.app.config; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import lombok.RequiredArgsConstructor; -import nextstep.app.service.BasicAuthenticationService; -import nextstep.domain.Member; -import nextstep.domain.MemberRepository; -import org.apache.commons.lang3.StringUtils; -import org.springframework.http.HttpHeaders; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -@Component -@RequiredArgsConstructor -public class BasicAuthenticationInterceptor implements HandlerInterceptor { - - private final MemberRepository memberRepository; - private final BasicAuthenticationService basicAuthenticationService; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); - - if (StringUtils.isNotEmpty(authorizationHeader) && authorizationHeader.startsWith("Basic ")) { - Member decodedMember = basicAuthenticationService.decodeAuthorization(authorizationHeader); - return memberRepository.findByEmail(decodedMember.getEmail()).isEmpty(); - } - return false; - } - -} diff --git a/src/main/java/nextstep/app/config/WebMvcConfiguration.java b/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java similarity index 53% rename from src/main/java/nextstep/app/config/WebMvcConfiguration.java rename to src/main/java/nextstep/app/configuration/WebMvcConfiguration.java index d75d6bf0..fdd0b29b 100644 --- a/src/main/java/nextstep/app/config/WebMvcConfiguration.java +++ b/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java @@ -1,6 +1,8 @@ -package nextstep.app.config; +package nextstep.app.configuration; import lombok.RequiredArgsConstructor; +import nextstep.app.configuration.interceptor.BasicAuthenticationInterceptor; +import nextstep.app.configuration.interceptor.MemberAuthorizationInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -9,10 +11,12 @@ @RequiredArgsConstructor public class WebMvcConfiguration implements WebMvcConfigurer { + private final MemberAuthorizationInterceptor memberAuthorizationInterceptor; private final BasicAuthenticationInterceptor basicAuthenticationInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(basicAuthenticationInterceptor); + registry.addInterceptor(memberAuthorizationInterceptor).addPathPatterns("/login"); + registry.addInterceptor(basicAuthenticationInterceptor).addPathPatterns("/members"); } } diff --git a/src/main/java/nextstep/app/configuration/interceptor/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/app/configuration/interceptor/BasicAuthenticationInterceptor.java new file mode 100644 index 00000000..eac91fa0 --- /dev/null +++ b/src/main/java/nextstep/app/configuration/interceptor/BasicAuthenticationInterceptor.java @@ -0,0 +1,46 @@ +package nextstep.app.configuration.interceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import lombok.RequiredArgsConstructor; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.app.service.auth.BasicAuthenticationService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import static nextstep.app.utils.Constants.BASIC_TOKEN_PREFIX; +import static nextstep.app.utils.Constants.SPRING_SECURITY_CONTEXT_KEY; + +@Component +@RequiredArgsConstructor +public class BasicAuthenticationInterceptor implements HandlerInterceptor { + + private final MemberRepository memberRepository; + private final BasicAuthenticationService basicAuthenticationService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + + if (StringUtils.isEmpty(authorizationHeader) || !authorizationHeader.startsWith(BASIC_TOKEN_PREFIX)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + Member decodedMember = basicAuthenticationService.decodeToken(authorizationHeader); + + if (decodedMember == null) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, decodedMember); + return memberRepository.findByEmail(decodedMember.getEmail()).isPresent(); + } + +} diff --git a/src/main/java/nextstep/app/configuration/interceptor/MemberAuthorizationInterceptor.java b/src/main/java/nextstep/app/configuration/interceptor/MemberAuthorizationInterceptor.java new file mode 100644 index 00000000..1d727426 --- /dev/null +++ b/src/main/java/nextstep/app/configuration/interceptor/MemberAuthorizationInterceptor.java @@ -0,0 +1,48 @@ +package nextstep.app.configuration.interceptor; + +import java.util.Objects; +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import lombok.RequiredArgsConstructor; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import static nextstep.app.utils.Constants.PASSWORD_ATTRIBUTE_NAME; +import static nextstep.app.utils.Constants.SPRING_SECURITY_CONTEXT_KEY; +import static nextstep.app.utils.Constants.USERNAME_ATTRIBUTE_NAME; + +@Component +@RequiredArgsConstructor +public class MemberAuthorizationInterceptor implements HandlerInterceptor { + + private final MemberRepository memberRepository; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + HttpSession session = request.getSession(); + + if (Objects.nonNull(session.getAttribute(SPRING_SECURITY_CONTEXT_KEY))) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return true; + } + Optional member = memberRepository.findByEmailAndPassword( + request.getParameter(USERNAME_ATTRIBUTE_NAME), + request.getParameter(PASSWORD_ATTRIBUTE_NAME)); + + if (member.isEmpty()) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member.get()); + return true; + } + +} diff --git a/src/main/java/nextstep/domain/Member.java b/src/main/java/nextstep/app/domain/Member.java similarity index 54% rename from src/main/java/nextstep/domain/Member.java rename to src/main/java/nextstep/app/domain/Member.java index 0f35ad3b..eec51bc4 100644 --- a/src/main/java/nextstep/domain/Member.java +++ b/src/main/java/nextstep/app/domain/Member.java @@ -1,6 +1,13 @@ -package nextstep.domain; +package nextstep.app.domain; -public class Member { +import java.io.Serializable; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class Member implements Serializable { private final String email; private final String password; private final String name; @@ -13,19 +20,4 @@ public Member(String email, String password, String name, String imageUrl) { this.imageUrl = imageUrl; } - public String getEmail() { - return email; - } - - public String getPassword() { - return password; - } - - public String getName() { - return name; - } - - public String getImageUrl() { - return imageUrl; - } } diff --git a/src/main/java/nextstep/domain/MemberRepository.java b/src/main/java/nextstep/app/domain/MemberRepository.java similarity index 65% rename from src/main/java/nextstep/domain/MemberRepository.java rename to src/main/java/nextstep/app/domain/MemberRepository.java index 19bbaf48..b5f5692b 100644 --- a/src/main/java/nextstep/domain/MemberRepository.java +++ b/src/main/java/nextstep/app/domain/MemberRepository.java @@ -1,4 +1,4 @@ -package nextstep.domain; +package nextstep.app.domain; import java.util.List; import java.util.Optional; @@ -6,6 +6,8 @@ public interface MemberRepository { Optional findByEmail(String email); + Optional findByEmailAndPassword(String email, String password); + List findAll(); void save(Member member); diff --git a/src/main/java/nextstep/infrastructure/InmemoryMemberRepository.java b/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java similarity index 71% rename from src/main/java/nextstep/infrastructure/InmemoryMemberRepository.java rename to src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java index 45f46169..ae549227 100644 --- a/src/main/java/nextstep/infrastructure/InmemoryMemberRepository.java +++ b/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java @@ -1,8 +1,4 @@ -package nextstep.infrastructure; - -import nextstep.domain.Member; -import nextstep.domain.MemberRepository; -import org.springframework.stereotype.Repository; +package nextstep.app.infrastructure; import java.util.HashMap; import java.util.List; @@ -10,6 +6,10 @@ import java.util.Optional; import java.util.stream.Collectors; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import org.springframework.stereotype.Repository; + @Repository public class InmemoryMemberRepository implements MemberRepository { public static final Member TEST_MEMBER_1 = new Member("a@a.com", "password", "a", ""); @@ -26,6 +26,14 @@ public Optional findByEmail(String email) { return Optional.ofNullable(members.get(email)); } + @Override + public Optional findByEmailAndPassword(String email, String password) { + return members.values() + .stream() + .filter(member -> member.getEmail().equals(email) && member.getPassword().equals(password)) + .findFirst(); + } + @Override public List findAll() { return members.values().stream().collect(Collectors.toUnmodifiableList()); diff --git a/src/main/java/nextstep/app/service/BasicAuthenticationService.java b/src/main/java/nextstep/app/service/BasicAuthenticationService.java deleted file mode 100644 index 6875f7e0..00000000 --- a/src/main/java/nextstep/app/service/BasicAuthenticationService.java +++ /dev/null @@ -1,13 +0,0 @@ -package nextstep.app.service; - -import nextstep.domain.Member; -import org.springframework.stereotype.Service; - -@Service -public class BasicAuthenticationService { - - public Member decodeAuthorization(String authKey) { - - } - -} \ No newline at end of file diff --git a/src/main/java/nextstep/app/service/auth/BasicAuthenticationService.java b/src/main/java/nextstep/app/service/auth/BasicAuthenticationService.java new file mode 100644 index 00000000..76f8b744 --- /dev/null +++ b/src/main/java/nextstep/app/service/auth/BasicAuthenticationService.java @@ -0,0 +1,32 @@ +package nextstep.app.service.auth; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; + +import nextstep.app.domain.Member; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import static nextstep.app.utils.Constants.BASIC_TOKEN_PREFIX; + +@Service +public class BasicAuthenticationService { + + public Member decodeToken(String token) { + if (Objects.isNull(token)) { + return null; + } + + String[] decodedToken = + new String(Base64.getDecoder().decode(token.replace(BASIC_TOKEN_PREFIX, "")), StandardCharsets.UTF_8) + .split(":"); + + if (decodedToken.length < 2 || StringUtils.isBlank(decodedToken[0]) || StringUtils.isBlank(decodedToken[1])) { + return null; + } + + return Member.builder().email(decodedToken[0]).password(decodedToken[1]).build(); + } + +} diff --git a/src/main/java/nextstep/ui/AuthenticationException.java b/src/main/java/nextstep/app/ui/AuthenticationException.java similarity index 72% rename from src/main/java/nextstep/ui/AuthenticationException.java rename to src/main/java/nextstep/app/ui/AuthenticationException.java index 145a0934..f809b6e4 100644 --- a/src/main/java/nextstep/ui/AuthenticationException.java +++ b/src/main/java/nextstep/app/ui/AuthenticationException.java @@ -1,4 +1,4 @@ -package nextstep.ui; +package nextstep.app.ui; public class AuthenticationException extends RuntimeException { } diff --git a/src/main/java/nextstep/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java similarity index 54% rename from src/main/java/nextstep/ui/LoginController.java rename to src/main/java/nextstep/app/ui/LoginController.java index f54e27bc..2022dbf1 100644 --- a/src/main/java/nextstep/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,27 +1,16 @@ -package nextstep.ui; +package nextstep.app.ui; -import nextstep.domain.MemberRepository; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - @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) { + public ResponseEntity login() { return ResponseEntity.ok().build(); } diff --git a/src/main/java/nextstep/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java similarity index 69% rename from src/main/java/nextstep/ui/MemberController.java rename to src/main/java/nextstep/app/ui/MemberController.java index 9cb553d6..33ccc214 100644 --- a/src/main/java/nextstep/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -1,22 +1,20 @@ -package nextstep.ui; +package nextstep.app.ui; -import nextstep.domain.Member; -import nextstep.domain.MemberRepository; +import java.util.List; + +import lombok.RequiredArgsConstructor; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; -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/app/utils/Constants.java b/src/main/java/nextstep/app/utils/Constants.java new file mode 100644 index 00000000..c1731b91 --- /dev/null +++ b/src/main/java/nextstep/app/utils/Constants.java @@ -0,0 +1,8 @@ +package nextstep.app.utils; + +public class Constants { + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + public static final String BASIC_TOKEN_PREFIX = "Basic "; + public static final String USERNAME_ATTRIBUTE_NAME = "username"; + public static final String PASSWORD_ATTRIBUTE_NAME = "password"; +} diff --git a/src/test/java/nextstep/app/BasicAuthenticationServiceTest.java b/src/test/java/nextstep/app/BasicAuthenticationServiceTest.java new file mode 100644 index 00000000..72e98063 --- /dev/null +++ b/src/test/java/nextstep/app/BasicAuthenticationServiceTest.java @@ -0,0 +1,62 @@ +package nextstep.app; + +import java.util.Base64; +import java.util.stream.Stream; + +import nextstep.app.domain.Member; +import nextstep.app.service.auth.BasicAuthenticationService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class BasicAuthenticationServiceTest { + + @InjectMocks + BasicAuthenticationService underTest; + + static Stream tokenProvider() { + return Stream.of( + Arguments.of("Basic invalid"), + null, + Arguments.of(""), + Arguments.of("Basic ") + ); + } + + @ParameterizedTest + @MethodSource("tokenProvider") + void decodeToken_shouldReturnNullWhenTokenIsCorrupted(String token) { + // arrange + // act + var result = underTest.decodeToken(token); + + // assert + assertThat(result).isNull(); + } + + @Test + void decodeToken_shouldReturnMemberWith() { + // arrange + var email = "test_email"; + var password = "test_password"; + var token = "Basic " + Base64.getEncoder().encodeToString((email+":"+password).getBytes()); + + // act + var result = underTest.decodeToken(token); + + // assert + assertThat(result).isNotNull().isInstanceOf(Member.class); + assertThat(result.getEmail()).isEqualTo(email); + assertThat(result.getPassword()).isEqualTo(password); + assertThat(result.getName()).isNull(); + assertThat(result.getImageUrl()).isNull(); + } + +} diff --git a/src/test/java/nextstep/app/LoginTest.java b/src/test/java/nextstep/app/LoginTest.java index 37a3c56a..27b4d581 100644 --- a/src/test/java/nextstep/app/LoginTest.java +++ b/src/test/java/nextstep/app/LoginTest.java @@ -1,7 +1,10 @@ package nextstep.app; -import nextstep.domain.Member; -import nextstep.infrastructure.InmemoryMemberRepository; +import javax.servlet.http.HttpSession; + +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.app.infrastructure.InmemoryMemberRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -11,8 +14,6 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; -import javax.servlet.http.HttpSession; - 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.MockMvcResultMatchers.status; @@ -25,6 +26,9 @@ class LoginTest { @Autowired private MockMvc mockMvc; + @Autowired + private MemberRepository memberRepository = new InmemoryMemberRepository(); + @DisplayName("로그인 성공") @Test void login_success() throws Exception { diff --git a/src/test/java/nextstep/app/MemberTest.java b/src/test/java/nextstep/app/MemberTest.java index 67dd42af..211963f5 100644 --- a/src/test/java/nextstep/app/MemberTest.java +++ b/src/test/java/nextstep/app/MemberTest.java @@ -1,8 +1,8 @@ package nextstep.app; -import nextstep.domain.Member; -import nextstep.domain.MemberRepository; -import nextstep.infrastructure.InmemoryMemberRepository; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.app.infrastructure.InmemoryMemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -23,6 +23,7 @@ @SpringBootTest @AutoConfigureMockMvc class MemberTest { + private static final Member TEST_MEMBER = InmemoryMemberRepository.TEST_MEMBER_1; @Autowired From 0a593373bbeaf585a4b6c3401c9e47ca7b73c930 Mon Sep 17 00:00:00 2001 From: "daehee.kim" Date: Mon, 4 Nov 2024 10:35:26 +0900 Subject: [PATCH 03/11] fix MemberAuthrizationINterceptor --- .../interceptor/MemberAuthorizationInterceptor.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/nextstep/app/configuration/interceptor/MemberAuthorizationInterceptor.java b/src/main/java/nextstep/app/configuration/interceptor/MemberAuthorizationInterceptor.java index 1d727426..3aa28328 100644 --- a/src/main/java/nextstep/app/configuration/interceptor/MemberAuthorizationInterceptor.java +++ b/src/main/java/nextstep/app/configuration/interceptor/MemberAuthorizationInterceptor.java @@ -29,9 +29,10 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons HttpSession session = request.getSession(); if (Objects.nonNull(session.getAttribute(SPRING_SECURITY_CONTEXT_KEY))) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return true; + Member member = (Member) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY); + return memberRepository.findByEmailAndPassword(member.getEmail(), member.getPassword()).isPresent(); } + Optional member = memberRepository.findByEmailAndPassword( request.getParameter(USERNAME_ATTRIBUTE_NAME), request.getParameter(PASSWORD_ATTRIBUTE_NAME)); From abadf8bcea5ea54d3932e15486517a55583e880d Mon Sep 17 00:00:00 2001 From: "daehee.kim" Date: Mon, 4 Nov 2024 10:53:32 +0900 Subject: [PATCH 04/11] renmae method, class toe get more implicity, remove unused method --- .../nextstep/app/configuration/WebMvcConfiguration.java | 6 +++--- .../interceptor/BasicAuthenticationInterceptor.java | 2 +- ...orizationInterceptor.java => FormLoginInterceptor.java} | 2 +- .../app/service/auth/BasicAuthenticationService.java | 2 +- src/main/java/nextstep/app/ui/LoginController.java | 7 +------ .../java/nextstep/app/BasicAuthenticationServiceTest.java | 4 ++-- 6 files changed, 9 insertions(+), 14 deletions(-) rename src/main/java/nextstep/app/configuration/interceptor/{MemberAuthorizationInterceptor.java => FormLoginInterceptor.java} (95%) diff --git a/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java b/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java index fdd0b29b..8678631a 100644 --- a/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java +++ b/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java @@ -2,7 +2,7 @@ import lombok.RequiredArgsConstructor; import nextstep.app.configuration.interceptor.BasicAuthenticationInterceptor; -import nextstep.app.configuration.interceptor.MemberAuthorizationInterceptor; +import nextstep.app.configuration.interceptor.FormLoginInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -11,12 +11,12 @@ @RequiredArgsConstructor public class WebMvcConfiguration implements WebMvcConfigurer { - private final MemberAuthorizationInterceptor memberAuthorizationInterceptor; + private final FormLoginInterceptor formLoginInterceptor; private final BasicAuthenticationInterceptor basicAuthenticationInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(memberAuthorizationInterceptor).addPathPatterns("/login"); + registry.addInterceptor(formLoginInterceptor).addPathPatterns("/login"); registry.addInterceptor(basicAuthenticationInterceptor).addPathPatterns("/members"); } } diff --git a/src/main/java/nextstep/app/configuration/interceptor/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/app/configuration/interceptor/BasicAuthenticationInterceptor.java index eac91fa0..3bca6b3c 100644 --- a/src/main/java/nextstep/app/configuration/interceptor/BasicAuthenticationInterceptor.java +++ b/src/main/java/nextstep/app/configuration/interceptor/BasicAuthenticationInterceptor.java @@ -32,7 +32,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons return false; } - Member decodedMember = basicAuthenticationService.decodeToken(authorizationHeader); + Member decodedMember = basicAuthenticationService.mapTokenToMember(authorizationHeader); if (decodedMember == null) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); diff --git a/src/main/java/nextstep/app/configuration/interceptor/MemberAuthorizationInterceptor.java b/src/main/java/nextstep/app/configuration/interceptor/FormLoginInterceptor.java similarity index 95% rename from src/main/java/nextstep/app/configuration/interceptor/MemberAuthorizationInterceptor.java rename to src/main/java/nextstep/app/configuration/interceptor/FormLoginInterceptor.java index 3aa28328..86055d15 100644 --- a/src/main/java/nextstep/app/configuration/interceptor/MemberAuthorizationInterceptor.java +++ b/src/main/java/nextstep/app/configuration/interceptor/FormLoginInterceptor.java @@ -19,7 +19,7 @@ @Component @RequiredArgsConstructor -public class MemberAuthorizationInterceptor implements HandlerInterceptor { +public class FormLoginInterceptor implements HandlerInterceptor { private final MemberRepository memberRepository; diff --git a/src/main/java/nextstep/app/service/auth/BasicAuthenticationService.java b/src/main/java/nextstep/app/service/auth/BasicAuthenticationService.java index 76f8b744..95afcb81 100644 --- a/src/main/java/nextstep/app/service/auth/BasicAuthenticationService.java +++ b/src/main/java/nextstep/app/service/auth/BasicAuthenticationService.java @@ -13,7 +13,7 @@ @Service public class BasicAuthenticationService { - public Member decodeToken(String token) { + public Member mapTokenToMember(String token) { if (Objects.isNull(token)) { return null; } diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 2022dbf1..4d5cf5e2 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,8 +1,6 @@ package nextstep.app.ui; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; @@ -14,8 +12,5 @@ public ResponseEntity login() { return ResponseEntity.ok().build(); } - @ExceptionHandler(AuthenticationException.class) - public ResponseEntity handleAuthenticationException() { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } } + diff --git a/src/test/java/nextstep/app/BasicAuthenticationServiceTest.java b/src/test/java/nextstep/app/BasicAuthenticationServiceTest.java index 72e98063..317ab3b0 100644 --- a/src/test/java/nextstep/app/BasicAuthenticationServiceTest.java +++ b/src/test/java/nextstep/app/BasicAuthenticationServiceTest.java @@ -35,7 +35,7 @@ static Stream tokenProvider() { void decodeToken_shouldReturnNullWhenTokenIsCorrupted(String token) { // arrange // act - var result = underTest.decodeToken(token); + var result = underTest.mapTokenToMember(token); // assert assertThat(result).isNull(); @@ -49,7 +49,7 @@ void decodeToken_shouldReturnMemberWith() { var token = "Basic " + Base64.getEncoder().encodeToString((email+":"+password).getBytes()); // act - var result = underTest.decodeToken(token); + var result = underTest.mapTokenToMember(token); // assert assertThat(result).isNotNull().isInstanceOf(Member.class); From fc074802f1c740b493ea20126e6ff43f862f2cee Mon Sep 17 00:00:00 2001 From: "daehee.kim" Date: Mon, 4 Nov 2024 12:41:06 +0900 Subject: [PATCH 05/11] separate packages (test not done) --- .../configuration/WebMvcConfiguration.java | 19 ++++++++++++-- .../BasicAuthenticationInterceptor.java | 22 ++++++++-------- .../interceptor/FormLoginInterceptor.java | 25 ++++++++++--------- .../nextstep/security/model/UserDetails.java | 11 ++++++++ .../service}/BasicAuthenticationService.java | 10 ++++---- .../security/service/UserDetailsService.java | 17 +++++++++++++ .../{app => security}/utils/Constants.java | 2 +- .../app/BasicAuthenticationServiceTest.java | 2 +- 8 files changed, 76 insertions(+), 32 deletions(-) rename src/main/java/nextstep/{app => security}/configuration/interceptor/BasicAuthenticationInterceptor.java (62%) rename src/main/java/nextstep/{app => security}/configuration/interceptor/FormLoginInterceptor.java (52%) create mode 100644 src/main/java/nextstep/security/model/UserDetails.java rename src/main/java/nextstep/{app/service/auth => security/service}/BasicAuthenticationService.java (68%) create mode 100644 src/main/java/nextstep/security/service/UserDetailsService.java rename src/main/java/nextstep/{app => security}/utils/Constants.java (90%) diff --git a/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java b/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java index 8678631a..15bd26ad 100644 --- a/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java +++ b/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java @@ -1,8 +1,14 @@ package nextstep.app.configuration; import lombok.RequiredArgsConstructor; -import nextstep.app.configuration.interceptor.BasicAuthenticationInterceptor; -import nextstep.app.configuration.interceptor.FormLoginInterceptor; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.app.ui.AuthenticationException; +import nextstep.security.configuration.interceptor.BasicAuthenticationInterceptor; +import nextstep.security.configuration.interceptor.FormLoginInterceptor; +import nextstep.security.model.UserDetails; +import nextstep.security.service.UserDetailsService; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -11,6 +17,7 @@ @RequiredArgsConstructor public class WebMvcConfiguration implements WebMvcConfigurer { + private final MemberRepository memberRepository; private final FormLoginInterceptor formLoginInterceptor; private final BasicAuthenticationInterceptor basicAuthenticationInterceptor; @@ -19,4 +26,12 @@ public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(formLoginInterceptor).addPathPatterns("/login"); registry.addInterceptor(basicAuthenticationInterceptor).addPathPatterns("/members"); } + + @Bean + public UserDetailsService userDetailsService() { + return username -> { + Member member = memberRepository.findByEmail(username).orElseThrow(AuthenticationException::new); + return UserDetails.builder().userName(member.getEmail()).password(member.getPassword()).build(); + }; + } } diff --git a/src/main/java/nextstep/app/configuration/interceptor/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/configuration/interceptor/BasicAuthenticationInterceptor.java similarity index 62% rename from src/main/java/nextstep/app/configuration/interceptor/BasicAuthenticationInterceptor.java rename to src/main/java/nextstep/security/configuration/interceptor/BasicAuthenticationInterceptor.java index 3bca6b3c..aa9c3503 100644 --- a/src/main/java/nextstep/app/configuration/interceptor/BasicAuthenticationInterceptor.java +++ b/src/main/java/nextstep/security/configuration/interceptor/BasicAuthenticationInterceptor.java @@ -1,26 +1,26 @@ -package nextstep.app.configuration.interceptor; +package nextstep.security.configuration.interceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; -import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; -import nextstep.app.service.auth.BasicAuthenticationService; +import nextstep.security.model.UserDetails; +import nextstep.security.service.BasicAuthenticationService; +import nextstep.security.service.UserDetailsService; import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; -import static nextstep.app.utils.Constants.BASIC_TOKEN_PREFIX; -import static nextstep.app.utils.Constants.SPRING_SECURITY_CONTEXT_KEY; +import static nextstep.security.utils.Constants.BASIC_TOKEN_PREFIX; +import static nextstep.security.utils.Constants.SPRING_SECURITY_CONTEXT_KEY; @Component @RequiredArgsConstructor public class BasicAuthenticationInterceptor implements HandlerInterceptor { - private final MemberRepository memberRepository; private final BasicAuthenticationService basicAuthenticationService; + private final UserDetailsService userDetailsService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) @@ -32,15 +32,15 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons return false; } - Member decodedMember = basicAuthenticationService.mapTokenToMember(authorizationHeader); + UserDetails decodedUserDetails = basicAuthenticationService.mapTokenToUserDetails(authorizationHeader); - if (decodedMember == null) { + if (decodedUserDetails == null) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } - request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, decodedMember); - return memberRepository.findByEmail(decodedMember.getEmail()).isPresent(); + request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, decodedUserDetails); + return userDetailsService.loadUserByUsername(decodedUserDetails.getUserName()).isPresent(); } } diff --git a/src/main/java/nextstep/app/configuration/interceptor/FormLoginInterceptor.java b/src/main/java/nextstep/security/configuration/interceptor/FormLoginInterceptor.java similarity index 52% rename from src/main/java/nextstep/app/configuration/interceptor/FormLoginInterceptor.java rename to src/main/java/nextstep/security/configuration/interceptor/FormLoginInterceptor.java index 86055d15..2161cfcb 100644 --- a/src/main/java/nextstep/app/configuration/interceptor/FormLoginInterceptor.java +++ b/src/main/java/nextstep/security/configuration/interceptor/FormLoginInterceptor.java @@ -1,4 +1,4 @@ -package nextstep.app.configuration.interceptor; +package nextstep.security.configuration.interceptor; import java.util.Objects; import java.util.Optional; @@ -8,20 +8,20 @@ import javax.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; -import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; +import nextstep.security.model.UserDetails; +import nextstep.security.service.UserDetailsService; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; -import static nextstep.app.utils.Constants.PASSWORD_ATTRIBUTE_NAME; -import static nextstep.app.utils.Constants.SPRING_SECURITY_CONTEXT_KEY; -import static nextstep.app.utils.Constants.USERNAME_ATTRIBUTE_NAME; +import static nextstep.security.utils.Constants.PASSWORD_ATTRIBUTE_NAME; +import static nextstep.security.utils.Constants.SPRING_SECURITY_CONTEXT_KEY; +import static nextstep.security.utils.Constants.USERNAME_ATTRIBUTE_NAME; @Component @RequiredArgsConstructor public class FormLoginInterceptor implements HandlerInterceptor { - private final MemberRepository memberRepository; + private final UserDetailsService userDetailsService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) @@ -29,20 +29,21 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons HttpSession session = request.getSession(); if (Objects.nonNull(session.getAttribute(SPRING_SECURITY_CONTEXT_KEY))) { - Member member = (Member) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY); - return memberRepository.findByEmailAndPassword(member.getEmail(), member.getPassword()).isPresent(); + UserDetails userDetails = (UserDetails) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY); + return userDetailsService.loadUserByUsernameAndEmail(userDetails.getUserName(), userDetails.getPassword()) + .isPresent(); } - Optional member = memberRepository.findByEmailAndPassword( + Optional userDetails = userDetailsService.loadUserByUsernameAndEmail( request.getParameter(USERNAME_ATTRIBUTE_NAME), request.getParameter(PASSWORD_ATTRIBUTE_NAME)); - if (member.isEmpty()) { + if (userDetails.isEmpty()) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } - session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member.get()); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetails.get()); return true; } diff --git a/src/main/java/nextstep/security/model/UserDetails.java b/src/main/java/nextstep/security/model/UserDetails.java new file mode 100644 index 00000000..f835f3d5 --- /dev/null +++ b/src/main/java/nextstep/security/model/UserDetails.java @@ -0,0 +1,11 @@ +package nextstep.security.model; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class UserDetails { + String userName; + String password; +} diff --git a/src/main/java/nextstep/app/service/auth/BasicAuthenticationService.java b/src/main/java/nextstep/security/service/BasicAuthenticationService.java similarity index 68% rename from src/main/java/nextstep/app/service/auth/BasicAuthenticationService.java rename to src/main/java/nextstep/security/service/BasicAuthenticationService.java index 95afcb81..a408a57a 100644 --- a/src/main/java/nextstep/app/service/auth/BasicAuthenticationService.java +++ b/src/main/java/nextstep/security/service/BasicAuthenticationService.java @@ -1,19 +1,19 @@ -package nextstep.app.service.auth; +package nextstep.security.service; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Objects; -import nextstep.app.domain.Member; +import nextstep.security.model.UserDetails; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; -import static nextstep.app.utils.Constants.BASIC_TOKEN_PREFIX; +import static nextstep.security.utils.Constants.BASIC_TOKEN_PREFIX; @Service public class BasicAuthenticationService { - public Member mapTokenToMember(String token) { + public UserDetails mapTokenToUserDetails(String token) { if (Objects.isNull(token)) { return null; } @@ -26,7 +26,7 @@ public Member mapTokenToMember(String token) { return null; } - return Member.builder().email(decodedToken[0]).password(decodedToken[1]).build(); + return UserDetails.builder().userName(decodedToken[0]).password(decodedToken[1]).build(); } } diff --git a/src/main/java/nextstep/security/service/UserDetailsService.java b/src/main/java/nextstep/security/service/UserDetailsService.java new file mode 100644 index 00000000..a0e4f12b --- /dev/null +++ b/src/main/java/nextstep/security/service/UserDetailsService.java @@ -0,0 +1,17 @@ +package nextstep.security.service; + +import java.util.Optional; + +import nextstep.app.domain.MemberRepository; +import nextstep.security.model.UserDetails; +import org.springframework.stereotype.Service; + +@Service +public interface UserDetailsService { + + MemberRepository memberRepository = null; + + Optional loadUserByUsername(String username); + Optional loadUserByUsernameAndEmail(String username, String email); + +} diff --git a/src/main/java/nextstep/app/utils/Constants.java b/src/main/java/nextstep/security/utils/Constants.java similarity index 90% rename from src/main/java/nextstep/app/utils/Constants.java rename to src/main/java/nextstep/security/utils/Constants.java index c1731b91..13063f51 100644 --- a/src/main/java/nextstep/app/utils/Constants.java +++ b/src/main/java/nextstep/security/utils/Constants.java @@ -1,4 +1,4 @@ -package nextstep.app.utils; +package nextstep.security.utils; public class Constants { public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; diff --git a/src/test/java/nextstep/app/BasicAuthenticationServiceTest.java b/src/test/java/nextstep/app/BasicAuthenticationServiceTest.java index 317ab3b0..796492f3 100644 --- a/src/test/java/nextstep/app/BasicAuthenticationServiceTest.java +++ b/src/test/java/nextstep/app/BasicAuthenticationServiceTest.java @@ -4,7 +4,7 @@ import java.util.stream.Stream; import nextstep.app.domain.Member; -import nextstep.app.service.auth.BasicAuthenticationService; +import nextstep.security.service.BasicAuthenticationService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; From 1f43c71dbe4a11d6931fb2f574e6d547d5503e77 Mon Sep 17 00:00:00 2001 From: "daehee.kim" Date: Mon, 4 Nov 2024 12:57:45 +0900 Subject: [PATCH 06/11] separated packages with app and security (build success) --- .../configuration/WebMvcConfiguration.java | 37 +++++++++++++++---- .../java/nextstep/app/ui/LoginController.java | 7 ++++ .../nextstep/app/ui/MemberController.java | 7 ++++ .../BasicAuthenticationInterceptor.java | 4 +- .../security/service/UserDetailsService.java | 2 - .../app/BasicAuthenticationServiceTest.java | 16 ++++---- 6 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java b/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java index 15bd26ad..2107afa1 100644 --- a/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java +++ b/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java @@ -1,5 +1,7 @@ package nextstep.app.configuration; +import java.util.Optional; + import lombok.RequiredArgsConstructor; import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; @@ -18,20 +20,41 @@ public class WebMvcConfiguration implements WebMvcConfigurer { private final MemberRepository memberRepository; - private final FormLoginInterceptor formLoginInterceptor; - private final BasicAuthenticationInterceptor basicAuthenticationInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(formLoginInterceptor).addPathPatterns("/login"); - registry.addInterceptor(basicAuthenticationInterceptor).addPathPatterns("/members"); + registry.addInterceptor(formLoginInterceptor()).addPathPatterns("/login"); + registry.addInterceptor(basicAuthenticationInterceptor()).addPathPatterns("/members"); } @Bean public UserDetailsService userDetailsService() { - return username -> { - Member member = memberRepository.findByEmail(username).orElseThrow(AuthenticationException::new); - return UserDetails.builder().userName(member.getEmail()).password(member.getPassword()).build(); + return new UserDetailsService() { + @Override + public Optional loadUserByUsername(String username) { + Member member = memberRepository.findByEmail(username).orElseThrow(AuthenticationException::new); + return Optional.ofNullable( + UserDetails.builder().userName(member.getEmail()).password(member.getPassword()).build()); + } + + @Override + public Optional loadUserByUsernameAndEmail(String username, String password) { + Member member = + memberRepository.findByEmailAndPassword(username, password) + .orElseThrow(AuthenticationException::new); + return Optional.ofNullable( + UserDetails.builder().userName(member.getEmail()).password(member.getPassword()).build()); + } }; } + + @Bean + public FormLoginInterceptor formLoginInterceptor() { + return new FormLoginInterceptor(userDetailsService()); + } + + @Bean + public BasicAuthenticationInterceptor basicAuthenticationInterceptor() { + return new BasicAuthenticationInterceptor(userDetailsService()); + } } diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 4d5cf5e2..dc08b437 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,6 +1,8 @@ package nextstep.app.ui; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; @@ -12,5 +14,10 @@ public ResponseEntity login() { return ResponseEntity.ok().build(); } + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException() { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + } diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index 33ccc214..7a6833d0 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -5,7 +5,9 @@ import lombok.RequiredArgsConstructor; import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -21,4 +23,9 @@ public ResponseEntity> list() { return ResponseEntity.ok(members); } + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException() { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + } diff --git a/src/main/java/nextstep/security/configuration/interceptor/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/configuration/interceptor/BasicAuthenticationInterceptor.java index aa9c3503..f8f8087c 100644 --- a/src/main/java/nextstep/security/configuration/interceptor/BasicAuthenticationInterceptor.java +++ b/src/main/java/nextstep/security/configuration/interceptor/BasicAuthenticationInterceptor.java @@ -9,17 +9,15 @@ import nextstep.security.service.UserDetailsService; import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpHeaders; -import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import static nextstep.security.utils.Constants.BASIC_TOKEN_PREFIX; import static nextstep.security.utils.Constants.SPRING_SECURITY_CONTEXT_KEY; -@Component @RequiredArgsConstructor public class BasicAuthenticationInterceptor implements HandlerInterceptor { - private final BasicAuthenticationService basicAuthenticationService; + private final BasicAuthenticationService basicAuthenticationService = new BasicAuthenticationService(); private final UserDetailsService userDetailsService; @Override diff --git a/src/main/java/nextstep/security/service/UserDetailsService.java b/src/main/java/nextstep/security/service/UserDetailsService.java index a0e4f12b..1164e7fb 100644 --- a/src/main/java/nextstep/security/service/UserDetailsService.java +++ b/src/main/java/nextstep/security/service/UserDetailsService.java @@ -2,14 +2,12 @@ import java.util.Optional; -import nextstep.app.domain.MemberRepository; import nextstep.security.model.UserDetails; import org.springframework.stereotype.Service; @Service public interface UserDetailsService { - MemberRepository memberRepository = null; Optional loadUserByUsername(String username); Optional loadUserByUsernameAndEmail(String username, String email); diff --git a/src/test/java/nextstep/app/BasicAuthenticationServiceTest.java b/src/test/java/nextstep/app/BasicAuthenticationServiceTest.java index 796492f3..de173a36 100644 --- a/src/test/java/nextstep/app/BasicAuthenticationServiceTest.java +++ b/src/test/java/nextstep/app/BasicAuthenticationServiceTest.java @@ -3,7 +3,7 @@ import java.util.Base64; import java.util.stream.Stream; -import nextstep.app.domain.Member; +import nextstep.security.model.UserDetails; import nextstep.security.service.BasicAuthenticationService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -32,31 +32,29 @@ static Stream tokenProvider() { @ParameterizedTest @MethodSource("tokenProvider") - void decodeToken_shouldReturnNullWhenTokenIsCorrupted(String token) { + void mapTokenToUserDetails_shouldReturnNullWhenTokenIsCorrupted(String token) { // arrange // act - var result = underTest.mapTokenToMember(token); + var result = underTest.mapTokenToUserDetails(token); // assert assertThat(result).isNull(); } @Test - void decodeToken_shouldReturnMemberWith() { + void mapTokenToUserDetails_shouldReturnUserDetailsWithMatchedResult() { // arrange var email = "test_email"; var password = "test_password"; var token = "Basic " + Base64.getEncoder().encodeToString((email+":"+password).getBytes()); // act - var result = underTest.mapTokenToMember(token); + var result = underTest.mapTokenToUserDetails(token); // assert - assertThat(result).isNotNull().isInstanceOf(Member.class); - assertThat(result.getEmail()).isEqualTo(email); + assertThat(result).isNotNull().isInstanceOf(UserDetails.class); + assertThat(result.getUserName()).isEqualTo(email); assertThat(result.getPassword()).isEqualTo(password); - assertThat(result.getName()).isNull(); - assertThat(result.getImageUrl()).isNull(); } } From 687fb385ecc91ee56d91f6cbc461b964ed23fa7f Mon Sep 17 00:00:00 2001 From: "daehee.kim" Date: Mon, 4 Nov 2024 15:35:56 +0900 Subject: [PATCH 07/11] step3 - requirement no.1 done (build success) --- ...ration.java => SecurityConfiguration.java} | 51 ++++++++----- .../DefaultSecurityFilterChain.java | 24 +++++++ .../configuration/DelegateFilterProxy.java | 24 +++++++ .../configuration/FilterChainProxy.java | 70 ++++++++++++++++++ .../configuration/SecurityFilterChain.java | 14 ++++ .../filter/BasicAuthenticationFilter.java | 72 +++++++++++++++++++ .../configuration/filter/FormLoginFilter.java | 66 +++++++++++++++++ .../BasicAuthenticationInterceptor.java | 44 ------------ .../interceptor/FormLoginInterceptor.java | 50 ------------- .../nextstep/security/model/UserDetails.java | 4 +- .../nextstep/security/utils/Constants.java | 2 + 11 files changed, 309 insertions(+), 112 deletions(-) rename src/main/java/nextstep/app/configuration/{WebMvcConfiguration.java => SecurityConfiguration.java} (56%) create mode 100644 src/main/java/nextstep/security/configuration/DefaultSecurityFilterChain.java create mode 100644 src/main/java/nextstep/security/configuration/DelegateFilterProxy.java create mode 100644 src/main/java/nextstep/security/configuration/FilterChainProxy.java create mode 100644 src/main/java/nextstep/security/configuration/SecurityFilterChain.java create mode 100644 src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java create mode 100644 src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java delete mode 100644 src/main/java/nextstep/security/configuration/interceptor/BasicAuthenticationInterceptor.java delete mode 100644 src/main/java/nextstep/security/configuration/interceptor/FormLoginInterceptor.java diff --git a/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java b/src/main/java/nextstep/app/configuration/SecurityConfiguration.java similarity index 56% rename from src/main/java/nextstep/app/configuration/WebMvcConfiguration.java rename to src/main/java/nextstep/app/configuration/SecurityConfiguration.java index 2107afa1..b3ac72ae 100644 --- a/src/main/java/nextstep/app/configuration/WebMvcConfiguration.java +++ b/src/main/java/nextstep/app/configuration/SecurityConfiguration.java @@ -1,30 +1,55 @@ package nextstep.app.configuration; +import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import nextstep.app.ui.AuthenticationException; -import nextstep.security.configuration.interceptor.BasicAuthenticationInterceptor; -import nextstep.security.configuration.interceptor.FormLoginInterceptor; +import nextstep.security.configuration.DefaultSecurityFilterChain; +import nextstep.security.configuration.DelegateFilterProxy; +import nextstep.security.configuration.FilterChainProxy; +import nextstep.security.configuration.SecurityFilterChain; +import nextstep.security.configuration.filter.BasicAuthenticationFilter; +import nextstep.security.configuration.filter.FormLoginFilter; import nextstep.security.model.UserDetails; import nextstep.security.service.UserDetailsService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @RequiredArgsConstructor -public class WebMvcConfiguration implements WebMvcConfigurer { +public class SecurityConfiguration { private final MemberRepository memberRepository; - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(formLoginInterceptor()).addPathPatterns("/login"); - registry.addInterceptor(basicAuthenticationInterceptor()).addPathPatterns("/members"); + @Bean + public DelegateFilterProxy delegateFilterProxy() { + return new DelegateFilterProxy(filterChainProxy(List.of(securityFilterChain()))); + } + + @Bean + public FilterChainProxy filterChainProxy(List securityFilterChains) { + return new FilterChainProxy(securityFilterChains); + } + + @Bean + public SecurityFilterChain securityFilterChain() { + return new DefaultSecurityFilterChain(List.of( + formLoginFilter(), + basicAuthenticationFilter() + )); + } + + @Bean + public FormLoginFilter formLoginFilter() { + return new FormLoginFilter(userDetailsService()); + } + + @Bean + public BasicAuthenticationFilter basicAuthenticationFilter() { + return new BasicAuthenticationFilter(userDetailsService()); } @Bean @@ -48,13 +73,5 @@ public Optional loadUserByUsernameAndEmail(String username, String }; } - @Bean - public FormLoginInterceptor formLoginInterceptor() { - return new FormLoginInterceptor(userDetailsService()); - } - @Bean - public BasicAuthenticationInterceptor basicAuthenticationInterceptor() { - return new BasicAuthenticationInterceptor(userDetailsService()); - } } diff --git a/src/main/java/nextstep/security/configuration/DefaultSecurityFilterChain.java b/src/main/java/nextstep/security/configuration/DefaultSecurityFilterChain.java new file mode 100644 index 00000000..997f1823 --- /dev/null +++ b/src/main/java/nextstep/security/configuration/DefaultSecurityFilterChain.java @@ -0,0 +1,24 @@ +package nextstep.security.configuration; + +import java.util.List; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class DefaultSecurityFilterChain implements SecurityFilterChain{ + + private final List filters; + + @Override + public boolean matches(HttpServletRequest request) { + return true; + } + + @Override + public List getFilters() { + return filters; + } +} diff --git a/src/main/java/nextstep/security/configuration/DelegateFilterProxy.java b/src/main/java/nextstep/security/configuration/DelegateFilterProxy.java new file mode 100644 index 00000000..5b801f99 --- /dev/null +++ b/src/main/java/nextstep/security/configuration/DelegateFilterProxy.java @@ -0,0 +1,24 @@ +package nextstep.security.configuration; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.filter.GenericFilterBean; + +@RequiredArgsConstructor +public class DelegateFilterProxy extends GenericFilterBean { + + private final Filter delegateFilter; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + delegateFilter.doFilter(servletRequest, servletResponse, filterChain); + } +} diff --git a/src/main/java/nextstep/security/configuration/FilterChainProxy.java b/src/main/java/nextstep/security/configuration/FilterChainProxy.java new file mode 100644 index 00000000..326d73a8 --- /dev/null +++ b/src/main/java/nextstep/security/configuration/FilterChainProxy.java @@ -0,0 +1,70 @@ +package nextstep.security.configuration; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.filter.GenericFilterBean; + +@RequiredArgsConstructor +public class FilterChainProxy extends GenericFilterBean { + + private final List securityFilterChains; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + List filters = getFilters((HttpServletRequest) servletRequest); + + VirtualFilterChain virtualFilterChain = new VirtualFilterChain(filterChain, filters); + virtualFilterChain.doFilter(servletRequest, servletResponse); + } + + private List getFilters(HttpServletRequest httpServletRequest) { + for (SecurityFilterChain securityFilterChain : securityFilterChains) { + if (securityFilterChain.matches(httpServletRequest)) { + return securityFilterChain.getFilters(); + } + } + return Collections.emptyList(); + } + + private static final class VirtualFilterChain implements FilterChain { + + private final FilterChain originalChain; + + private final List inputFilters; + + private final int inputFilterSize; + + private int currentPosition = 0; + + private VirtualFilterChain(FilterChain originalChain, List inputFilters) { + this.originalChain = originalChain; + this.inputFilters = inputFilters; + inputFilterSize = inputFilters.size(); + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + + if (currentPosition == inputFilterSize) { + originalChain.doFilter(servletRequest, servletResponse); + return; + } + + currentPosition++; + Filter nextFilter = inputFilters.get(currentPosition - 1); + nextFilter.doFilter(servletRequest, servletResponse, this); + } + } +} diff --git a/src/main/java/nextstep/security/configuration/SecurityFilterChain.java b/src/main/java/nextstep/security/configuration/SecurityFilterChain.java new file mode 100644 index 00000000..3781d035 --- /dev/null +++ b/src/main/java/nextstep/security/configuration/SecurityFilterChain.java @@ -0,0 +1,14 @@ +package nextstep.security.configuration; + +import java.util.List; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; + +public interface SecurityFilterChain { + + boolean matches(HttpServletRequest request); + + List getFilters(); + +} diff --git a/src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java new file mode 100644 index 00000000..0c400fcd --- /dev/null +++ b/src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java @@ -0,0 +1,72 @@ +package nextstep.security.configuration.filter; + +import java.io.IOException; +import java.util.Objects; +import java.util.Optional; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import lombok.RequiredArgsConstructor; +import nextstep.app.ui.AuthenticationException; +import nextstep.security.model.UserDetails; +import nextstep.security.service.BasicAuthenticationService; +import nextstep.security.service.UserDetailsService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.web.filter.GenericFilterBean; + +import static nextstep.security.utils.Constants.BASIC_TOKEN_PREFIX; +import static nextstep.security.utils.Constants.GET_MEMBERS_ENDPOINT_ADDRESS; +import static nextstep.security.utils.Constants.SPRING_SECURITY_CONTEXT_KEY; + +@RequiredArgsConstructor +public class BasicAuthenticationFilter extends GenericFilterBean { + + private final BasicAuthenticationService basicAuthenticationService = new BasicAuthenticationService(); + private final UserDetailsService userDetailsService; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + + HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + + if (!GET_MEMBERS_ENDPOINT_ADDRESS.equals(httpRequest.getRequestURI())) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + String authorizationHeader = httpRequest.getHeader(HttpHeaders.AUTHORIZATION); + + try { + if (StringUtils.isEmpty(authorizationHeader) || !authorizationHeader.startsWith(BASIC_TOKEN_PREFIX)) { + throw new AuthenticationException(); + } + + UserDetails decodedUserDetails = basicAuthenticationService.mapTokenToUserDetails(authorizationHeader); + + if (Objects.isNull(decodedUserDetails)) { + throw new AuthenticationException(); + } + + Optional userDetails = userDetailsService.loadUserByUsernameAndEmail( + decodedUserDetails.getUserName(), + decodedUserDetails.getPassword()); + + if (userDetails.isEmpty()) { + throw new AuthenticationException(); + } + + httpRequest.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetails.get()); + filterChain.doFilter(servletRequest, servletResponse); + } catch (Exception e) { + ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + } +} diff --git a/src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java b/src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java new file mode 100644 index 00000000..5f0faf68 --- /dev/null +++ b/src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java @@ -0,0 +1,66 @@ +package nextstep.security.configuration.filter; + +import java.io.IOException; +import java.util.Optional; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import lombok.RequiredArgsConstructor; +import nextstep.app.ui.AuthenticationException; +import nextstep.security.model.UserDetails; +import nextstep.security.service.UserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.GenericFilterBean; + +import static nextstep.security.utils.Constants.LOGIN_ENDPOINT_ADDRESS; +import static nextstep.security.utils.Constants.PASSWORD_ATTRIBUTE_NAME; +import static nextstep.security.utils.Constants.SPRING_SECURITY_CONTEXT_KEY; +import static nextstep.security.utils.Constants.USERNAME_ATTRIBUTE_NAME; + +@Component +@RequiredArgsConstructor +public class FormLoginFilter extends GenericFilterBean { + + private final UserDetailsService userDetailsService; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + + if (!LOGIN_ENDPOINT_ADDRESS.equals(httpRequest.getRequestURI())) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + try { + + // 여기서 context holder 확인해서 빠꾸시킬것 + // if (Objects.nonNull(session.getAttribute(SPRING_SECURITY_CONTEXT_KEY))) { + // UserDetails userDetails = (UserDetails) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY); + // return userDetailsService.loadUserByUsernameAndEmail(userDetails.getUserName(), userDetails.getPassword()) + // .isPresent(); + // } + + Optional userDetails = userDetailsService.loadUserByUsernameAndEmail( + httpRequest.getParameter(USERNAME_ATTRIBUTE_NAME), + httpRequest.getParameter(PASSWORD_ATTRIBUTE_NAME)); + + if (userDetails.isEmpty()) { + throw new AuthenticationException(); + } + + HttpSession session = httpRequest.getSession(); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetails.get()); + } catch (Exception e) { + ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + } +} diff --git a/src/main/java/nextstep/security/configuration/interceptor/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/configuration/interceptor/BasicAuthenticationInterceptor.java deleted file mode 100644 index f8f8087c..00000000 --- a/src/main/java/nextstep/security/configuration/interceptor/BasicAuthenticationInterceptor.java +++ /dev/null @@ -1,44 +0,0 @@ -package nextstep.security.configuration.interceptor; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import lombok.RequiredArgsConstructor; -import nextstep.security.model.UserDetails; -import nextstep.security.service.BasicAuthenticationService; -import nextstep.security.service.UserDetailsService; -import org.apache.commons.lang3.StringUtils; -import org.springframework.http.HttpHeaders; -import org.springframework.web.servlet.HandlerInterceptor; - -import static nextstep.security.utils.Constants.BASIC_TOKEN_PREFIX; -import static nextstep.security.utils.Constants.SPRING_SECURITY_CONTEXT_KEY; - -@RequiredArgsConstructor -public class BasicAuthenticationInterceptor implements HandlerInterceptor { - - private final BasicAuthenticationService basicAuthenticationService = new BasicAuthenticationService(); - private final UserDetailsService userDetailsService; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); - - if (StringUtils.isEmpty(authorizationHeader) || !authorizationHeader.startsWith(BASIC_TOKEN_PREFIX)) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return false; - } - - UserDetails decodedUserDetails = basicAuthenticationService.mapTokenToUserDetails(authorizationHeader); - - if (decodedUserDetails == null) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return false; - } - - request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, decodedUserDetails); - return userDetailsService.loadUserByUsername(decodedUserDetails.getUserName()).isPresent(); - } - -} diff --git a/src/main/java/nextstep/security/configuration/interceptor/FormLoginInterceptor.java b/src/main/java/nextstep/security/configuration/interceptor/FormLoginInterceptor.java deleted file mode 100644 index 2161cfcb..00000000 --- a/src/main/java/nextstep/security/configuration/interceptor/FormLoginInterceptor.java +++ /dev/null @@ -1,50 +0,0 @@ -package nextstep.security.configuration.interceptor; - -import java.util.Objects; -import java.util.Optional; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -import lombok.RequiredArgsConstructor; -import nextstep.security.model.UserDetails; -import nextstep.security.service.UserDetailsService; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -import static nextstep.security.utils.Constants.PASSWORD_ATTRIBUTE_NAME; -import static nextstep.security.utils.Constants.SPRING_SECURITY_CONTEXT_KEY; -import static nextstep.security.utils.Constants.USERNAME_ATTRIBUTE_NAME; - -@Component -@RequiredArgsConstructor -public class FormLoginInterceptor implements HandlerInterceptor { - - private final UserDetailsService userDetailsService; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - HttpSession session = request.getSession(); - - if (Objects.nonNull(session.getAttribute(SPRING_SECURITY_CONTEXT_KEY))) { - UserDetails userDetails = (UserDetails) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY); - return userDetailsService.loadUserByUsernameAndEmail(userDetails.getUserName(), userDetails.getPassword()) - .isPresent(); - } - - Optional userDetails = userDetailsService.loadUserByUsernameAndEmail( - request.getParameter(USERNAME_ATTRIBUTE_NAME), - request.getParameter(PASSWORD_ATTRIBUTE_NAME)); - - if (userDetails.isEmpty()) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return false; - } - - session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetails.get()); - return true; - } - -} diff --git a/src/main/java/nextstep/security/model/UserDetails.java b/src/main/java/nextstep/security/model/UserDetails.java index f835f3d5..0652f859 100644 --- a/src/main/java/nextstep/security/model/UserDetails.java +++ b/src/main/java/nextstep/security/model/UserDetails.java @@ -1,11 +1,13 @@ package nextstep.security.model; +import java.io.Serializable; + import lombok.Builder; import lombok.Getter; @Builder @Getter -public class UserDetails { +public class UserDetails implements Serializable { String userName; String password; } diff --git a/src/main/java/nextstep/security/utils/Constants.java b/src/main/java/nextstep/security/utils/Constants.java index 13063f51..342abf78 100644 --- a/src/main/java/nextstep/security/utils/Constants.java +++ b/src/main/java/nextstep/security/utils/Constants.java @@ -5,4 +5,6 @@ public class Constants { public static final String BASIC_TOKEN_PREFIX = "Basic "; public static final String USERNAME_ATTRIBUTE_NAME = "username"; public static final String PASSWORD_ATTRIBUTE_NAME = "password"; + public static final String LOGIN_ENDPOINT_ADDRESS = "/login"; + public static final String GET_MEMBERS_ENDPOINT_ADDRESS = "/members"; } From 173661944597157940274a08adefe9a9547ce61b Mon Sep 17 00:00:00 2001 From: "daehee.kim" Date: Mon, 4 Nov 2024 16:16:05 +0900 Subject: [PATCH 08/11] step3-2 done, build success --- .../authentication/Authentication.java | 11 ++++ .../authentication/AuthenticationManager.java | 7 +++ .../AuthenticationProvider.java | 11 ++++ .../DaoAuthenticationProvider.java | 28 +++++++++ .../authentication/ProviderManager.java | 21 +++++++ .../UsernamePasswordAuthenticationToken.java | 34 +++++++++++ .../filter/BasicAuthenticationFilter.java | 57 ++++++++++++------- .../configuration/filter/FormLoginFilter.java | 41 +++++++++---- 8 files changed, 177 insertions(+), 33 deletions(-) create mode 100644 src/main/java/nextstep/security/configuration/authentication/Authentication.java create mode 100644 src/main/java/nextstep/security/configuration/authentication/AuthenticationManager.java create mode 100644 src/main/java/nextstep/security/configuration/authentication/AuthenticationProvider.java create mode 100644 src/main/java/nextstep/security/configuration/authentication/DaoAuthenticationProvider.java create mode 100644 src/main/java/nextstep/security/configuration/authentication/ProviderManager.java create mode 100644 src/main/java/nextstep/security/configuration/authentication/UsernamePasswordAuthenticationToken.java diff --git a/src/main/java/nextstep/security/configuration/authentication/Authentication.java b/src/main/java/nextstep/security/configuration/authentication/Authentication.java new file mode 100644 index 00000000..edd93585 --- /dev/null +++ b/src/main/java/nextstep/security/configuration/authentication/Authentication.java @@ -0,0 +1,11 @@ +package nextstep.security.configuration.authentication; + +public interface Authentication { + + Object getCredentials(); + + Object getPrincipal(); + + boolean isAuthenticated(); + +} diff --git a/src/main/java/nextstep/security/configuration/authentication/AuthenticationManager.java b/src/main/java/nextstep/security/configuration/authentication/AuthenticationManager.java new file mode 100644 index 00000000..054021d3 --- /dev/null +++ b/src/main/java/nextstep/security/configuration/authentication/AuthenticationManager.java @@ -0,0 +1,7 @@ +package nextstep.security.configuration.authentication; + +public interface AuthenticationManager { + + Authentication authenticate(Authentication authentication); + +} diff --git a/src/main/java/nextstep/security/configuration/authentication/AuthenticationProvider.java b/src/main/java/nextstep/security/configuration/authentication/AuthenticationProvider.java new file mode 100644 index 00000000..aea3bb66 --- /dev/null +++ b/src/main/java/nextstep/security/configuration/authentication/AuthenticationProvider.java @@ -0,0 +1,11 @@ +package nextstep.security.configuration.authentication; + +import nextstep.app.ui.AuthenticationException; + +public interface AuthenticationProvider { + + Authentication authenticate(Authentication authentication) throws AuthenticationException; + + boolean supports(Class authentication); + +} diff --git a/src/main/java/nextstep/security/configuration/authentication/DaoAuthenticationProvider.java b/src/main/java/nextstep/security/configuration/authentication/DaoAuthenticationProvider.java new file mode 100644 index 00000000..e856359f --- /dev/null +++ b/src/main/java/nextstep/security/configuration/authentication/DaoAuthenticationProvider.java @@ -0,0 +1,28 @@ +package nextstep.security.configuration.authentication; + +import lombok.RequiredArgsConstructor; +import nextstep.app.ui.AuthenticationException; +import nextstep.security.model.UserDetails; +import nextstep.security.service.UserDetailsService; + +@RequiredArgsConstructor +public class DaoAuthenticationProvider implements AuthenticationProvider{ + + private final UserDetailsService userDetailsService; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + UserDetails userDetails = + userDetailsService.loadUserByUsernameAndEmail( + authentication.getPrincipal().toString(), + authentication.getCredentials().toString()) + .orElseThrow(AuthenticationException::new); + + return UsernamePasswordAuthenticationToken.authenticated(userDetails.getUserName(), userDetails.getPassword()); + } + + @Override + public boolean supports(Class authentication) { + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/nextstep/security/configuration/authentication/ProviderManager.java b/src/main/java/nextstep/security/configuration/authentication/ProviderManager.java new file mode 100644 index 00000000..ba32916b --- /dev/null +++ b/src/main/java/nextstep/security/configuration/authentication/ProviderManager.java @@ -0,0 +1,21 @@ +package nextstep.security.configuration.authentication; + +import java.util.List; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class ProviderManager implements AuthenticationManager{ + + private final List authenticationProviders; + + @Override + public Authentication authenticate(Authentication authentication) { + for (AuthenticationProvider provider : authenticationProviders) { + if (provider.supports(authentication.getClass())) { + return provider.authenticate(authentication); + } + } + return null; + } +} diff --git a/src/main/java/nextstep/security/configuration/authentication/UsernamePasswordAuthenticationToken.java b/src/main/java/nextstep/security/configuration/authentication/UsernamePasswordAuthenticationToken.java new file mode 100644 index 00000000..4709b762 --- /dev/null +++ b/src/main/java/nextstep/security/configuration/authentication/UsernamePasswordAuthenticationToken.java @@ -0,0 +1,34 @@ +package nextstep.security.configuration.authentication; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class UsernamePasswordAuthenticationToken implements Authentication{ + + private final Object credentials; + private final Object principal; + private final boolean isAuthenticated; + + public static UsernamePasswordAuthenticationToken unauthenticated(String principal, String credentials) { + return new UsernamePasswordAuthenticationToken(credentials, principal, false); + } + + public static UsernamePasswordAuthenticationToken authenticated(String principal, String credentials) { + return new UsernamePasswordAuthenticationToken(credentials, principal, true); + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public boolean isAuthenticated() { + return isAuthenticated; + } +} diff --git a/src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java index 0c400fcd..e835b73a 100644 --- a/src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java +++ b/src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java @@ -1,8 +1,8 @@ package nextstep.security.configuration.filter; import java.io.IOException; +import java.util.List; import java.util.Objects; -import java.util.Optional; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -11,8 +11,12 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; import nextstep.app.ui.AuthenticationException; +import nextstep.security.configuration.authentication.Authentication; +import nextstep.security.configuration.authentication.AuthenticationManager; +import nextstep.security.configuration.authentication.DaoAuthenticationProvider; +import nextstep.security.configuration.authentication.ProviderManager; +import nextstep.security.configuration.authentication.UsernamePasswordAuthenticationToken; import nextstep.security.model.UserDetails; import nextstep.security.service.BasicAuthenticationService; import nextstep.security.service.UserDetailsService; @@ -24,11 +28,14 @@ import static nextstep.security.utils.Constants.GET_MEMBERS_ENDPOINT_ADDRESS; import static nextstep.security.utils.Constants.SPRING_SECURITY_CONTEXT_KEY; -@RequiredArgsConstructor public class BasicAuthenticationFilter extends GenericFilterBean { private final BasicAuthenticationService basicAuthenticationService = new BasicAuthenticationService(); - private final UserDetailsService userDetailsService; + private final AuthenticationManager authenticationManager; + + public BasicAuthenticationFilter(UserDetailsService userDetailsService) { + authenticationManager = new ProviderManager(List.of(new DaoAuthenticationProvider(userDetailsService))); + } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) @@ -41,32 +48,40 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo return; } - String authorizationHeader = httpRequest.getHeader(HttpHeaders.AUTHORIZATION); - try { - if (StringUtils.isEmpty(authorizationHeader) || !authorizationHeader.startsWith(BASIC_TOKEN_PREFIX)) { - throw new AuthenticationException(); - } - UserDetails decodedUserDetails = basicAuthenticationService.mapTokenToUserDetails(authorizationHeader); - - if (Objects.isNull(decodedUserDetails)) { - throw new AuthenticationException(); - } - - Optional userDetails = userDetailsService.loadUserByUsernameAndEmail( - decodedUserDetails.getUserName(), - decodedUserDetails.getPassword()); + try { + Authentication authentication = convert(httpRequest); - if (userDetails.isEmpty()) { - throw new AuthenticationException(); + if (Objects.isNull(authentication)) { + filterChain.doFilter(servletRequest, servletResponse); + return; } - httpRequest.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetails.get()); + httpRequest.getSession() + .setAttribute(SPRING_SECURITY_CONTEXT_KEY, authenticationManager.authenticate(authentication)); filterChain.doFilter(servletRequest, servletResponse); } catch (Exception e) { ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED); } } + + private Authentication convert(HttpServletRequest httpRequest) { + String authorizationHeader = httpRequest.getHeader(HttpHeaders.AUTHORIZATION); + + if (StringUtils.isEmpty(authorizationHeader) || !authorizationHeader.startsWith(BASIC_TOKEN_PREFIX)) { + throw new AuthenticationException(); + } + + UserDetails decodedUserDetails = basicAuthenticationService.mapTokenToUserDetails(authorizationHeader); + + if (Objects.isNull(decodedUserDetails)) { + throw new AuthenticationException(); + } + + return UsernamePasswordAuthenticationToken.unauthenticated( + decodedUserDetails.getUserName(), + decodedUserDetails.getPassword()); + } } diff --git a/src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java b/src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java index 5f0faf68..eddf4886 100644 --- a/src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java +++ b/src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java @@ -1,7 +1,8 @@ package nextstep.security.configuration.filter; import java.io.IOException; -import java.util.Optional; +import java.util.List; +import java.util.Objects; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -11,9 +12,11 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import lombok.RequiredArgsConstructor; -import nextstep.app.ui.AuthenticationException; -import nextstep.security.model.UserDetails; +import nextstep.security.configuration.authentication.Authentication; +import nextstep.security.configuration.authentication.AuthenticationManager; +import nextstep.security.configuration.authentication.DaoAuthenticationProvider; +import nextstep.security.configuration.authentication.ProviderManager; +import nextstep.security.configuration.authentication.UsernamePasswordAuthenticationToken; import nextstep.security.service.UserDetailsService; import org.springframework.stereotype.Component; import org.springframework.web.filter.GenericFilterBean; @@ -24,10 +27,15 @@ import static nextstep.security.utils.Constants.USERNAME_ATTRIBUTE_NAME; @Component -@RequiredArgsConstructor public class FormLoginFilter extends GenericFilterBean { - private final UserDetailsService userDetailsService; + private final AuthenticationManager authenticationManager; + + public FormLoginFilter(UserDetailsService userDetailsService) { + authenticationManager = new ProviderManager( + List.of(new DaoAuthenticationProvider(userDetailsService)) + ); + } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) @@ -48,19 +56,28 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo // .isPresent(); // } - Optional userDetails = userDetailsService.loadUserByUsernameAndEmail( - httpRequest.getParameter(USERNAME_ATTRIBUTE_NAME), - httpRequest.getParameter(PASSWORD_ATTRIBUTE_NAME)); + Authentication authentication = convert(httpRequest); - if (userDetails.isEmpty()) { - throw new AuthenticationException(); + if (Objects.isNull(authentication)) { + filterChain.doFilter(servletRequest, servletResponse); + return; } HttpSession session = httpRequest.getSession(); - session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetails.get()); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, authenticationManager.authenticate(authentication)); } catch (Exception e) { ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED); } } + + private Authentication convert(HttpServletRequest httpRequest) { + try { + return UsernamePasswordAuthenticationToken.unauthenticated( + httpRequest.getParameter(USERNAME_ATTRIBUTE_NAME), + httpRequest.getParameter(PASSWORD_ATTRIBUTE_NAME)); + } catch (Exception e) { + return null; + } + } } From af393f597fd55872bb6a21fa38319a0ce7b4ea95 Mon Sep 17 00:00:00 2001 From: "daehee.kim" Date: Mon, 4 Nov 2024 16:36:53 +0900 Subject: [PATCH 09/11] make small refactoring on filters --- .../authentication/Authentication.java | 4 +++- .../UsernamePasswordAuthenticationToken.java | 2 +- .../filter/BasicAuthenticationFilter.java | 13 +++---------- .../configuration/filter/FormLoginFilter.java | 10 ++++++---- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/main/java/nextstep/security/configuration/authentication/Authentication.java b/src/main/java/nextstep/security/configuration/authentication/Authentication.java index edd93585..e4aaa2f6 100644 --- a/src/main/java/nextstep/security/configuration/authentication/Authentication.java +++ b/src/main/java/nextstep/security/configuration/authentication/Authentication.java @@ -1,6 +1,8 @@ package nextstep.security.configuration.authentication; -public interface Authentication { +import java.io.Serializable; + +public interface Authentication extends Serializable { Object getCredentials(); diff --git a/src/main/java/nextstep/security/configuration/authentication/UsernamePasswordAuthenticationToken.java b/src/main/java/nextstep/security/configuration/authentication/UsernamePasswordAuthenticationToken.java index 4709b762..87a52948 100644 --- a/src/main/java/nextstep/security/configuration/authentication/UsernamePasswordAuthenticationToken.java +++ b/src/main/java/nextstep/security/configuration/authentication/UsernamePasswordAuthenticationToken.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; @RequiredArgsConstructor -public class UsernamePasswordAuthenticationToken implements Authentication{ +public class UsernamePasswordAuthenticationToken implements Authentication { private final Object credentials; private final Object principal; diff --git a/src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java index e835b73a..73d96685 100644 --- a/src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java +++ b/src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java @@ -48,18 +48,11 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo return; } - - try { - Authentication authentication = convert(httpRequest); - - if (Objects.isNull(authentication)) { - filterChain.doFilter(servletRequest, servletResponse); - return; - } - + Authentication authenticationRequest = convert(httpRequest); httpRequest.getSession() - .setAttribute(SPRING_SECURITY_CONTEXT_KEY, authenticationManager.authenticate(authentication)); + .setAttribute(SPRING_SECURITY_CONTEXT_KEY, + authenticationManager.authenticate(authenticationRequest)); filterChain.doFilter(servletRequest, servletResponse); } catch (Exception e) { ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED); diff --git a/src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java b/src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java index eddf4886..ee95ded4 100644 --- a/src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java +++ b/src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java @@ -49,22 +49,24 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo try { - // 여기서 context holder 확인해서 빠꾸시킬것 + //TODO: 여기서 context holder 확인해서 빠꾸시킬것 // if (Objects.nonNull(session.getAttribute(SPRING_SECURITY_CONTEXT_KEY))) { // UserDetails userDetails = (UserDetails) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY); // return userDetailsService.loadUserByUsernameAndEmail(userDetails.getUserName(), userDetails.getPassword()) // .isPresent(); // } - Authentication authentication = convert(httpRequest); + Authentication authenticationRequest = convert(httpRequest); - if (Objects.isNull(authentication)) { + if (Objects.isNull(authenticationRequest)) { filterChain.doFilter(servletRequest, servletResponse); return; } HttpSession session = httpRequest.getSession(); - session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, authenticationManager.authenticate(authentication)); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, + authenticationManager.authenticate(authenticationRequest)); + filterChain.doFilter(servletRequest, servletResponse); } catch (Exception e) { ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED); } From 8a3d385739e8f6abd9b16d4ae22c124d515f3680 Mon Sep 17 00:00:00 2001 From: "daehee.kim" Date: Tue, 5 Nov 2024 10:14:35 +0900 Subject: [PATCH 10/11] add context holder --- .../configuration/SecurityConfiguration.java | 24 ++++-------- .../authentication/AuthenticationManager.java | 7 ---- .../filter/BasicAuthenticationFilter.java | 25 +++++++----- .../configuration/filter/FormLoginFilter.java | 36 ++++++++--------- .../filter/SecurityContextHolderFilter.java | 30 ++++++++++++++ .../authentication/Authentication.java | 2 +- .../UsernamePasswordAuthenticationToken.java | 2 +- .../model/context/SecurityContext.java | 21 ++++++++++ .../HttpSessionSecurityContextRepository.java | 31 +++++++++++++++ .../authentication/AuthenticationManager.java | 9 +++++ .../AuthenticationProvider.java | 3 +- .../DaoAuthenticationProvider.java | 4 +- .../authentication/ProviderManager.java | 3 +- .../context/SecurityContextHolder.java | 39 +++++++++++++++++++ .../filter}/DefaultSecurityFilterChain.java | 2 +- .../filter}/DelegateFilterProxy.java | 2 +- .../filter}/FilterChainProxy.java | 2 +- .../filter}/SecurityFilterChain.java | 2 +- src/test/java/nextstep/app/LoginTest.java | 23 +++++++++++ 19 files changed, 206 insertions(+), 61 deletions(-) delete mode 100644 src/main/java/nextstep/security/configuration/authentication/AuthenticationManager.java create mode 100644 src/main/java/nextstep/security/configuration/filter/SecurityContextHolderFilter.java rename src/main/java/nextstep/security/{configuration => model}/authentication/Authentication.java (76%) rename src/main/java/nextstep/security/{configuration => model}/authentication/UsernamePasswordAuthenticationToken.java (94%) create mode 100644 src/main/java/nextstep/security/model/context/SecurityContext.java create mode 100644 src/main/java/nextstep/security/repository/HttpSessionSecurityContextRepository.java create mode 100644 src/main/java/nextstep/security/service/authentication/AuthenticationManager.java rename src/main/java/nextstep/security/{configuration => service}/authentication/AuthenticationProvider.java (68%) rename src/main/java/nextstep/security/{configuration => service}/authentication/DaoAuthenticationProvider.java (84%) rename src/main/java/nextstep/security/{configuration => service}/authentication/ProviderManager.java (83%) create mode 100644 src/main/java/nextstep/security/service/context/SecurityContextHolder.java rename src/main/java/nextstep/security/{configuration => service/filter}/DefaultSecurityFilterChain.java (91%) rename src/main/java/nextstep/security/{configuration => service/filter}/DelegateFilterProxy.java (94%) rename src/main/java/nextstep/security/{configuration => service/filter}/FilterChainProxy.java (98%) rename src/main/java/nextstep/security/{configuration => service/filter}/SecurityFilterChain.java (84%) diff --git a/src/main/java/nextstep/app/configuration/SecurityConfiguration.java b/src/main/java/nextstep/app/configuration/SecurityConfiguration.java index b3ac72ae..c1201b47 100644 --- a/src/main/java/nextstep/app/configuration/SecurityConfiguration.java +++ b/src/main/java/nextstep/app/configuration/SecurityConfiguration.java @@ -7,14 +7,15 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import nextstep.app.ui.AuthenticationException; -import nextstep.security.configuration.DefaultSecurityFilterChain; -import nextstep.security.configuration.DelegateFilterProxy; -import nextstep.security.configuration.FilterChainProxy; -import nextstep.security.configuration.SecurityFilterChain; import nextstep.security.configuration.filter.BasicAuthenticationFilter; import nextstep.security.configuration.filter.FormLoginFilter; +import nextstep.security.configuration.filter.SecurityContextHolderFilter; import nextstep.security.model.UserDetails; import nextstep.security.service.UserDetailsService; +import nextstep.security.service.filter.DefaultSecurityFilterChain; +import nextstep.security.service.filter.DelegateFilterProxy; +import nextstep.security.service.filter.FilterChainProxy; +import nextstep.security.service.filter.SecurityFilterChain; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -37,21 +38,12 @@ public FilterChainProxy filterChainProxy(List securityFilte @Bean public SecurityFilterChain securityFilterChain() { return new DefaultSecurityFilterChain(List.of( - formLoginFilter(), - basicAuthenticationFilter() + new SecurityContextHolderFilter(), + new FormLoginFilter(userDetailsService()), + new BasicAuthenticationFilter(userDetailsService()) )); } - @Bean - public FormLoginFilter formLoginFilter() { - return new FormLoginFilter(userDetailsService()); - } - - @Bean - public BasicAuthenticationFilter basicAuthenticationFilter() { - return new BasicAuthenticationFilter(userDetailsService()); - } - @Bean public UserDetailsService userDetailsService() { return new UserDetailsService() { diff --git a/src/main/java/nextstep/security/configuration/authentication/AuthenticationManager.java b/src/main/java/nextstep/security/configuration/authentication/AuthenticationManager.java deleted file mode 100644 index 054021d3..00000000 --- a/src/main/java/nextstep/security/configuration/authentication/AuthenticationManager.java +++ /dev/null @@ -1,7 +0,0 @@ -package nextstep.security.configuration.authentication; - -public interface AuthenticationManager { - - Authentication authenticate(Authentication authentication); - -} diff --git a/src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java index 73d96685..eb5bbbcb 100644 --- a/src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java +++ b/src/main/java/nextstep/security/configuration/filter/BasicAuthenticationFilter.java @@ -12,21 +12,22 @@ import javax.servlet.http.HttpServletResponse; import nextstep.app.ui.AuthenticationException; -import nextstep.security.configuration.authentication.Authentication; -import nextstep.security.configuration.authentication.AuthenticationManager; -import nextstep.security.configuration.authentication.DaoAuthenticationProvider; -import nextstep.security.configuration.authentication.ProviderManager; -import nextstep.security.configuration.authentication.UsernamePasswordAuthenticationToken; import nextstep.security.model.UserDetails; +import nextstep.security.model.authentication.Authentication; +import nextstep.security.model.authentication.UsernamePasswordAuthenticationToken; +import nextstep.security.model.context.SecurityContext; import nextstep.security.service.BasicAuthenticationService; import nextstep.security.service.UserDetailsService; +import nextstep.security.service.authentication.AuthenticationManager; +import nextstep.security.service.authentication.DaoAuthenticationProvider; +import nextstep.security.service.authentication.ProviderManager; +import nextstep.security.service.context.SecurityContextHolder; import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpHeaders; import org.springframework.web.filter.GenericFilterBean; import static nextstep.security.utils.Constants.BASIC_TOKEN_PREFIX; import static nextstep.security.utils.Constants.GET_MEMBERS_ENDPOINT_ADDRESS; -import static nextstep.security.utils.Constants.SPRING_SECURITY_CONTEXT_KEY; public class BasicAuthenticationFilter extends GenericFilterBean { @@ -46,13 +47,19 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo if (!GET_MEMBERS_ENDPOINT_ADDRESS.equals(httpRequest.getRequestURI())) { filterChain.doFilter(servletRequest, servletResponse); return; + } else if (SecurityContextHolder.getContext().getAuthentication() != null) { + filterChain.doFilter(servletRequest, servletResponse); + return; } try { Authentication authenticationRequest = convert(httpRequest); - httpRequest.getSession() - .setAttribute(SPRING_SECURITY_CONTEXT_KEY, - authenticationManager.authenticate(authenticationRequest)); + Authentication authenticated = authenticationManager.authenticate(authenticationRequest); + + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authenticated); + SecurityContextHolder.setContext(context); + filterChain.doFilter(servletRequest, servletResponse); } catch (Exception e) { ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED); diff --git a/src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java b/src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java index ee95ded4..8c70e1a6 100644 --- a/src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java +++ b/src/main/java/nextstep/security/configuration/filter/FormLoginFilter.java @@ -10,26 +10,28 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import nextstep.security.configuration.authentication.Authentication; -import nextstep.security.configuration.authentication.AuthenticationManager; -import nextstep.security.configuration.authentication.DaoAuthenticationProvider; -import nextstep.security.configuration.authentication.ProviderManager; -import nextstep.security.configuration.authentication.UsernamePasswordAuthenticationToken; +import nextstep.security.model.authentication.Authentication; +import nextstep.security.model.authentication.UsernamePasswordAuthenticationToken; +import nextstep.security.model.context.SecurityContext; +import nextstep.security.repository.HttpSessionSecurityContextRepository; import nextstep.security.service.UserDetailsService; +import nextstep.security.service.authentication.AuthenticationManager; +import nextstep.security.service.authentication.DaoAuthenticationProvider; +import nextstep.security.service.authentication.ProviderManager; +import nextstep.security.service.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.GenericFilterBean; import static nextstep.security.utils.Constants.LOGIN_ENDPOINT_ADDRESS; import static nextstep.security.utils.Constants.PASSWORD_ATTRIBUTE_NAME; -import static nextstep.security.utils.Constants.SPRING_SECURITY_CONTEXT_KEY; import static nextstep.security.utils.Constants.USERNAME_ATTRIBUTE_NAME; @Component public class FormLoginFilter extends GenericFilterBean { private final AuthenticationManager authenticationManager; + private final HttpSessionSecurityContextRepository httpSessionSecurityContextRepository = new HttpSessionSecurityContextRepository(); public FormLoginFilter(UserDetailsService userDetailsService) { authenticationManager = new ProviderManager( @@ -48,25 +50,19 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo } try { - - //TODO: 여기서 context holder 확인해서 빠꾸시킬것 - // if (Objects.nonNull(session.getAttribute(SPRING_SECURITY_CONTEXT_KEY))) { - // UserDetails userDetails = (UserDetails) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY); - // return userDetailsService.loadUserByUsernameAndEmail(userDetails.getUserName(), userDetails.getPassword()) - // .isPresent(); - // } - Authentication authenticationRequest = convert(httpRequest); if (Objects.isNull(authenticationRequest)) { - filterChain.doFilter(servletRequest, servletResponse); + ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } - HttpSession session = httpRequest.getSession(); - session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, - authenticationManager.authenticate(authenticationRequest)); - filterChain.doFilter(servletRequest, servletResponse); + Authentication authenticated = authenticationManager.authenticate(authenticationRequest); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authenticated); + SecurityContextHolder.setContext(context); + httpSessionSecurityContextRepository.saveContext(context, httpRequest, + (HttpServletResponse) servletResponse); } catch (Exception e) { ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED); } diff --git a/src/main/java/nextstep/security/configuration/filter/SecurityContextHolderFilter.java b/src/main/java/nextstep/security/configuration/filter/SecurityContextHolderFilter.java new file mode 100644 index 00000000..32303380 --- /dev/null +++ b/src/main/java/nextstep/security/configuration/filter/SecurityContextHolderFilter.java @@ -0,0 +1,30 @@ +package nextstep.security.configuration.filter; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import nextstep.security.model.context.SecurityContext; +import nextstep.security.repository.HttpSessionSecurityContextRepository; +import nextstep.security.service.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +public class SecurityContextHolderFilter extends GenericFilterBean { + + private final HttpSessionSecurityContextRepository httpSessionSecurityContextRepository = new HttpSessionSecurityContextRepository(); + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + SecurityContext context = httpSessionSecurityContextRepository.loadContext( + (HttpServletRequest) servletRequest); + + SecurityContextHolder.setContext(context); + filterChain.doFilter(servletRequest, servletResponse); + SecurityContextHolder.clearContext(); + } +} diff --git a/src/main/java/nextstep/security/configuration/authentication/Authentication.java b/src/main/java/nextstep/security/model/authentication/Authentication.java similarity index 76% rename from src/main/java/nextstep/security/configuration/authentication/Authentication.java rename to src/main/java/nextstep/security/model/authentication/Authentication.java index e4aaa2f6..efcf9ad0 100644 --- a/src/main/java/nextstep/security/configuration/authentication/Authentication.java +++ b/src/main/java/nextstep/security/model/authentication/Authentication.java @@ -1,4 +1,4 @@ -package nextstep.security.configuration.authentication; +package nextstep.security.model.authentication; import java.io.Serializable; diff --git a/src/main/java/nextstep/security/configuration/authentication/UsernamePasswordAuthenticationToken.java b/src/main/java/nextstep/security/model/authentication/UsernamePasswordAuthenticationToken.java similarity index 94% rename from src/main/java/nextstep/security/configuration/authentication/UsernamePasswordAuthenticationToken.java rename to src/main/java/nextstep/security/model/authentication/UsernamePasswordAuthenticationToken.java index 87a52948..ad149ad9 100644 --- a/src/main/java/nextstep/security/configuration/authentication/UsernamePasswordAuthenticationToken.java +++ b/src/main/java/nextstep/security/model/authentication/UsernamePasswordAuthenticationToken.java @@ -1,4 +1,4 @@ -package nextstep.security.configuration.authentication; +package nextstep.security.model.authentication; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/nextstep/security/model/context/SecurityContext.java b/src/main/java/nextstep/security/model/context/SecurityContext.java new file mode 100644 index 00000000..4b0e3302 --- /dev/null +++ b/src/main/java/nextstep/security/model/context/SecurityContext.java @@ -0,0 +1,21 @@ +package nextstep.security.model.context; + +import java.io.Serializable; + +import lombok.Getter; +import lombok.Setter; +import nextstep.security.model.authentication.Authentication; + +@Getter +@Setter +public class SecurityContext implements Serializable { + + private Authentication authentication; + + public SecurityContext() {} + + public SecurityContext(Authentication authentication) { + this.authentication = authentication; + } + +} diff --git a/src/main/java/nextstep/security/repository/HttpSessionSecurityContextRepository.java b/src/main/java/nextstep/security/repository/HttpSessionSecurityContextRepository.java new file mode 100644 index 00000000..94a52e7a --- /dev/null +++ b/src/main/java/nextstep/security/repository/HttpSessionSecurityContextRepository.java @@ -0,0 +1,31 @@ +package nextstep.security.repository; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import nextstep.security.model.context.SecurityContext; +import org.springframework.stereotype.Repository; + +import static nextstep.security.utils.Constants.SPRING_SECURITY_CONTEXT_KEY; + +/** +This class is not actually running upon JPA, it's kind of mock server + That's why this class contains logics itself. + */ +@Repository +public class HttpSessionSecurityContextRepository { + + public SecurityContext loadContext(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session == null) { + return null; + } + return (SecurityContext) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY); + } + 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/service/authentication/AuthenticationManager.java b/src/main/java/nextstep/security/service/authentication/AuthenticationManager.java new file mode 100644 index 00000000..ef25574b --- /dev/null +++ b/src/main/java/nextstep/security/service/authentication/AuthenticationManager.java @@ -0,0 +1,9 @@ +package nextstep.security.service.authentication; + +import nextstep.security.model.authentication.Authentication; + +public interface AuthenticationManager { + + Authentication authenticate(Authentication authentication); + +} diff --git a/src/main/java/nextstep/security/configuration/authentication/AuthenticationProvider.java b/src/main/java/nextstep/security/service/authentication/AuthenticationProvider.java similarity index 68% rename from src/main/java/nextstep/security/configuration/authentication/AuthenticationProvider.java rename to src/main/java/nextstep/security/service/authentication/AuthenticationProvider.java index aea3bb66..7e45305d 100644 --- a/src/main/java/nextstep/security/configuration/authentication/AuthenticationProvider.java +++ b/src/main/java/nextstep/security/service/authentication/AuthenticationProvider.java @@ -1,6 +1,7 @@ -package nextstep.security.configuration.authentication; +package nextstep.security.service.authentication; import nextstep.app.ui.AuthenticationException; +import nextstep.security.model.authentication.Authentication; public interface AuthenticationProvider { diff --git a/src/main/java/nextstep/security/configuration/authentication/DaoAuthenticationProvider.java b/src/main/java/nextstep/security/service/authentication/DaoAuthenticationProvider.java similarity index 84% rename from src/main/java/nextstep/security/configuration/authentication/DaoAuthenticationProvider.java rename to src/main/java/nextstep/security/service/authentication/DaoAuthenticationProvider.java index e856359f..5371fa33 100644 --- a/src/main/java/nextstep/security/configuration/authentication/DaoAuthenticationProvider.java +++ b/src/main/java/nextstep/security/service/authentication/DaoAuthenticationProvider.java @@ -1,8 +1,10 @@ -package nextstep.security.configuration.authentication; +package nextstep.security.service.authentication; import lombok.RequiredArgsConstructor; import nextstep.app.ui.AuthenticationException; import nextstep.security.model.UserDetails; +import nextstep.security.model.authentication.Authentication; +import nextstep.security.model.authentication.UsernamePasswordAuthenticationToken; import nextstep.security.service.UserDetailsService; @RequiredArgsConstructor diff --git a/src/main/java/nextstep/security/configuration/authentication/ProviderManager.java b/src/main/java/nextstep/security/service/authentication/ProviderManager.java similarity index 83% rename from src/main/java/nextstep/security/configuration/authentication/ProviderManager.java rename to src/main/java/nextstep/security/service/authentication/ProviderManager.java index ba32916b..fc9fb7f3 100644 --- a/src/main/java/nextstep/security/configuration/authentication/ProviderManager.java +++ b/src/main/java/nextstep/security/service/authentication/ProviderManager.java @@ -1,8 +1,9 @@ -package nextstep.security.configuration.authentication; +package nextstep.security.service.authentication; import java.util.List; import lombok.RequiredArgsConstructor; +import nextstep.security.model.authentication.Authentication; @RequiredArgsConstructor public class ProviderManager implements AuthenticationManager{ diff --git a/src/main/java/nextstep/security/service/context/SecurityContextHolder.java b/src/main/java/nextstep/security/service/context/SecurityContextHolder.java new file mode 100644 index 00000000..20ab60f3 --- /dev/null +++ b/src/main/java/nextstep/security/service/context/SecurityContextHolder.java @@ -0,0 +1,39 @@ +package nextstep.security.service.context; + +import java.util.Objects; + +import nextstep.security.model.context.SecurityContext; + +public class SecurityContextHolder { + + private static final ThreadLocal contextHolder; + + static { + contextHolder = new ThreadLocal<>(); + } + + public static void clearContext() { + contextHolder.remove(); + } + + public static void setContext(SecurityContext context) { + if (Objects.nonNull(context)) { + contextHolder.set(context); + } + } + + public static SecurityContext getContext() { + if (Objects.nonNull(contextHolder.get())) { + return contextHolder.get(); + } else { + SecurityContext emptyContext = createEmptyContext(); + contextHolder.set(emptyContext); + return emptyContext; + } + } + + public static SecurityContext createEmptyContext() { + return new SecurityContext(); + } + +} diff --git a/src/main/java/nextstep/security/configuration/DefaultSecurityFilterChain.java b/src/main/java/nextstep/security/service/filter/DefaultSecurityFilterChain.java similarity index 91% rename from src/main/java/nextstep/security/configuration/DefaultSecurityFilterChain.java rename to src/main/java/nextstep/security/service/filter/DefaultSecurityFilterChain.java index 997f1823..23b8be96 100644 --- a/src/main/java/nextstep/security/configuration/DefaultSecurityFilterChain.java +++ b/src/main/java/nextstep/security/service/filter/DefaultSecurityFilterChain.java @@ -1,4 +1,4 @@ -package nextstep.security.configuration; +package nextstep.security.service.filter; import java.util.List; diff --git a/src/main/java/nextstep/security/configuration/DelegateFilterProxy.java b/src/main/java/nextstep/security/service/filter/DelegateFilterProxy.java similarity index 94% rename from src/main/java/nextstep/security/configuration/DelegateFilterProxy.java rename to src/main/java/nextstep/security/service/filter/DelegateFilterProxy.java index 5b801f99..7cca2650 100644 --- a/src/main/java/nextstep/security/configuration/DelegateFilterProxy.java +++ b/src/main/java/nextstep/security/service/filter/DelegateFilterProxy.java @@ -1,4 +1,4 @@ -package nextstep.security.configuration; +package nextstep.security.service.filter; import java.io.IOException; diff --git a/src/main/java/nextstep/security/configuration/FilterChainProxy.java b/src/main/java/nextstep/security/service/filter/FilterChainProxy.java similarity index 98% rename from src/main/java/nextstep/security/configuration/FilterChainProxy.java rename to src/main/java/nextstep/security/service/filter/FilterChainProxy.java index 326d73a8..307d8884 100644 --- a/src/main/java/nextstep/security/configuration/FilterChainProxy.java +++ b/src/main/java/nextstep/security/service/filter/FilterChainProxy.java @@ -1,4 +1,4 @@ -package nextstep.security.configuration; +package nextstep.security.service.filter; import java.io.IOException; import java.util.Collections; diff --git a/src/main/java/nextstep/security/configuration/SecurityFilterChain.java b/src/main/java/nextstep/security/service/filter/SecurityFilterChain.java similarity index 84% rename from src/main/java/nextstep/security/configuration/SecurityFilterChain.java rename to src/main/java/nextstep/security/service/filter/SecurityFilterChain.java index 3781d035..e4f93ec3 100644 --- a/src/main/java/nextstep/security/configuration/SecurityFilterChain.java +++ b/src/main/java/nextstep/security/service/filter/SecurityFilterChain.java @@ -1,4 +1,4 @@ -package nextstep.security.configuration; +package nextstep.security.service.filter; import java.util.List; diff --git a/src/test/java/nextstep/app/LoginTest.java b/src/test/java/nextstep/app/LoginTest.java index 27b4d581..33dd3499 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 javax.servlet.http.Cookie; import javax.servlet.http.HttpSession; import nextstep.app.domain.Member; @@ -12,10 +13,13 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @@ -68,4 +72,23 @@ void login_fail_with_invalid_password() throws Exception { response.andExpect(status().isUnauthorized()); } + + @DisplayName("로그인 후 세션을 통해 회원 목록 조회") + @Test + void login_after_members() throws Exception { + ResultActions loginResponse = mockMvc.perform(post("/login") + .param("username", TEST_MEMBER.getEmail()) + .param("password", TEST_MEMBER.getPassword()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ).andDo(print()); + loginResponse.andExpect(status().isOk()); + MvcResult loginResult = loginResponse.andReturn(); + HttpSession session = loginResult.getRequest().getSession(); + String sessionId = session.getId(); + ResultActions membersResponse = mockMvc.perform(get("/members") + .cookie(new Cookie("JSESSIONID", sessionId)) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ); + membersResponse.andExpect(status().isOk()); + } } From ca95ede2e6006f8c141c80ff77f9645b8f4aee55 Mon Sep 17 00:00:00 2001 From: "daehee.kim" Date: Tue, 5 Nov 2024 10:27:46 +0900 Subject: [PATCH 11/11] mocked session for testing --- src/test/java/nextstep/app/LoginTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/java/nextstep/app/LoginTest.java b/src/test/java/nextstep/app/LoginTest.java index 33dd3499..6946e6fe 100644 --- a/src/test/java/nextstep/app/LoginTest.java +++ b/src/test/java/nextstep/app/LoginTest.java @@ -1,6 +1,5 @@ package nextstep.app; -import javax.servlet.http.Cookie; import javax.servlet.http.HttpSession; import nextstep.app.domain.Member; @@ -12,8 +11,8 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpSession; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import static org.assertj.core.api.Assertions.assertThat; @@ -76,17 +75,18 @@ void login_fail_with_invalid_password() throws Exception { @DisplayName("로그인 후 세션을 통해 회원 목록 조회") @Test void login_after_members() throws Exception { + var mockedSession = new MockHttpSession(); ResultActions loginResponse = mockMvc.perform(post("/login") .param("username", TEST_MEMBER.getEmail()) .param("password", TEST_MEMBER.getPassword()) .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + .session(mockedSession) ).andDo(print()); + loginResponse.andExpect(status().isOk()); - MvcResult loginResult = loginResponse.andReturn(); - HttpSession session = loginResult.getRequest().getSession(); - String sessionId = session.getId(); + ResultActions membersResponse = mockMvc.perform(get("/members") - .cookie(new Cookie("JSESSIONID", sessionId)) + .session(mockedSession) .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); membersResponse.andExpect(status().isOk());