From 6e1b868df0c04522641bc8e67a7a4739d0e4fce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=88=98=EB=B9=88?= Date: Fri, 1 Nov 2024 10:38:08 +0900 Subject: [PATCH 01/11] =?UTF-8?q?docs=20=ED=94=84=EB=A6=AC=20=EC=BD=94?= =?UTF-8?q?=EC=8A=A4=20=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index 1e7ba652..de886b4d 100644 --- a/README.md +++ b/README.md @@ -1 +1,32 @@ # spring-security-authentication + +## 인증과 서비스 로직간의 분리 + +- [ ] 인증은 security 패키지에 위치해야한다. +- [ ] 서비스는 app 패키지에 위치해야한다. + +## 기능 요구 사항 + +1. 아이디와 비밀번호 기반의 로그인 기능 구현 +2. Basic 인증을 사용하여 사용자를 식별할 수 있도록 스프링 프래임워크를 통한 웹 앱 구현 + +### 아이디와 비밀번호 기반 로그인 구현 + +- [ ] 사용자가 입력한 아이디와 비밀번호를 확인하여 인증한다. +- [ ] 로그인 성공시 Session 을 사용하여 인증 정보를 저장한다. +- [ ] LoginTest 의 모든 테스트가 성공해야한다. + +### Basic 인증 구현 + +- [ ] 요청의 Authorization 헤더에 Basic 인증 정보를 추출 하여 인증을 추출한다. +- [ ] 인증을 성공한 경우 Session 을 사용하여 인증 정보를 저장한다. +- [ ] MemberTest 의 모든 테스트가 통과해야한다. + +## 프로그래밍 요구사항 + +- [ ] 자바 코드 컨벤션을 준수한다. +- [ ] 들여쓰기는 depth 가 3 이 넘지 않도록 한다. +- [ ] 3항 연산자를 사용하지 않는다. +- [ ] 함수의 길이가 15 라인을 넘지 않도록 한다. +- [ ] else 예약어를 사용하지 않는다. +- [ ] 정리한 기능 목록이 정상적으로 동작하는지 테스트 코드를 구현한다. From a32c048fc070721e5080f955872132df04dd71fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=88=98=EB=B9=88?= Date: Sat, 2 Nov 2024 19:51:33 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat=20UserDetailService=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/service/UserDetail.java | 20 ++++++++++++++++++ .../app/service/UserDetailService.java | 21 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/main/java/nextstep/app/service/UserDetail.java create mode 100644 src/main/java/nextstep/app/service/UserDetailService.java diff --git a/src/main/java/nextstep/app/service/UserDetail.java b/src/main/java/nextstep/app/service/UserDetail.java new file mode 100644 index 00000000..e44d2295 --- /dev/null +++ b/src/main/java/nextstep/app/service/UserDetail.java @@ -0,0 +1,20 @@ +package nextstep.app.service; + +public class UserDetail { + + private final String email; + private final String password; + + public UserDetail(String email, String password) { + this.email = email; + this.password = password; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } +} diff --git a/src/main/java/nextstep/app/service/UserDetailService.java b/src/main/java/nextstep/app/service/UserDetailService.java new file mode 100644 index 00000000..0771f796 --- /dev/null +++ b/src/main/java/nextstep/app/service/UserDetailService.java @@ -0,0 +1,21 @@ +package nextstep.app.service; + +import nextstep.app.domain.MemberRepository; +import org.springframework.stereotype.Service; + +@Service +public class UserDetailService { + + private final MemberRepository memberRepository; + + public UserDetailService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public UserDetail findUserDetail(String email) { + return memberRepository.findByEmail(email) + .map(member -> new UserDetail(member.getEmail(), member.getPassword())) + .orElse(null); + } + +} From 0c551a561b8519bc56ee3096687e6017061dbf86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=88=98=EB=B9=88?= Date: Sat, 2 Nov 2024 19:53:18 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat=20Authentication=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UserDetailService 만 의존하도록 구현 --- .../authentication/Authentication.java | 10 +++++ .../authentication/AuthenticationManager.java | 7 ++++ .../AuthenticationProvider.java | 9 +++++ .../authentication/DefaultAuthentication.java | 39 +++++++++++++++++++ .../DefaultAuthenticationManager.java | 27 +++++++++++++ .../DefaultAuthenticationProvider.java | 35 +++++++++++++++++ 6 files changed, 127 insertions(+) create mode 100644 src/main/java/nextstep/security/authentication/Authentication.java create mode 100644 src/main/java/nextstep/security/authentication/AuthenticationManager.java create mode 100644 src/main/java/nextstep/security/authentication/AuthenticationProvider.java create mode 100644 src/main/java/nextstep/security/authentication/DefaultAuthentication.java create mode 100644 src/main/java/nextstep/security/authentication/DefaultAuthenticationManager.java create mode 100644 src/main/java/nextstep/security/authentication/DefaultAuthenticationProvider.java diff --git a/src/main/java/nextstep/security/authentication/Authentication.java b/src/main/java/nextstep/security/authentication/Authentication.java new file mode 100644 index 00000000..26f466f6 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/Authentication.java @@ -0,0 +1,10 @@ +package nextstep.security.authentication; + +public interface Authentication { + + Object getPrincipal(); + + Object getCredentials(); + + boolean isAuthenticated(); +} diff --git a/src/main/java/nextstep/security/authentication/AuthenticationManager.java b/src/main/java/nextstep/security/authentication/AuthenticationManager.java new file mode 100644 index 00000000..47973f66 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationManager.java @@ -0,0 +1,7 @@ +package nextstep.security.authentication; + +public interface AuthenticationManager { + + Authentication authenticate(Authentication authentication); + +} diff --git a/src/main/java/nextstep/security/authentication/AuthenticationProvider.java b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java new file mode 100644 index 00000000..37c69738 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java @@ -0,0 +1,9 @@ +package nextstep.security.authentication; + +public interface AuthenticationProvider { + + Authentication authenticate(Authentication authentication); + + boolean supports(Class authentication); + +} diff --git a/src/main/java/nextstep/security/authentication/DefaultAuthentication.java b/src/main/java/nextstep/security/authentication/DefaultAuthentication.java new file mode 100644 index 00000000..3eabffac --- /dev/null +++ b/src/main/java/nextstep/security/authentication/DefaultAuthentication.java @@ -0,0 +1,39 @@ +package nextstep.security.authentication; + +import java.io.Serializable; + +public class DefaultAuthentication implements Authentication, Serializable { + + private final String email; + private final String password; + private final boolean authenticated; + + private DefaultAuthentication(String email, String password, boolean authenticated) { + this.email = email; + this.password = password; + this.authenticated = authenticated; + } + + public static DefaultAuthentication unauthenticated(String email, String password) { + return new DefaultAuthentication(email, password, false); + } + + public static DefaultAuthentication authenticated(String email, String password) { + return new DefaultAuthentication(email, password, true); + } + + @Override + public Object getPrincipal() { + return email; + } + + @Override + public Object getCredentials() { + return password; + } + + @Override + public boolean isAuthenticated() { + return authenticated; + } +} diff --git a/src/main/java/nextstep/security/authentication/DefaultAuthenticationManager.java b/src/main/java/nextstep/security/authentication/DefaultAuthenticationManager.java new file mode 100644 index 00000000..5db2071e --- /dev/null +++ b/src/main/java/nextstep/security/authentication/DefaultAuthenticationManager.java @@ -0,0 +1,27 @@ +package nextstep.security.authentication; + +import java.util.List; + +public class DefaultAuthenticationManager implements AuthenticationManager { + + private final List authenticationProviders; + + public DefaultAuthenticationManager(List authenticationProviders) { + this.authenticationProviders = authenticationProviders; + } + + @Override + public Authentication authenticate(Authentication authentication) { + for (AuthenticationProvider provider : authenticationProviders) { + + if (!provider.supports(authentication.getClass())) { + continue; + } + + return provider.authenticate(authentication); + } + + return null; + } + +} diff --git a/src/main/java/nextstep/security/authentication/DefaultAuthenticationProvider.java b/src/main/java/nextstep/security/authentication/DefaultAuthenticationProvider.java new file mode 100644 index 00000000..236b99a6 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/DefaultAuthenticationProvider.java @@ -0,0 +1,35 @@ +package nextstep.security.authentication; + +import nextstep.app.service.UserDetail; +import nextstep.app.service.UserDetailService; + +public class DefaultAuthenticationProvider implements AuthenticationProvider { + + private final UserDetailService userDetailService; + + public DefaultAuthenticationProvider(UserDetailService userDetailService) { + this.userDetailService = userDetailService; + } + + @Override + public Authentication authenticate(Authentication authentication) { + UserDetail userDetail = userDetailService.findUserDetail( + authentication.getPrincipal().toString()); + + if (userDetail == null) { + return null; + } + + if (!userDetail.getPassword().equals(authentication.getCredentials().toString())) { + return null; + } + + return DefaultAuthentication.authenticated( + userDetail.getEmail(), userDetail.getPassword()); + } + + @Override + public boolean supports(Class authentication) { + return DefaultAuthentication.class.isAssignableFrom(authentication); + } +} From 178fdd6e26cee8fe3b5c4fa78dcb2eeebddafbf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=88=98=EB=B9=88?= Date: Sat, 2 Nov 2024 19:55:19 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat=20=EC=95=84=EC=9D=B4=EB=94=94?= =?UTF-8?q?=EC=99=80=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=EC=9D=98=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/config/WebConfig.java | 40 +++++++++++++ .../FormLoginAuthorizationInterceptor.java | 57 +++++++++++++++++++ .../nextstep/security/SecurityConstants.java | 11 ++++ 3 files changed, 108 insertions(+) create mode 100644 src/main/java/nextstep/app/config/WebConfig.java create mode 100644 src/main/java/nextstep/security/FormLoginAuthorizationInterceptor.java create mode 100644 src/main/java/nextstep/security/SecurityConstants.java diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java new file mode 100644 index 00000000..2b24ba56 --- /dev/null +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -0,0 +1,40 @@ +package nextstep.app.config; + +import java.util.List; +import nextstep.app.service.UserDetailService; +import nextstep.security.FormLoginAuthorizationInterceptor; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.AuthenticationProvider; +import nextstep.security.authentication.DefaultAuthenticationManager; +import nextstep.security.authentication.DefaultAuthenticationProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final UserDetailService userDetailService; + + public WebConfig(UserDetailService userDetailService) { + this.userDetailService = userDetailService; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new FormLoginAuthorizationInterceptor(authenticationManager())) + .addPathPatterns("/login"); + } + + @Bean + public AuthenticationManager authenticationManager() { + return new DefaultAuthenticationManager(authenticationProviders()); + } + + @Bean + public List authenticationProviders() { + return List.of(new DefaultAuthenticationProvider(userDetailService)); + } + +} diff --git a/src/main/java/nextstep/security/FormLoginAuthorizationInterceptor.java b/src/main/java/nextstep/security/FormLoginAuthorizationInterceptor.java new file mode 100644 index 00000000..0c467ace --- /dev/null +++ b/src/main/java/nextstep/security/FormLoginAuthorizationInterceptor.java @@ -0,0 +1,57 @@ +package nextstep.security; + +import static nextstep.security.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; + +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import nextstep.app.ui.AuthenticationException; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.DefaultAuthentication; +import org.springframework.web.servlet.HandlerInterceptor; + +public class FormLoginAuthorizationInterceptor implements HandlerInterceptor { + + private final AuthenticationManager authenticationManager; + + public FormLoginAuthorizationInterceptor(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + try { + Map paramMap = request.getParameterMap(); + + Authentication authentication = authenticationManager.authenticate( + createAuthentication(paramMap)); + + validateAuthentication(authentication); + + request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, authentication); + return true; + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + } + + private Authentication createAuthentication(Map paramMap) { + return DefaultAuthentication.unauthenticated( + paramMap.get("username")[0], + paramMap.get("password")[0]); + } + + private void validateAuthentication(Authentication authentication) { + if (authentication == null) { + throw new AuthenticationException(); + } + + if (!authentication.isAuthenticated()) { + throw new AuthenticationException(); + } + } + +} diff --git a/src/main/java/nextstep/security/SecurityConstants.java b/src/main/java/nextstep/security/SecurityConstants.java new file mode 100644 index 00000000..23b31da5 --- /dev/null +++ b/src/main/java/nextstep/security/SecurityConstants.java @@ -0,0 +1,11 @@ +package nextstep.security; + +public class SecurityConstants { + + private SecurityConstants() { + throw new IllegalStateException("Utility class"); + } + + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + +} From af589691dd57bc73344b50b523e32f0ed0a1c257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=88=98=EB=B9=88?= Date: Sat, 2 Nov 2024 19:57:10 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat=20Basic=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/config/WebConfig.java | 3 + .../BasicAuthorizationInterceptor.java | 86 +++++++++++++++++++ .../nextstep/security/SecurityConstants.java | 2 + 3 files changed, 91 insertions(+) create mode 100644 src/main/java/nextstep/security/BasicAuthorizationInterceptor.java diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index 2b24ba56..0da5a3fd 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -2,6 +2,7 @@ import java.util.List; import nextstep.app.service.UserDetailService; +import nextstep.security.BasicAuthorizationInterceptor; import nextstep.security.FormLoginAuthorizationInterceptor; import nextstep.security.authentication.AuthenticationManager; import nextstep.security.authentication.AuthenticationProvider; @@ -25,6 +26,8 @@ public WebConfig(UserDetailService userDetailService) { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new FormLoginAuthorizationInterceptor(authenticationManager())) .addPathPatterns("/login"); + registry.addInterceptor(new BasicAuthorizationInterceptor(authenticationManager())) + .excludePathPatterns("/login"); } @Bean diff --git a/src/main/java/nextstep/security/BasicAuthorizationInterceptor.java b/src/main/java/nextstep/security/BasicAuthorizationInterceptor.java new file mode 100644 index 00000000..6f0486ce --- /dev/null +++ b/src/main/java/nextstep/security/BasicAuthorizationInterceptor.java @@ -0,0 +1,86 @@ +package nextstep.security; + +import static nextstep.security.SecurityConstants.BASIC_TOKEN_PREFIX; +import static nextstep.security.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; + +import java.nio.charset.StandardCharsets; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import nextstep.app.ui.AuthenticationException; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.DefaultAuthentication; +import org.springframework.http.HttpHeaders; +import org.springframework.util.Base64Utils; +import org.springframework.web.servlet.HandlerInterceptor; + +public class BasicAuthorizationInterceptor implements HandlerInterceptor { + + private final AuthenticationManager authenticationManager; + + public BasicAuthorizationInterceptor(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + try { + String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + + validateBasicToken(authorization); + + String decodedToken = decodeToken(authorization); + + Authentication authentication = authenticationManager.authenticate( + createAuthentication(decodedToken)); + + validateAuthentication(authentication); + + request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, authentication); + return true; + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + } + + private void validateBasicToken(String authorization) { + if (authorization == null) { + throw new AuthenticationException(); + } + + if (!authorization.startsWith(BASIC_TOKEN_PREFIX)) { + throw new AuthenticationException(); + } + } + + private String decodeToken(String authorization) { + String encodedToken = authorization.substring(BASIC_TOKEN_PREFIX.length()); + + if (encodedToken.isBlank()) { + throw new AuthenticationException(); + } + return new String(Base64Utils.decodeFromString(encodedToken), StandardCharsets.UTF_8); + } + + private Authentication createAuthentication(String decodedToken) { + String[] emailAndPassword = decodedToken.split(":"); + + if (emailAndPassword.length != 2) { + throw new AuthenticationException(); + } + + return DefaultAuthentication.unauthenticated(emailAndPassword[0], emailAndPassword[1]); + } + + private void validateAuthentication(Authentication authentication) { + if (authentication == null) { + throw new AuthenticationException(); + } + + if (!authentication.isAuthenticated()) { + throw new AuthenticationException(); + } + } +} diff --git a/src/main/java/nextstep/security/SecurityConstants.java b/src/main/java/nextstep/security/SecurityConstants.java index 23b31da5..8304f80e 100644 --- a/src/main/java/nextstep/security/SecurityConstants.java +++ b/src/main/java/nextstep/security/SecurityConstants.java @@ -8,4 +8,6 @@ private SecurityConstants() { public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + public static final String BASIC_TOKEN_PREFIX = "Basic "; + } From 7a336dfed9c9abf3d8bae9baf7c6931660a5558a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=88=98=EB=B9=88?= Date: Sat, 2 Nov 2024 19:58:45 +0900 Subject: [PATCH 06/11] =?UTF-8?q?refactor=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/app/ui/LoginController.java | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 0ea94f1b..a9f65597 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,24 +1,15 @@ package nextstep.app.ui; -import nextstep.app.domain.MemberRepository; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; 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) { From 96c7612ab4c5b42de6649348d08ce8a80c4be79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=88=98=EB=B9=88?= Date: Sat, 2 Nov 2024 20:00:07 +0900 Subject: [PATCH 07/11] =?UTF-8?q?dcos=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=99=84=EB=A3=8C=20=EC=97=AC=EB=B6=80=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index de886b4d..ca6c292c 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ ## 인증과 서비스 로직간의 분리 -- [ ] 인증은 security 패키지에 위치해야한다. -- [ ] 서비스는 app 패키지에 위치해야한다. +- [x] 인증은 security 패키지에 위치해야한다. +- [x] 서비스는 app 패키지에 위치해야한다. ## 기능 요구 사항 @@ -12,21 +12,21 @@ ### 아이디와 비밀번호 기반 로그인 구현 -- [ ] 사용자가 입력한 아이디와 비밀번호를 확인하여 인증한다. -- [ ] 로그인 성공시 Session 을 사용하여 인증 정보를 저장한다. -- [ ] LoginTest 의 모든 테스트가 성공해야한다. +- [x] 사용자가 입력한 아이디와 비밀번호를 확인하여 인증한다. +- [x] 로그인 성공시 Session 을 사용하여 인증 정보를 저장한다. +- [x] LoginTest 의 모든 테스트가 성공해야한다. ### Basic 인증 구현 -- [ ] 요청의 Authorization 헤더에 Basic 인증 정보를 추출 하여 인증을 추출한다. -- [ ] 인증을 성공한 경우 Session 을 사용하여 인증 정보를 저장한다. -- [ ] MemberTest 의 모든 테스트가 통과해야한다. +- [x] 요청의 Authorization 헤더에 Basic 인증 정보를 추출 하여 인증을 추출한다. +- [x] 인증을 성공한 경우 Session 을 사용하여 인증 정보를 저장한다. +- [x] MemberTest 의 모든 테스트가 통과해야한다. ## 프로그래밍 요구사항 -- [ ] 자바 코드 컨벤션을 준수한다. -- [ ] 들여쓰기는 depth 가 3 이 넘지 않도록 한다. -- [ ] 3항 연산자를 사용하지 않는다. -- [ ] 함수의 길이가 15 라인을 넘지 않도록 한다. -- [ ] else 예약어를 사용하지 않는다. -- [ ] 정리한 기능 목록이 정상적으로 동작하는지 테스트 코드를 구현한다. +- [x] 자바 코드 컨벤션을 준수한다. +- [x] 들여쓰기는 depth 가 3 이 넘지 않도록 한다. +- [x] 3항 연산자를 사용하지 않는다. +- [x] 함수의 길이가 15 라인을 넘지 않도록 한다. +- [x] else 예약어를 사용하지 않는다. +- [x] 정리한 기능 목록이 정상적으로 동작하는지 테스트 코드를 구현한다. From 673e7aaa99f51cf20e668ba6e3ecd857b640f7dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=88=98=EB=B9=88?= Date: Mon, 4 Nov 2024 14:31:33 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat=20SecurityFilterChain=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +++ .../nextstep/app/config/SecurityConfig.java | 34 ++++++++ .../filter/DefaultSecurityFilterChain.java | 25 ++++++ .../filter/DelegatingFilterProxy.java | 25 ++++++ .../security/filter/FilterChainProxy.java | 79 +++++++++++++++++++ .../security/filter/SecurityFilterChain.java | 13 +++ 6 files changed, 189 insertions(+) create mode 100644 src/main/java/nextstep/app/config/SecurityConfig.java create mode 100644 src/main/java/nextstep/security/filter/DefaultSecurityFilterChain.java create mode 100644 src/main/java/nextstep/security/filter/DelegatingFilterProxy.java create mode 100644 src/main/java/nextstep/security/filter/FilterChainProxy.java create mode 100644 src/main/java/nextstep/security/filter/SecurityFilterChain.java diff --git a/README.md b/README.md index ca6c292c..c06bf0cb 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,16 @@ - [x] 함수의 길이가 15 라인을 넘지 않도록 한다. - [x] else 예약어를 사용하지 않는다. - [x] 정리한 기능 목록이 정상적으로 동작하는지 테스트 코드를 구현한다. + +# 페어 코딩 + +## Interceptor 에서 Filter 로 변경하기 + +## Registry 등록에서 DelegatingFilterProxy 로 변경하기 + +## AuthenticationManager 로 인증 추상화 하기 + +## SecurityContextHolder 로 인증 정보 객체 저장하기 + +## SecurityContextHolderFilter 구현하기 + diff --git a/src/main/java/nextstep/app/config/SecurityConfig.java b/src/main/java/nextstep/app/config/SecurityConfig.java new file mode 100644 index 00000000..e2594d1a --- /dev/null +++ b/src/main/java/nextstep/app/config/SecurityConfig.java @@ -0,0 +1,34 @@ +package nextstep.app.config; + +import java.util.List; +import nextstep.security.filter.DefaultSecurityFilterChain; +import nextstep.security.filter.DelegatingFilterProxy; +import nextstep.security.filter.FilterChainProxy; +import nextstep.security.filter.SecurityFilterChain; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain() { + return new DefaultSecurityFilterChain( + // todo 우리가 만든 필터를 여기에 추가해야 함. + List.of() + ); + } + + @Bean + public FilterChainProxy filterChainProxy(List securityFilterChains) { + // 여러 개의 시큐리티 필터 체인을 목록으로 가진다. + return new FilterChainProxy(securityFilterChains); + } + + @Bean + public DelegatingFilterProxy delegatingFilterProxy() { + // 필터 체인 프록시에게 위임하는 역할을 한다. + return new DelegatingFilterProxy(filterChainProxy(List.of(securityFilterChain()))); + } + +} diff --git a/src/main/java/nextstep/security/filter/DefaultSecurityFilterChain.java b/src/main/java/nextstep/security/filter/DefaultSecurityFilterChain.java new file mode 100644 index 00000000..6d47995a --- /dev/null +++ b/src/main/java/nextstep/security/filter/DefaultSecurityFilterChain.java @@ -0,0 +1,25 @@ +package nextstep.security.filter; + +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; + +public class DefaultSecurityFilterChain implements SecurityFilterChain { + + private List filters; + + public DefaultSecurityFilterChain(List filters) { + this.filters = filters; + } + + @Override + public boolean matches(HttpServletRequest request) { + // todo 구현 예정 + return true; + } + + @Override + public List getFilters() { + return filters; + } +} diff --git a/src/main/java/nextstep/security/filter/DelegatingFilterProxy.java b/src/main/java/nextstep/security/filter/DelegatingFilterProxy.java new file mode 100644 index 00000000..038e0880 --- /dev/null +++ b/src/main/java/nextstep/security/filter/DelegatingFilterProxy.java @@ -0,0 +1,25 @@ +package nextstep.security.filter; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import org.springframework.web.filter.GenericFilterBean; + +public class DelegatingFilterProxy extends GenericFilterBean { + + private final Filter delegate; + + public DelegatingFilterProxy(Filter delegate) { + this.delegate = delegate; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) throws IOException, ServletException { + delegate.doFilter(servletRequest, servletResponse, filterChain); + } + +} diff --git a/src/main/java/nextstep/security/filter/FilterChainProxy.java b/src/main/java/nextstep/security/filter/FilterChainProxy.java new file mode 100644 index 00000000..818c363e --- /dev/null +++ b/src/main/java/nextstep/security/filter/FilterChainProxy.java @@ -0,0 +1,79 @@ +package nextstep.security.filter; + +import java.io.IOException; +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 org.springframework.web.filter.GenericFilterBean; + +public class FilterChainProxy extends GenericFilterBean { + + // 필터가 아닌 필터 체인을 목록으로 가지는 이유? + private final List filterChains; + + public FilterChainProxy(List filterChains) { + this.filterChains = filterChains; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) throws IOException, ServletException { + // 필터 체인에서 일치하는 필터 목록을 가져온다. + List filters = getFilterChain(servletRequest); + + // 필터 체인과 찾은 목록으로 버츄얼 필터체인을 생성한다. + VirtualFilterChain virtualFilterChain = new VirtualFilterChain(filterChain, filters); + + // 생성한 버츄얼 필터 체인이 실행된다. + virtualFilterChain.doFilter(servletRequest, servletResponse); + } + + private static final class VirtualFilterChain implements FilterChain { + + private final FilterChain originalChain; + private final List additionalFilters; + + private final int size; + private int currentPosition = 0; + + private VirtualFilterChain(FilterChain originalChain, List additionalFilters) { + this.originalChain = originalChain; + this.additionalFilters = additionalFilters; + this.size = additionalFilters.size(); + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + + // 인덱스와 필터 목록의 갯수를 비교한다. + if (this.currentPosition == this.size) { + this.originalChain.doFilter(servletRequest, servletResponse); + return; + } + + // 인덱스를 한칸 이동 + this.currentPosition++; + + // 실행할 필터를 선택 + Filter nextFilter = additionalFilters.get(currentPosition - 1); + + // 필터를 실행한다. + nextFilter.doFilter(servletRequest, servletResponse, this); + } + } + + private List getFilterChain(ServletRequest servletRequest) { + for (SecurityFilterChain chain : filterChains) { + if (chain.matches((HttpServletRequest) servletRequest)) { + return chain.getFilters(); + } + } + return null; + } + +} diff --git a/src/main/java/nextstep/security/filter/SecurityFilterChain.java b/src/main/java/nextstep/security/filter/SecurityFilterChain.java new file mode 100644 index 00000000..80ae35b2 --- /dev/null +++ b/src/main/java/nextstep/security/filter/SecurityFilterChain.java @@ -0,0 +1,13 @@ +package nextstep.security.filter; + +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; + +public interface SecurityFilterChain { + + boolean matches(HttpServletRequest request); + + List getFilters(); + +} \ No newline at end of file From 41b1e3825ab83638c664782794be77535525615f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=88=98=EB=B9=88?= Date: Mon, 4 Nov 2024 15:26:53 +0900 Subject: [PATCH 09/11] =?UTF-8?q?feat=20Interceptor=20=EB=A5=BC=20Filter?= =?UTF-8?q?=20=EB=A1=9C=20=EC=9E=AC=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 +++++++ .../nextstep/app/config/SecurityConfig.java | 15 +++++- .../java/nextstep/app/config/WebConfig.java | 11 ---- ...or.java => BasicAuthenticationFilter.java} | 50 ++++++++++++------- ...UsernamePasswordAuthenticationFilter.java} | 27 +++++++--- 5 files changed, 83 insertions(+), 37 deletions(-) rename src/main/java/nextstep/security/{BasicAuthorizationInterceptor.java => BasicAuthenticationFilter.java} (60%) rename src/main/java/nextstep/security/{FormLoginAuthorizationInterceptor.java => UsernamePasswordAuthenticationFilter.java} (66%) diff --git a/README.md b/README.md index c06bf0cb..a97a014e 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,25 @@ # 페어 코딩 +- `SecurityFilterChain` : 보안 필터의 묶음을 정의하는 인터페이스 +- `DefaultSecurityFilterChain` : `SecurityFilterChain` 의 구현체 +- `FilterChainProxy` : 보안 필터의 묶음을 관리하는 객체 +- `DelegatingFilterProxy` : 스프링에 등록한 필터를 실행을 위임할 객체 + +패키지 분리 + ## Interceptor 에서 Filter 로 변경하기 +### `GenericFilterBean` 와 `OncePerRequestFilter` 의 차이점 + +실행 횟수: `GenericFilterBean` 은 요청마다 실행될 수 있지만, `OncePerRequestFilter` 는 요청당 한 번만 실행됩니다. +사용 목적: 요청별로 한 번만 실행되어야 하는 필터링 로직에는 `OncePerRequestFilter` 가 적합하며, 그렇지 않으면 `GenericFilterBean` 을 사용할 +수 있습니다. + +- [x] `BasicAuthorizationInterceptor` -> `BasicAuthenticationFilter` +- [x] `FormLoginAuthorizationInterceptor` -> `UsernamePasswordAuthenticationFilter` +- [x] `WebMvcConfigurer` -> `SecurityConfig` 로 변경 + ## Registry 등록에서 DelegatingFilterProxy 로 변경하기 ## AuthenticationManager 로 인증 추상화 하기 diff --git a/src/main/java/nextstep/app/config/SecurityConfig.java b/src/main/java/nextstep/app/config/SecurityConfig.java index e2594d1a..5a0be553 100644 --- a/src/main/java/nextstep/app/config/SecurityConfig.java +++ b/src/main/java/nextstep/app/config/SecurityConfig.java @@ -1,6 +1,9 @@ package nextstep.app.config; import java.util.List; +import nextstep.security.BasicAuthenticationFilter; +import nextstep.security.UsernamePasswordAuthenticationFilter; +import nextstep.security.authentication.AuthenticationManager; import nextstep.security.filter.DefaultSecurityFilterChain; import nextstep.security.filter.DelegatingFilterProxy; import nextstep.security.filter.FilterChainProxy; @@ -11,11 +14,19 @@ @Configuration public class SecurityConfig { + private final AuthenticationManager authenticationManager; + + public SecurityConfig(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + @Bean public SecurityFilterChain securityFilterChain() { return new DefaultSecurityFilterChain( - // todo 우리가 만든 필터를 여기에 추가해야 함. - List.of() + List.of( + new BasicAuthenticationFilter(authenticationManager), + new UsernamePasswordAuthenticationFilter(authenticationManager) + ) ); } diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index 0da5a3fd..db5dde78 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -2,15 +2,12 @@ import java.util.List; import nextstep.app.service.UserDetailService; -import nextstep.security.BasicAuthorizationInterceptor; -import nextstep.security.FormLoginAuthorizationInterceptor; import nextstep.security.authentication.AuthenticationManager; import nextstep.security.authentication.AuthenticationProvider; import nextstep.security.authentication.DefaultAuthenticationManager; import nextstep.security.authentication.DefaultAuthenticationProvider; 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 @@ -22,14 +19,6 @@ public WebConfig(UserDetailService userDetailService) { this.userDetailService = userDetailService; } - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new FormLoginAuthorizationInterceptor(authenticationManager())) - .addPathPatterns("/login"); - registry.addInterceptor(new BasicAuthorizationInterceptor(authenticationManager())) - .excludePathPatterns("/login"); - } - @Bean public AuthenticationManager authenticationManager() { return new DefaultAuthenticationManager(authenticationProviders()); diff --git a/src/main/java/nextstep/security/BasicAuthorizationInterceptor.java b/src/main/java/nextstep/security/BasicAuthenticationFilter.java similarity index 60% rename from src/main/java/nextstep/security/BasicAuthorizationInterceptor.java rename to src/main/java/nextstep/security/BasicAuthenticationFilter.java index 6f0486ce..b1e823d8 100644 --- a/src/main/java/nextstep/security/BasicAuthorizationInterceptor.java +++ b/src/main/java/nextstep/security/BasicAuthenticationFilter.java @@ -3,7 +3,11 @@ import static nextstep.security.SecurityConstants.BASIC_TOKEN_PREFIX; import static nextstep.security.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.List; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import nextstep.app.ui.AuthenticationException; @@ -12,37 +16,49 @@ import nextstep.security.authentication.DefaultAuthentication; import org.springframework.http.HttpHeaders; import org.springframework.util.Base64Utils; -import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.filter.OncePerRequestFilter; -public class BasicAuthorizationInterceptor implements HandlerInterceptor { +public class BasicAuthenticationFilter extends OncePerRequestFilter { private final AuthenticationManager authenticationManager; - public BasicAuthorizationInterceptor(AuthenticationManager authenticationManager) { + private final List ACCEPTED_URIS = List.of( + "/members" + ); + + public BasicAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, - Object handler) throws Exception { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + if (!ACCEPTED_URIS.contains(request.getRequestURI())) { + filterChain.doFilter(request, response); + return; + } + try { - String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + checkAuthentication(request); + filterChain.doFilter(request, response); + } catch (Exception ex) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } - validateBasicToken(authorization); + private void checkAuthentication(HttpServletRequest request) { + String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); - String decodedToken = decodeToken(authorization); + validateBasicToken(authorization); - Authentication authentication = authenticationManager.authenticate( - createAuthentication(decodedToken)); + String decodedToken = decodeToken(authorization); - validateAuthentication(authentication); + Authentication authentication = authenticationManager.authenticate( + createAuthentication(decodedToken)); - request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, authentication); - return true; - } catch (Exception e) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return false; - } + validateAuthentication(authentication); + + request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, authentication); } private void validateBasicToken(String authorization) { diff --git a/src/main/java/nextstep/security/FormLoginAuthorizationInterceptor.java b/src/main/java/nextstep/security/UsernamePasswordAuthenticationFilter.java similarity index 66% rename from src/main/java/nextstep/security/FormLoginAuthorizationInterceptor.java rename to src/main/java/nextstep/security/UsernamePasswordAuthenticationFilter.java index 0c467ace..d6032b7d 100644 --- a/src/main/java/nextstep/security/FormLoginAuthorizationInterceptor.java +++ b/src/main/java/nextstep/security/UsernamePasswordAuthenticationFilter.java @@ -2,26 +2,39 @@ import static nextstep.security.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; +import java.io.IOException; +import java.util.List; import java.util.Map; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import nextstep.app.ui.AuthenticationException; import nextstep.security.authentication.Authentication; import nextstep.security.authentication.AuthenticationManager; import nextstep.security.authentication.DefaultAuthentication; -import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.filter.OncePerRequestFilter; -public class FormLoginAuthorizationInterceptor implements HandlerInterceptor { +public class UsernamePasswordAuthenticationFilter extends OncePerRequestFilter { private final AuthenticationManager authenticationManager; - public FormLoginAuthorizationInterceptor(AuthenticationManager authenticationManager) { + private final List ACCEPTED_URIS = List.of( + "/login" + ); + + public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, - Object handler) throws Exception { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + if (!ACCEPTED_URIS.contains(request.getRequestURI())) { + filterChain.doFilter(request, response); + return; + } + try { Map paramMap = request.getParameterMap(); @@ -31,10 +44,10 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons validateAuthentication(authentication); request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, authentication); - return true; + + filterChain.doFilter(request, response); } catch (Exception e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return false; } } From cb54cd0418eac190b81081f971c5ebf695791163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=88=98=EB=B9=88?= Date: Mon, 4 Nov 2024 15:42:32 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat=20AuthenticationManager=20=EB=A1=9C?= =?UTF-8?q?=20=EC=9D=B8=EC=A6=9D=20=EC=B6=94=EC=83=81=ED=99=94=20=ED=95=98?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +++-- .../java/nextstep/app/config/WebConfig.java | 8 ++-- .../security/BasicAuthenticationFilter.java | 5 ++- .../UsernamePasswordAuthenticationFilter.java | 4 +- ...er.java => DaoAuthenticationProvider.java} | 8 ++-- .../authentication/DefaultAuthentication.java | 39 ------------------ ...ationManager.java => ProviderManager.java} | 4 +- .../UsernamePasswordAuthenticationToken.java | 40 +++++++++++++++++++ 8 files changed, 61 insertions(+), 56 deletions(-) rename src/main/java/nextstep/security/authentication/{DefaultAuthenticationProvider.java => DaoAuthenticationProvider.java} (72%) delete mode 100644 src/main/java/nextstep/security/authentication/DefaultAuthentication.java rename src/main/java/nextstep/security/authentication/{DefaultAuthenticationManager.java => ProviderManager.java} (76%) create mode 100644 src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java diff --git a/README.md b/README.md index a97a014e..d4b04901 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ 패키지 분리 -## Interceptor 에서 Filter 로 변경하기 +## 1. Interceptor 에서 Filter 로 변경하기 ### `GenericFilterBean` 와 `OncePerRequestFilter` 의 차이점 @@ -52,9 +52,12 @@ - [x] `FormLoginAuthorizationInterceptor` -> `UsernamePasswordAuthenticationFilter` - [x] `WebMvcConfigurer` -> `SecurityConfig` 로 변경 -## Registry 등록에서 DelegatingFilterProxy 로 변경하기 +## 2. AuthenticationManager 로 인증 추상화 하기 -## AuthenticationManager 로 인증 추상화 하기 +- [x] `AuthenticationManager` 구현 +- [x] `ProviderManager` 구현 +- [x] `AuthenticationProvider` 구현 +- [x] `DaoAuthenticationProvider` 구현 ## SecurityContextHolder 로 인증 정보 객체 저장하기 diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index db5dde78..ae056c7b 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -4,8 +4,8 @@ import nextstep.app.service.UserDetailService; import nextstep.security.authentication.AuthenticationManager; import nextstep.security.authentication.AuthenticationProvider; -import nextstep.security.authentication.DefaultAuthenticationManager; -import nextstep.security.authentication.DefaultAuthenticationProvider; +import nextstep.security.authentication.DaoAuthenticationProvider; +import nextstep.security.authentication.ProviderManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -21,12 +21,12 @@ public WebConfig(UserDetailService userDetailService) { @Bean public AuthenticationManager authenticationManager() { - return new DefaultAuthenticationManager(authenticationProviders()); + return new ProviderManager(authenticationProviders()); } @Bean public List authenticationProviders() { - return List.of(new DefaultAuthenticationProvider(userDetailService)); + return List.of(new DaoAuthenticationProvider(userDetailService)); } } diff --git a/src/main/java/nextstep/security/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/BasicAuthenticationFilter.java index b1e823d8..c861e420 100644 --- a/src/main/java/nextstep/security/BasicAuthenticationFilter.java +++ b/src/main/java/nextstep/security/BasicAuthenticationFilter.java @@ -13,7 +13,7 @@ import nextstep.app.ui.AuthenticationException; import nextstep.security.authentication.Authentication; import nextstep.security.authentication.AuthenticationManager; -import nextstep.security.authentication.DefaultAuthentication; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.http.HttpHeaders; import org.springframework.util.Base64Utils; import org.springframework.web.filter.OncePerRequestFilter; @@ -87,7 +87,8 @@ private Authentication createAuthentication(String decodedToken) { throw new AuthenticationException(); } - return DefaultAuthentication.unauthenticated(emailAndPassword[0], emailAndPassword[1]); + return UsernamePasswordAuthenticationToken.unauthenticated(emailAndPassword[0], + emailAndPassword[1]); } private void validateAuthentication(Authentication authentication) { diff --git a/src/main/java/nextstep/security/UsernamePasswordAuthenticationFilter.java b/src/main/java/nextstep/security/UsernamePasswordAuthenticationFilter.java index d6032b7d..842d33e9 100644 --- a/src/main/java/nextstep/security/UsernamePasswordAuthenticationFilter.java +++ b/src/main/java/nextstep/security/UsernamePasswordAuthenticationFilter.java @@ -12,7 +12,7 @@ import nextstep.app.ui.AuthenticationException; import nextstep.security.authentication.Authentication; import nextstep.security.authentication.AuthenticationManager; -import nextstep.security.authentication.DefaultAuthentication; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.web.filter.OncePerRequestFilter; public class UsernamePasswordAuthenticationFilter extends OncePerRequestFilter { @@ -52,7 +52,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } private Authentication createAuthentication(Map paramMap) { - return DefaultAuthentication.unauthenticated( + return UsernamePasswordAuthenticationToken.unauthenticated( paramMap.get("username")[0], paramMap.get("password")[0]); } diff --git a/src/main/java/nextstep/security/authentication/DefaultAuthenticationProvider.java b/src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java similarity index 72% rename from src/main/java/nextstep/security/authentication/DefaultAuthenticationProvider.java rename to src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java index 236b99a6..81c4a726 100644 --- a/src/main/java/nextstep/security/authentication/DefaultAuthenticationProvider.java +++ b/src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java @@ -3,11 +3,11 @@ import nextstep.app.service.UserDetail; import nextstep.app.service.UserDetailService; -public class DefaultAuthenticationProvider implements AuthenticationProvider { +public class DaoAuthenticationProvider implements AuthenticationProvider { private final UserDetailService userDetailService; - public DefaultAuthenticationProvider(UserDetailService userDetailService) { + public DaoAuthenticationProvider(UserDetailService userDetailService) { this.userDetailService = userDetailService; } @@ -24,12 +24,12 @@ public Authentication authenticate(Authentication authentication) { return null; } - return DefaultAuthentication.authenticated( + return UsernamePasswordAuthenticationToken.authenticated( userDetail.getEmail(), userDetail.getPassword()); } @Override public boolean supports(Class authentication) { - return DefaultAuthentication.class.isAssignableFrom(authentication); + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } } diff --git a/src/main/java/nextstep/security/authentication/DefaultAuthentication.java b/src/main/java/nextstep/security/authentication/DefaultAuthentication.java deleted file mode 100644 index 3eabffac..00000000 --- a/src/main/java/nextstep/security/authentication/DefaultAuthentication.java +++ /dev/null @@ -1,39 +0,0 @@ -package nextstep.security.authentication; - -import java.io.Serializable; - -public class DefaultAuthentication implements Authentication, Serializable { - - private final String email; - private final String password; - private final boolean authenticated; - - private DefaultAuthentication(String email, String password, boolean authenticated) { - this.email = email; - this.password = password; - this.authenticated = authenticated; - } - - public static DefaultAuthentication unauthenticated(String email, String password) { - return new DefaultAuthentication(email, password, false); - } - - public static DefaultAuthentication authenticated(String email, String password) { - return new DefaultAuthentication(email, password, true); - } - - @Override - public Object getPrincipal() { - return email; - } - - @Override - public Object getCredentials() { - return password; - } - - @Override - public boolean isAuthenticated() { - return authenticated; - } -} diff --git a/src/main/java/nextstep/security/authentication/DefaultAuthenticationManager.java b/src/main/java/nextstep/security/authentication/ProviderManager.java similarity index 76% rename from src/main/java/nextstep/security/authentication/DefaultAuthenticationManager.java rename to src/main/java/nextstep/security/authentication/ProviderManager.java index 5db2071e..7c160cd0 100644 --- a/src/main/java/nextstep/security/authentication/DefaultAuthenticationManager.java +++ b/src/main/java/nextstep/security/authentication/ProviderManager.java @@ -2,11 +2,11 @@ import java.util.List; -public class DefaultAuthenticationManager implements AuthenticationManager { +public class ProviderManager implements AuthenticationManager { private final List authenticationProviders; - public DefaultAuthenticationManager(List authenticationProviders) { + public ProviderManager(List authenticationProviders) { this.authenticationProviders = authenticationProviders; } diff --git a/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java new file mode 100644 index 00000000..d20b8f68 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java @@ -0,0 +1,40 @@ +package nextstep.security.authentication; + +public class UsernamePasswordAuthenticationToken implements Authentication { + + private final Object principal; + private final Object credentials; + private final boolean authenticated; + + private UsernamePasswordAuthenticationToken(Object principal, Object credentials, + boolean authenticated) { + this.principal = principal; + this.credentials = credentials; + this.authenticated = authenticated; + } + + public static UsernamePasswordAuthenticationToken unauthenticated(Object email, + Object password) { + return new UsernamePasswordAuthenticationToken(email, password, false); + } + + public static UsernamePasswordAuthenticationToken authenticated(Object email, Object password) { + return new UsernamePasswordAuthenticationToken(email, password, true); + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public boolean isAuthenticated() { + return authenticated; + } + +} From 0ddc158edb69216161331008bc7c0efc951b0509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=88=98=EB=B9=88?= Date: Mon, 4 Nov 2024 15:58:50 +0900 Subject: [PATCH 11/11] =?UTF-8?q?feat=20SecurityContextHolder=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +++ .../security/BasicAuthenticationFilter.java | 8 +++-- .../UsernamePasswordAuthenticationFilter.java | 9 ++++-- .../security/context/SecurityContext.java | 24 ++++++++++++++ .../context/SecurityContextHolder.java | 32 +++++++++++++++++++ src/test/java/nextstep/app/LoginTest.java | 20 ++++++------ 6 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 src/main/java/nextstep/security/context/SecurityContext.java create mode 100644 src/main/java/nextstep/security/context/SecurityContextHolder.java diff --git a/README.md b/README.md index d4b04901..8c2db020 100644 --- a/README.md +++ b/README.md @@ -61,5 +61,9 @@ ## SecurityContextHolder 로 인증 정보 객체 저장하기 +- [x] `SecurityContext` 구현 +- [x] `SecurityContextHolder` 구현 +- [x] seesion 에서 `SecurityContext` 로 인증 정보 저장하도록 변경 + ## SecurityContextHolderFilter 구현하기 diff --git a/src/main/java/nextstep/security/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/BasicAuthenticationFilter.java index c861e420..d1715f8a 100644 --- a/src/main/java/nextstep/security/BasicAuthenticationFilter.java +++ b/src/main/java/nextstep/security/BasicAuthenticationFilter.java @@ -1,7 +1,6 @@ package nextstep.security; import static nextstep.security.SecurityConstants.BASIC_TOKEN_PREFIX; -import static nextstep.security.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -14,6 +13,8 @@ import nextstep.security.authentication.Authentication; import nextstep.security.authentication.AuthenticationManager; import nextstep.security.authentication.UsernamePasswordAuthenticationToken; +import nextstep.security.context.SecurityContext; +import nextstep.security.context.SecurityContextHolder; import org.springframework.http.HttpHeaders; import org.springframework.util.Base64Utils; import org.springframework.web.filter.OncePerRequestFilter; @@ -43,6 +44,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse filterChain.doFilter(request, response); } catch (Exception ex) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + SecurityContextHolder.clearContext(); } } @@ -58,7 +60,9 @@ private void checkAuthentication(HttpServletRequest request) { validateAuthentication(authentication); - request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, authentication); + SecurityContext ctx = SecurityContextHolder.createEmptyContext(); + ctx.setAuthentication(authentication); + SecurityContextHolder.setContext(ctx); } private void validateBasicToken(String authorization) { diff --git a/src/main/java/nextstep/security/UsernamePasswordAuthenticationFilter.java b/src/main/java/nextstep/security/UsernamePasswordAuthenticationFilter.java index 842d33e9..e3ca7948 100644 --- a/src/main/java/nextstep/security/UsernamePasswordAuthenticationFilter.java +++ b/src/main/java/nextstep/security/UsernamePasswordAuthenticationFilter.java @@ -1,7 +1,5 @@ package nextstep.security; -import static nextstep.security.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; - import java.io.IOException; import java.util.List; import java.util.Map; @@ -13,6 +11,8 @@ import nextstep.security.authentication.Authentication; import nextstep.security.authentication.AuthenticationManager; import nextstep.security.authentication.UsernamePasswordAuthenticationToken; +import nextstep.security.context.SecurityContext; +import nextstep.security.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; public class UsernamePasswordAuthenticationFilter extends OncePerRequestFilter { @@ -43,11 +43,14 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse validateAuthentication(authentication); - request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, authentication); + SecurityContext ctx = SecurityContextHolder.createEmptyContext(); + ctx.setAuthentication(authentication); + SecurityContextHolder.setContext(ctx); filterChain.doFilter(request, response); } catch (Exception e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + SecurityContextHolder.clearContext(); } } diff --git a/src/main/java/nextstep/security/context/SecurityContext.java b/src/main/java/nextstep/security/context/SecurityContext.java new file mode 100644 index 00000000..0cd3560a --- /dev/null +++ b/src/main/java/nextstep/security/context/SecurityContext.java @@ -0,0 +1,24 @@ +package nextstep.security.context; + +import nextstep.security.authentication.Authentication; + +public class SecurityContext { + + private Authentication authentication; + + public SecurityContext() { + } + + public SecurityContext(Authentication authentication) { + this.authentication = authentication; + } + + public Authentication getAuthentication() { + return authentication; + } + + public void setAuthentication(Authentication authentication) { + this.authentication = authentication; + } + +} diff --git a/src/main/java/nextstep/security/context/SecurityContextHolder.java b/src/main/java/nextstep/security/context/SecurityContextHolder.java new file mode 100644 index 00000000..705ecf82 --- /dev/null +++ b/src/main/java/nextstep/security/context/SecurityContextHolder.java @@ -0,0 +1,32 @@ +package nextstep.security.context; + +public class SecurityContextHolder { + + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + + public static void clearContext() { + contextHolder.remove(); + } + + public static SecurityContext getContext() { + SecurityContext ctx = contextHolder.get(); + + if (ctx == null) { + ctx = createEmptyContext(); + contextHolder.set(ctx); + } + + return ctx; + } + + public static void setContext(SecurityContext context) { + if (context != null) { + contextHolder.set(context); + } + } + + public static SecurityContext createEmptyContext() { + return new SecurityContext(); + } + +} diff --git a/src/test/java/nextstep/app/LoginTest.java b/src/test/java/nextstep/app/LoginTest.java index 717bcc8a..31efcf26 100644 --- a/src/test/java/nextstep/app/LoginTest.java +++ b/src/test/java/nextstep/app/LoginTest.java @@ -1,7 +1,13 @@ package nextstep.app; +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; + import nextstep.app.domain.Member; import nextstep.app.infrastructure.InmemoryMemberRepository; +import nextstep.security.authentication.Authentication; +import nextstep.security.context.SecurityContextHolder; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -9,18 +15,12 @@ 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; - -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; - @SpringBootTest @AutoConfigureMockMvc class LoginTest { + private static final Member TEST_MEMBER = InmemoryMemberRepository.TEST_MEMBER_1; @Autowired @@ -37,9 +37,9 @@ void login_success() throws Exception { loginResponse.andExpect(status().isOk()); - HttpSession session = loginResponse.andReturn().getRequest().getSession(); - assertThat(session).isNotNull(); - assertThat(session.getAttribute("SPRING_SECURITY_CONTEXT")).isNotNull(); + // SecurityContextHolder 로 변경 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + assertThat(authentication).isNotNull(); } @DisplayName("로그인 실패 - 사용자 없음")