From e4265fc5ac0c09f00065a7e082740fb445ff2c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=95=98?= Date: Thu, 31 Oct 2024 22:58:30 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20basic=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=EC=85=89=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++ .../java/nextstep/app/config/WebConfig.java | 22 ++++++ src/main/java/nextstep/app/domain/Member.java | 6 +- .../nextstep/app/domain/MemberService.java | 22 ++++++ .../java/nextstep/app/ui/LoginController.java | 4 + .../nextstep/app/ui/MemberController.java | 1 - .../BasicAuthenticationInterceptor.java | 77 +++++++++++++++++++ .../security/service/UserDetailsService.java | 15 ++++ .../security/userdetails/UserDetails.java | 6 ++ 9 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 src/main/java/nextstep/app/config/WebConfig.java create mode 100644 src/main/java/nextstep/app/domain/MemberService.java create mode 100644 src/main/java/nextstep/security/interceptor/BasicAuthenticationInterceptor.java create mode 100644 src/main/java/nextstep/security/service/UserDetailsService.java create mode 100644 src/main/java/nextstep/security/userdetails/UserDetails.java diff --git a/README.md b/README.md index 1e7ba652..ffc06154 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ # spring-security-authentication + +## 모듈 구성 +* app +* security +> app -> security + +## 인터셉터 분리 +* HandlerInterceptor 를 사용하여 인증 관련 로직을 Controller 클래스에서 분리한다. +* 앞서 구현한 두 인증 방식(아이디 비밀번호 로그인 방식과 Basic 인증 방식) 모두 인터셉터에서 처리되도록 구현한다. 가급적이면 하나의 인터셉터는 하나의 작업만 수행하도록 설계한다. + +## 인증 로직과 서비스 로직 간의 패키지 분리 +* 서비스 코드와 인증 코드를 명확히 분리하여 관리하도록 한다. +* 서비스 관련 코드는 app 패키지에 위치시키고, 인증 관련 코드는 security 패키지에 위치시킨다. +* 리팩터링 과정에서 패키지 간의 양방향 참조가 발생한다면 단방향 참조로 리팩터링한다. +* app 패키지는 패키지에 의존할 수 있지만, 반대로 security 패키지는 app 패키지에 의존하지 않도록 한다. +* 인증 관련 작업은 패키지에서 전담하도록 설계하여, 서비스 로직이 인증 세부 사항에 의존하지 않게 만든다. LoginTest 와 의 모든 테스트는 지속해서 통과해야 한다. 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..6a2d169b --- /dev/null +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -0,0 +1,22 @@ +package nextstep.app.config; + +import nextstep.security.interceptor.BasicAuthenticationInterceptor; +import nextstep.security.service.UserDetailsService; +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 UserDetailsService userDetailsService; + + public WebConfig(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new BasicAuthenticationInterceptor(userDetailsService)); + } +} diff --git a/src/main/java/nextstep/app/domain/Member.java b/src/main/java/nextstep/app/domain/Member.java index 6cafa9c7..e20f9317 100644 --- a/src/main/java/nextstep/app/domain/Member.java +++ b/src/main/java/nextstep/app/domain/Member.java @@ -1,6 +1,8 @@ package nextstep.app.domain; -public class Member { +import nextstep.security.userdetails.UserDetails; + +public class Member implements UserDetails { private final String email; private final String password; private final String name; @@ -13,10 +15,12 @@ public Member(String email, String password, String name, String imageUrl) { this.imageUrl = imageUrl; } + @Override public String getEmail() { return email; } + @Override public String getPassword() { return password; } diff --git a/src/main/java/nextstep/app/domain/MemberService.java b/src/main/java/nextstep/app/domain/MemberService.java new file mode 100644 index 00000000..dce6e306 --- /dev/null +++ b/src/main/java/nextstep/app/domain/MemberService.java @@ -0,0 +1,22 @@ +package nextstep.app.domain; + +import nextstep.security.service.UserDetailsService; +import nextstep.security.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +@Service +public class MemberService implements UserDetailsService { + + private final MemberRepository memberRepository; + + public MemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public UserDetails loadUserByEmailAndPassword(String email, String password) { + return memberRepository.findByEmail(email) + .filter(v -> v.getPassword().equals(password)) + .orElse(null); + } +} diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 0ea94f1b..26114596 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 nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; +import nextstep.security.userdetails.UserDetails; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -22,6 +24,8 @@ public LoginController(MemberRepository memberRepository) { @PostMapping("/login") public ResponseEntity login(HttpServletRequest request, HttpSession session) { + UserDetails userDetails = (UserDetails) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY); + memberRepository.save(new Member(userDetails.getEmail(), userDetails.getPassword(), null, null)); return ResponseEntity.ok().build(); } diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index c8cc74d6..b2267629 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -22,5 +22,4 @@ public ResponseEntity> list() { List members = memberRepository.findAll(); return ResponseEntity.ok(members); } - } diff --git a/src/main/java/nextstep/security/interceptor/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/interceptor/BasicAuthenticationInterceptor.java new file mode 100644 index 00000000..8c1d33ab --- /dev/null +++ b/src/main/java/nextstep/security/interceptor/BasicAuthenticationInterceptor.java @@ -0,0 +1,77 @@ +package nextstep.security.interceptor; + +import nextstep.security.service.UserDetailsService; +import nextstep.security.userdetails.UserDetails; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class BasicAuthenticationInterceptor implements HandlerInterceptor { + + public static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; + private static final String BASIC = "Basic"; + private static final String BLANK = " "; + private static final String COLON = ":"; + + private final UserDetailsService userDetailsService; + + public BasicAuthenticationInterceptor(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String[] authValue = parseBasicHeader(request); + if (authValue == null) { + return true; + } + + UserDetails userDetails = userDetailsService.loadUserByEmailAndPassword(authValue[0], authValue[1]); + if (userDetails != null) { + RequestContextHolder.getRequestAttributes().setAttribute(SPRING_SECURITY_CONTEXT, userDetails, RequestAttributes.SCOPE_SESSION); + } + return true; + } + + @Nullable + private String[] parseBasicHeader(HttpServletRequest request) { + // authorization 헤더를 조회한다. + String authorizationValue = request.getHeader(HttpHeaders.AUTHORIZATION); + if (authorizationValue == null || authorizationValue.isBlank()) { + return null; + } + + // Basic authentication 방식인지 확인한다. + String[] strs = authorizationValue.split(BLANK); + if (strs.length != 2) { + return null; + } + + String authType = strs[0]; + String authValue = strs[1]; + + // Basic authentication 방식이 아닌 경우 null을 반환한다. + if (!BASIC.equalsIgnoreCase(authType)) { + return null; + } + + String decodedBasicValue = new String(Base64.getDecoder().decode(authValue), StandardCharsets.UTF_8); + if (decodedBasicValue.isBlank()) { + return null; + } + + strs = decodedBasicValue.split(COLON); + if (strs.length != 2) { + return null; + } + + return new String[]{strs[0], strs[1]}; + } +} 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..34685e80 --- /dev/null +++ b/src/main/java/nextstep/security/service/UserDetailsService.java @@ -0,0 +1,15 @@ +package nextstep.security.service; + +import nextstep.security.userdetails.UserDetails; + +public interface UserDetailsService { + + /** + * 사용자 이름을 기반으로 사용자 정보를 가져온다. + * + * @param email 사용자 이메일 + * @param password 사용자 비밀번호 + * @return 사용자 정보 + */ + UserDetails loadUserByEmailAndPassword(String email, String password); +} diff --git a/src/main/java/nextstep/security/userdetails/UserDetails.java b/src/main/java/nextstep/security/userdetails/UserDetails.java new file mode 100644 index 00000000..02872215 --- /dev/null +++ b/src/main/java/nextstep/security/userdetails/UserDetails.java @@ -0,0 +1,6 @@ +package nextstep.security.userdetails; + +public interface UserDetails { + String getEmail(); + String getPassword(); +} From 8e8a8470ac01c015df20c687f1d38bcd814cf5b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=95=98?= Date: Fri, 1 Nov 2024 17:13:11 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20=EC=9D=B8=ED=84=B0=EC=85=89=ED=84=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/config/WebConfig.java | 2 ++ .../security/context/UserContextHolder.java | 28 +++++++++++++++++++ .../interceptor/AuthorizationInterceptor.java | 22 +++++++++++++++ .../BasicAuthenticationInterceptor.java | 5 ++-- 4 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 src/main/java/nextstep/security/context/UserContextHolder.java create mode 100644 src/main/java/nextstep/security/interceptor/AuthorizationInterceptor.java diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index 6a2d169b..47f7ff54 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -1,5 +1,6 @@ package nextstep.app.config; +import nextstep.security.interceptor.AuthorizationInterceptor; import nextstep.security.interceptor.BasicAuthenticationInterceptor; import nextstep.security.service.UserDetailsService; import org.springframework.context.annotation.Configuration; @@ -18,5 +19,6 @@ public WebConfig(UserDetailsService userDetailsService) { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new BasicAuthenticationInterceptor(userDetailsService)); + registry.addInterceptor(new AuthorizationInterceptor()).addPathPatterns("/members"); } } diff --git a/src/main/java/nextstep/security/context/UserContextHolder.java b/src/main/java/nextstep/security/context/UserContextHolder.java new file mode 100644 index 00000000..4715afaf --- /dev/null +++ b/src/main/java/nextstep/security/context/UserContextHolder.java @@ -0,0 +1,28 @@ +package nextstep.security.context; + +import nextstep.security.userdetails.UserDetails; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +import static nextstep.security.interceptor.BasicAuthenticationInterceptor.SPRING_SECURITY_CONTEXT; + +public final class UserContextHolder { + + private UserContextHolder() {} + + public static UserDetails getUser() { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + return null; + } + return (UserDetails) attributes.getAttribute(SPRING_SECURITY_CONTEXT, RequestAttributes.SCOPE_SESSION); + } + + public static void setUser(UserDetails userDetails) { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + return; + } + attributes.setAttribute(SPRING_SECURITY_CONTEXT, userDetails, RequestAttributes.SCOPE_SESSION); + } +} diff --git a/src/main/java/nextstep/security/interceptor/AuthorizationInterceptor.java b/src/main/java/nextstep/security/interceptor/AuthorizationInterceptor.java new file mode 100644 index 00000000..dec08362 --- /dev/null +++ b/src/main/java/nextstep/security/interceptor/AuthorizationInterceptor.java @@ -0,0 +1,22 @@ +package nextstep.security.interceptor; + +import nextstep.security.context.UserContextHolder; +import nextstep.security.userdetails.UserDetails; +import org.springframework.http.HttpStatus; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class AuthorizationInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + UserDetails userDetails = UserContextHolder.getUser(); + if (userDetails == null) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + return false; + } + return true; + } +} diff --git a/src/main/java/nextstep/security/interceptor/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/interceptor/BasicAuthenticationInterceptor.java index 8c1d33ab..e73ebd99 100644 --- a/src/main/java/nextstep/security/interceptor/BasicAuthenticationInterceptor.java +++ b/src/main/java/nextstep/security/interceptor/BasicAuthenticationInterceptor.java @@ -1,11 +1,10 @@ package nextstep.security.interceptor; +import nextstep.security.context.UserContextHolder; import nextstep.security.service.UserDetailsService; import nextstep.security.userdetails.UserDetails; import org.springframework.http.HttpHeaders; import org.springframework.lang.Nullable; -import org.springframework.web.context.request.RequestAttributes; -import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; @@ -35,7 +34,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons UserDetails userDetails = userDetailsService.loadUserByEmailAndPassword(authValue[0], authValue[1]); if (userDetails != null) { - RequestContextHolder.getRequestAttributes().setAttribute(SPRING_SECURITY_CONTEXT, userDetails, RequestAttributes.SCOPE_SESSION); + UserContextHolder.setUser(userDetails); } return true; } From bbd9cffe5cbeb7c7374ea73acccdeb991112fef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=95=98?= Date: Fri, 1 Nov 2024 17:38:41 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/ui/LoginController.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 26114596..da8e9b6c 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -2,7 +2,7 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; -import nextstep.security.userdetails.UserDetails; +import nextstep.security.context.UserContextHolder; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -11,6 +11,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; +import java.util.Map; @RestController public class LoginController { @@ -24,8 +25,18 @@ public LoginController(MemberRepository memberRepository) { @PostMapping("/login") public ResponseEntity login(HttpServletRequest request, HttpSession session) { - UserDetails userDetails = (UserDetails) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY); - memberRepository.save(new Member(userDetails.getEmail(), userDetails.getPassword(), null, null)); + Map paramMap = request.getParameterMap(); + String email = paramMap.get("username")[0]; + String password = paramMap.get("password")[0]; + + Member member = memberRepository.findByEmail(email).orElseThrow(AuthenticationException::new); + + if (!member.getPassword().equals(password)) { + throw new AuthenticationException(); + } + + UserContextHolder.setUser(member); + return ResponseEntity.ok().build(); } From 569940ea273c98924574dbca1f2b0585f489bac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=95=98?= Date: Fri, 1 Nov 2024 18:33:45 +0900 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20formLoginAuthInterceptor=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/config/WebConfig.java | 2 ++ .../java/nextstep/app/ui/LoginController.java | 22 ------------ .../interceptor/FormLoginAuthInterceptor.java | 34 +++++++++++++++++++ 3 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 src/main/java/nextstep/security/interceptor/FormLoginAuthInterceptor.java diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index 47f7ff54..3598e241 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -2,6 +2,7 @@ import nextstep.security.interceptor.AuthorizationInterceptor; import nextstep.security.interceptor.BasicAuthenticationInterceptor; +import nextstep.security.interceptor.FormLoginAuthInterceptor; import nextstep.security.service.UserDetailsService; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; @@ -19,6 +20,7 @@ public WebConfig(UserDetailsService userDetailsService) { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new BasicAuthenticationInterceptor(userDetailsService)); + registry.addInterceptor(new FormLoginAuthInterceptor(userDetailsService)).addPathPatterns("/login"); registry.addInterceptor(new AuthorizationInterceptor()).addPathPatterns("/members"); } } diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index da8e9b6c..ef549f41 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,8 +1,5 @@ package nextstep.app.ui; -import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; -import nextstep.security.context.UserContextHolder; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -11,32 +8,13 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; -import java.util.Map; @RestController public class LoginController { public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - private final MemberRepository memberRepository; - - public LoginController(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } - @PostMapping("/login") public ResponseEntity login(HttpServletRequest request, HttpSession session) { - Map paramMap = request.getParameterMap(); - String email = paramMap.get("username")[0]; - String password = paramMap.get("password")[0]; - - Member member = memberRepository.findByEmail(email).orElseThrow(AuthenticationException::new); - - if (!member.getPassword().equals(password)) { - throw new AuthenticationException(); - } - - UserContextHolder.setUser(member); - return ResponseEntity.ok().build(); } diff --git a/src/main/java/nextstep/security/interceptor/FormLoginAuthInterceptor.java b/src/main/java/nextstep/security/interceptor/FormLoginAuthInterceptor.java new file mode 100644 index 00000000..c4b99b7a --- /dev/null +++ b/src/main/java/nextstep/security/interceptor/FormLoginAuthInterceptor.java @@ -0,0 +1,34 @@ +package nextstep.security.interceptor; + +import nextstep.app.ui.AuthenticationException; +import nextstep.security.context.UserContextHolder; +import nextstep.security.service.UserDetailsService; +import nextstep.security.userdetails.UserDetails; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +public class FormLoginAuthInterceptor implements HandlerInterceptor { + + private final UserDetailsService userDetailsService; + + public FormLoginAuthInterceptor(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + Map paramMap = request.getParameterMap(); + String email = paramMap.get("username")[0]; + String password = paramMap.get("password")[0]; + + UserDetails userDetails = userDetailsService.loadUserByEmailAndPassword(email, password); + if (userDetails == null) { + throw new AuthenticationException(); + } + UserContextHolder.setUser(userDetails); + return true; + } +} From 1a4cb391795f7fd2431d26d17a2c9f074e0a5cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=95=98?= Date: Fri, 1 Nov 2024 18:51:15 +0900 Subject: [PATCH 5/7] =?UTF-8?q?docs:=20readme=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ffc06154..9444f1de 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,7 @@ # spring-security-authentication -## 모듈 구성 -* app -* security -> app -> security - -## 인터셉터 분리 -* HandlerInterceptor 를 사용하여 인증 관련 로직을 Controller 클래스에서 분리한다. -* 앞서 구현한 두 인증 방식(아이디 비밀번호 로그인 방식과 Basic 인증 방식) 모두 인터셉터에서 처리되도록 구현한다. 가급적이면 하나의 인터셉터는 하나의 작업만 수행하도록 설계한다. - -## 인증 로직과 서비스 로직 간의 패키지 분리 -* 서비스 코드와 인증 코드를 명확히 분리하여 관리하도록 한다. -* 서비스 관련 코드는 app 패키지에 위치시키고, 인증 관련 코드는 security 패키지에 위치시킨다. -* 리팩터링 과정에서 패키지 간의 양방향 참조가 발생한다면 단방향 참조로 리팩터링한다. -* app 패키지는 패키지에 의존할 수 있지만, 반대로 security 패키지는 app 패키지에 의존하지 않도록 한다. -* 인증 관련 작업은 패키지에서 전담하도록 설계하여, 서비스 로직이 인증 세부 사항에 의존하지 않게 만든다. LoginTest 와 의 모든 테스트는 지속해서 통과해야 한다. +## 구현 기능 목록 +* refactor: formLoginAuthInterceptor +* feat: 로그인 기능 구현 +* feat: 인증 검사 인터셉터 추가 +* feat: basic 인증 인터셉터 추가 \ No newline at end of file From 8001a34293c8a02f37a631ef361a1b9e66b486be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=95=98?= Date: Mon, 4 Nov 2024 15:00:18 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20interceptor=20->=20filter=20?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/app/config/SecurityConfig.java | 68 ++++++++++++++ .../java/nextstep/app/config/WebConfig.java | 3 +- .../security/SecurityFilterChain.java | 12 +++ .../security/filter/AuthorizationFilter.java | 23 +++++ .../filter/BasicAuthenticationFilter.java | 77 ++++++++++++++++ .../security/filter/FilterChainProxy.java | 89 +++++++++++++++++++ .../security/filter/FormLoginAuthFilter.java | 31 +++++++ 7 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 src/main/java/nextstep/app/config/SecurityConfig.java create mode 100644 src/main/java/nextstep/security/SecurityFilterChain.java create mode 100644 src/main/java/nextstep/security/filter/AuthorizationFilter.java create mode 100644 src/main/java/nextstep/security/filter/BasicAuthenticationFilter.java create mode 100644 src/main/java/nextstep/security/filter/FilterChainProxy.java create mode 100644 src/main/java/nextstep/security/filter/FormLoginAuthFilter.java 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..342ac71b --- /dev/null +++ b/src/main/java/nextstep/app/config/SecurityConfig.java @@ -0,0 +1,68 @@ +package nextstep.app.config; + +import nextstep.security.SecurityFilterChain; +import nextstep.security.filter.AuthorizationFilter; +import nextstep.security.filter.BasicAuthenticationFilter; +import nextstep.security.filter.FilterChainProxy; +import nextstep.security.filter.FormLoginAuthFilter; +import nextstep.security.service.UserDetailsService; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +@Configuration +public class SecurityConfig { + + private final UserDetailsService userDetailsService; + + public SecurityConfig(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Bean + public GenericFilterBean delegatingFilterProxy() { + return new FilterChainProxy(List.of( + new SecurityFilterChain() { + @Override + public boolean matches(HttpServletRequest request) { + return request.getRequestURI().equals("/login"); + } + + @Override + public List getFilters() { + return List.of( + new FormLoginAuthFilter(userDetailsService), + new AuthorizationFilter() + ); + } + }, + new SecurityFilterChain() { + @Override + public boolean matches(HttpServletRequest request) { + return request.getRequestURI().startsWith("/members"); + } + + @Override + public List getFilters() { + return List.of( + new BasicAuthenticationFilter(userDetailsService), + new AuthorizationFilter() + ); + } + } + )); + } + + @Bean + public FilterRegistrationBean delegatingFilterProxyFilterRegistrationBean(GenericFilterBean delegatingFilterProxy) { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(delegatingFilterProxy); + registrationBean.addUrlPatterns("/*"); + return registrationBean; + } +} diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index 3598e241..4910bf0a 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -4,11 +4,10 @@ import nextstep.security.interceptor.BasicAuthenticationInterceptor; import nextstep.security.interceptor.FormLoginAuthInterceptor; import nextstep.security.service.UserDetailsService; -import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -@Configuration +//@Configuration public class WebConfig implements WebMvcConfigurer { private final UserDetailsService userDetailsService; diff --git a/src/main/java/nextstep/security/SecurityFilterChain.java b/src/main/java/nextstep/security/SecurityFilterChain.java new file mode 100644 index 00000000..42ac8192 --- /dev/null +++ b/src/main/java/nextstep/security/SecurityFilterChain.java @@ -0,0 +1,12 @@ +package nextstep.security; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +public interface SecurityFilterChain { + + boolean matches(HttpServletRequest request); + + List getFilters(); +} diff --git a/src/main/java/nextstep/security/filter/AuthorizationFilter.java b/src/main/java/nextstep/security/filter/AuthorizationFilter.java new file mode 100644 index 00000000..a72616eb --- /dev/null +++ b/src/main/java/nextstep/security/filter/AuthorizationFilter.java @@ -0,0 +1,23 @@ +package nextstep.security.filter; + +import nextstep.security.context.UserContextHolder; +import nextstep.security.userdetails.UserDetails; +import org.springframework.http.HttpStatus; + +import javax.servlet.*; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class AuthorizationFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + UserDetails userDetails = UserContextHolder.getUser(); + if (userDetails == null) { + ((HttpServletResponse) response).setStatus(HttpStatus.UNAUTHORIZED.value()); + return; + } + + chain.doFilter(request, response); + } +} diff --git a/src/main/java/nextstep/security/filter/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/filter/BasicAuthenticationFilter.java new file mode 100644 index 00000000..3d818a54 --- /dev/null +++ b/src/main/java/nextstep/security/filter/BasicAuthenticationFilter.java @@ -0,0 +1,77 @@ +package nextstep.security.filter; + +import nextstep.security.context.UserContextHolder; +import nextstep.security.service.UserDetailsService; +import nextstep.security.userdetails.UserDetails; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class BasicAuthenticationFilter implements Filter { + + public static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; + private static final String BASIC = "Basic"; + private static final String BLANK = " "; + private static final String COLON = ":"; + + private final UserDetailsService userDetailsService; + + public BasicAuthenticationFilter(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + String[] credentials = parseBasicHeader((HttpServletRequest) request); + if (credentials == null) { + chain.doFilter(request, response); + return; + } + + UserDetails userDetails = userDetailsService.loadUserByEmailAndPassword(credentials[0], credentials[1]); + if (userDetails != null) { + UserContextHolder.setUser(userDetails); + } + chain.doFilter(request, response); + } + + @Nullable + private String[] parseBasicHeader(HttpServletRequest request) { + // authorization 헤더를 조회한다. + String authorizationValue = request.getHeader(HttpHeaders.AUTHORIZATION); + if (authorizationValue == null || authorizationValue.isBlank()) { + return null; + } + + // Basic authentication 방식인지 확인한다. + String[] strs = authorizationValue.split(BLANK); + if (strs.length != 2) { + return null; + } + + String authType = strs[0]; + String authValue = strs[1]; + + // Basic authentication 방식이 아닌 경우 null을 반환한다. + if (!BASIC.equalsIgnoreCase(authType)) { + return null; + } + + String decodedBasicValue = new String(Base64.getDecoder().decode(authValue), StandardCharsets.UTF_8); + if (decodedBasicValue.isBlank()) { + return null; + } + + strs = decodedBasicValue.split(COLON); + if (strs.length != 2) { + return null; + } + + return new String[]{strs[0], strs[1]}; + } +} 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..282595c3 --- /dev/null +++ b/src/main/java/nextstep/security/filter/FilterChainProxy.java @@ -0,0 +1,89 @@ +package nextstep.security.filter; + +import nextstep.security.SecurityFilterChain; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public class FilterChainProxy extends GenericFilterBean { + + private final List filterChains; + private final FilterChainDecorator filterChainDecorator; + + public FilterChainProxy(List filterChains) { + this.filterChains = filterChains; + this.filterChainDecorator = new VirtualFilterChainDecorator(); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + doFilterInternal(request, response, chain); + } + + private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { + // 1. 요청에 맞는 체인 안에 필터 조회 + List filters = getFilters((HttpServletRequest) request); + + // 2. 필터 실행 + filterChainDecorator.decorate(chain, filters) + .doFilter(request, response); + } + + private List getFilters(HttpServletRequest request) { + return filterChains.stream() + .filter(v -> v.matches(request)) + .map(SecurityFilterChain::getFilters) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + public interface FilterChainDecorator { + default FilterChain decorate(FilterChain original) { + return this.decorate(original, List.of()); + } + + FilterChain decorate(FilterChain original, List filters); + } + + public static final class VirtualFilterChainDecorator implements FilterChainDecorator { + public VirtualFilterChainDecorator() { + } + + public FilterChain decorate(FilterChain original) { + return original; + } + + public FilterChain decorate(FilterChain original, List filters) { + return new VirtualFilterChain(original, filters); + } + } + + private static final class VirtualFilterChain implements FilterChain { + private final FilterChain originalChain; + private final List additionalFilters; + private final int size; + private int currentPosition = 0; + + private VirtualFilterChain(FilterChain chain, List additionalFilters) { + this.originalChain = chain; + this.additionalFilters = additionalFilters; + this.size = additionalFilters.size(); + } + + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + if (this.currentPosition == this.size) { + this.originalChain.doFilter(request, response); + } else { + // 현재 포지션 위치 찾는 거 였음.. + ++this.currentPosition; + Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1); + nextFilter.doFilter(request, response, this); + } + } + } +} diff --git a/src/main/java/nextstep/security/filter/FormLoginAuthFilter.java b/src/main/java/nextstep/security/filter/FormLoginAuthFilter.java new file mode 100644 index 00000000..59686862 --- /dev/null +++ b/src/main/java/nextstep/security/filter/FormLoginAuthFilter.java @@ -0,0 +1,31 @@ +package nextstep.security.filter; + +import nextstep.security.context.UserContextHolder; +import nextstep.security.service.UserDetailsService; +import nextstep.security.userdetails.UserDetails; + +import javax.servlet.*; +import java.io.IOException; +import java.util.Map; + +public class FormLoginAuthFilter implements Filter { + + private final UserDetailsService userDetailsService; + + public FormLoginAuthFilter(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + Map paramMap = request.getParameterMap(); + String email = paramMap.get("username")[0]; + String password = paramMap.get("password")[0]; + + UserDetails userDetails = userDetailsService.loadUserByEmailAndPassword(email, password); + if (userDetails != null) { + UserContextHolder.setUser(userDetails); + } + chain.doFilter(request, response); + } +} From 8c5ef7e4128237ac5735390f1ce957bd71590482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=95=98?= Date: Mon, 4 Nov 2024 15:45:16 +0900 Subject: [PATCH 7/7] =?UTF-8?q?feat:=20authenticationManager=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/app/config/SecurityConfig.java | 42 +++++-------------- .../security/DefaultSecurityFilterChain.java | 26 ++++++++++++ .../authentication/Authentication.java | 6 +++ .../UsernamePasswordAuthenticationToken.java | 21 ++++++++++ .../filter/BasicAuthenticationFilter.java | 16 +++---- .../security/filter/FormLoginAuthFilter.java | 16 +++---- .../manager/AuthenticationManager.java | 8 ++++ .../security/manager/ProviderManager.java | 24 +++++++++++ .../provider/AuthenticationProvider.java | 10 +++++ .../provider/UsernamePasswordProvider.java | 28 +++++++++++++ 10 files changed, 152 insertions(+), 45 deletions(-) create mode 100644 src/main/java/nextstep/security/DefaultSecurityFilterChain.java create mode 100644 src/main/java/nextstep/security/authentication/Authentication.java create mode 100644 src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java create mode 100644 src/main/java/nextstep/security/manager/AuthenticationManager.java create mode 100644 src/main/java/nextstep/security/manager/ProviderManager.java create mode 100644 src/main/java/nextstep/security/provider/AuthenticationProvider.java create mode 100644 src/main/java/nextstep/security/provider/UsernamePasswordProvider.java diff --git a/src/main/java/nextstep/app/config/SecurityConfig.java b/src/main/java/nextstep/app/config/SecurityConfig.java index 342ac71b..005feb6b 100644 --- a/src/main/java/nextstep/app/config/SecurityConfig.java +++ b/src/main/java/nextstep/app/config/SecurityConfig.java @@ -1,18 +1,18 @@ package nextstep.app.config; -import nextstep.security.SecurityFilterChain; +import nextstep.security.DefaultSecurityFilterChain; import nextstep.security.filter.AuthorizationFilter; import nextstep.security.filter.BasicAuthenticationFilter; import nextstep.security.filter.FilterChainProxy; import nextstep.security.filter.FormLoginAuthFilter; +import nextstep.security.manager.ProviderManager; +import nextstep.security.provider.UsernamePasswordProvider; import nextstep.security.service.UserDetailsService; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.GenericFilterBean; -import javax.servlet.Filter; -import javax.servlet.http.HttpServletRequest; import java.util.List; @Configuration @@ -27,34 +27,14 @@ public SecurityConfig(UserDetailsService userDetailsService) { @Bean public GenericFilterBean delegatingFilterProxy() { return new FilterChainProxy(List.of( - new SecurityFilterChain() { - @Override - public boolean matches(HttpServletRequest request) { - return request.getRequestURI().equals("/login"); - } - - @Override - public List getFilters() { - return List.of( - new FormLoginAuthFilter(userDetailsService), - new AuthorizationFilter() - ); - } - }, - new SecurityFilterChain() { - @Override - public boolean matches(HttpServletRequest request) { - return request.getRequestURI().startsWith("/members"); - } - - @Override - public List getFilters() { - return List.of( - new BasicAuthenticationFilter(userDetailsService), - new AuthorizationFilter() - ); - } - } + new DefaultSecurityFilterChain("/login", List.of( + new FormLoginAuthFilter(new ProviderManager(List.of(new UsernamePasswordProvider(userDetailsService)))), + new AuthorizationFilter() + )), + new DefaultSecurityFilterChain("/members", List.of( + new BasicAuthenticationFilter(new ProviderManager(List.of(new UsernamePasswordProvider(userDetailsService)))), + new AuthorizationFilter() + )) )); } diff --git a/src/main/java/nextstep/security/DefaultSecurityFilterChain.java b/src/main/java/nextstep/security/DefaultSecurityFilterChain.java new file mode 100644 index 00000000..9f65a41c --- /dev/null +++ b/src/main/java/nextstep/security/DefaultSecurityFilterChain.java @@ -0,0 +1,26 @@ +package nextstep.security; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +public class DefaultSecurityFilterChain implements SecurityFilterChain { + + private final String path; + private final List filters; + + public DefaultSecurityFilterChain(String path, List filters) { + this.path = path; + this.filters = filters; + } + + @Override + public boolean matches(HttpServletRequest request) { + return request.getRequestURI().startsWith(path); + } + + @Override + public List getFilters() { + return filters; + } +} 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..e409fbd1 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/Authentication.java @@ -0,0 +1,6 @@ +package nextstep.security.authentication; + +public interface Authentication { + Object getCredentials(); + Object getPrincipal(); +} 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..acc78e01 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java @@ -0,0 +1,21 @@ +package nextstep.security.authentication; + +public class UsernamePasswordAuthenticationToken implements Authentication { + private final Object principal; + private final Object credentials; + + public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { + this.principal = principal; + this.credentials = credentials; + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public Object getPrincipal() { + return principal; + } +} diff --git a/src/main/java/nextstep/security/filter/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/filter/BasicAuthenticationFilter.java index 3d818a54..bff3d96a 100644 --- a/src/main/java/nextstep/security/filter/BasicAuthenticationFilter.java +++ b/src/main/java/nextstep/security/filter/BasicAuthenticationFilter.java @@ -1,7 +1,9 @@ package nextstep.security.filter; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; import nextstep.security.context.UserContextHolder; -import nextstep.security.service.UserDetailsService; +import nextstep.security.manager.AuthenticationManager; import nextstep.security.userdetails.UserDetails; import org.springframework.http.HttpHeaders; import org.springframework.lang.Nullable; @@ -19,10 +21,10 @@ public class BasicAuthenticationFilter implements Filter { private static final String BLANK = " "; private static final String COLON = ":"; - private final UserDetailsService userDetailsService; + private final AuthenticationManager authenticationManager; - public BasicAuthenticationFilter(UserDetailsService userDetailsService) { - this.userDetailsService = userDetailsService; + public BasicAuthenticationFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; } @Override @@ -33,9 +35,9 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha return; } - UserDetails userDetails = userDetailsService.loadUserByEmailAndPassword(credentials[0], credentials[1]); - if (userDetails != null) { - UserContextHolder.setUser(userDetails); + Authentication authentication = this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(credentials[0], credentials[1])); + if (authentication != null) { + UserContextHolder.setUser((UserDetails) authentication.getPrincipal()); } chain.doFilter(request, response); } diff --git a/src/main/java/nextstep/security/filter/FormLoginAuthFilter.java b/src/main/java/nextstep/security/filter/FormLoginAuthFilter.java index 59686862..39f79bce 100644 --- a/src/main/java/nextstep/security/filter/FormLoginAuthFilter.java +++ b/src/main/java/nextstep/security/filter/FormLoginAuthFilter.java @@ -1,7 +1,9 @@ package nextstep.security.filter; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; import nextstep.security.context.UserContextHolder; -import nextstep.security.service.UserDetailsService; +import nextstep.security.manager.AuthenticationManager; import nextstep.security.userdetails.UserDetails; import javax.servlet.*; @@ -10,10 +12,10 @@ public class FormLoginAuthFilter implements Filter { - private final UserDetailsService userDetailsService; + private final AuthenticationManager authenticationManager; - public FormLoginAuthFilter(UserDetailsService userDetailsService) { - this.userDetailsService = userDetailsService; + public FormLoginAuthFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; } @Override @@ -22,9 +24,9 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha String email = paramMap.get("username")[0]; String password = paramMap.get("password")[0]; - UserDetails userDetails = userDetailsService.loadUserByEmailAndPassword(email, password); - if (userDetails != null) { - UserContextHolder.setUser(userDetails); + Authentication authentication = this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(email, password)); + if (authentication != null) { + UserContextHolder.setUser((UserDetails) authentication.getPrincipal()); } chain.doFilter(request, response); } diff --git a/src/main/java/nextstep/security/manager/AuthenticationManager.java b/src/main/java/nextstep/security/manager/AuthenticationManager.java new file mode 100644 index 00000000..3c8ad968 --- /dev/null +++ b/src/main/java/nextstep/security/manager/AuthenticationManager.java @@ -0,0 +1,8 @@ +package nextstep.security.manager; + +import nextstep.security.authentication.Authentication; + +public interface AuthenticationManager { + + Authentication authenticate(Authentication authentication); +} diff --git a/src/main/java/nextstep/security/manager/ProviderManager.java b/src/main/java/nextstep/security/manager/ProviderManager.java new file mode 100644 index 00000000..a2c25001 --- /dev/null +++ b/src/main/java/nextstep/security/manager/ProviderManager.java @@ -0,0 +1,24 @@ +package nextstep.security.manager; + +import nextstep.security.authentication.Authentication; +import nextstep.security.provider.AuthenticationProvider; + +import java.util.List; + +public class ProviderManager implements AuthenticationManager { + + private final List providers; + + public ProviderManager(List providers) { + this.providers = providers; + } + + @Override + public Authentication authenticate(Authentication authentication) { + return providers.stream() + .filter(v -> v.supports(authentication.getClass())) + .map(v -> v.authenticate(authentication)) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/nextstep/security/provider/AuthenticationProvider.java b/src/main/java/nextstep/security/provider/AuthenticationProvider.java new file mode 100644 index 00000000..ed4094ff --- /dev/null +++ b/src/main/java/nextstep/security/provider/AuthenticationProvider.java @@ -0,0 +1,10 @@ +package nextstep.security.provider; + +import nextstep.security.authentication.Authentication; + +public interface AuthenticationProvider { + + boolean supports(Class authentication); + + Authentication authenticate(Authentication authentication); +} diff --git a/src/main/java/nextstep/security/provider/UsernamePasswordProvider.java b/src/main/java/nextstep/security/provider/UsernamePasswordProvider.java new file mode 100644 index 00000000..9cc5a456 --- /dev/null +++ b/src/main/java/nextstep/security/provider/UsernamePasswordProvider.java @@ -0,0 +1,28 @@ +package nextstep.security.provider; + +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; +import nextstep.security.service.UserDetailsService; +import nextstep.security.userdetails.UserDetails; + +public class UsernamePasswordProvider implements AuthenticationProvider { + + private final UserDetailsService userDetailsService; + + public UsernamePasswordProvider(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public boolean supports(Class authentication) { + return authentication.isAssignableFrom(UsernamePasswordAuthenticationToken.class); + } + + @Override + public Authentication authenticate(Authentication authentication) { + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) authentication; + + UserDetails userDetails = userDetailsService.loadUserByEmailAndPassword((String) usernamePasswordAuthenticationToken.getPrincipal(), (String) usernamePasswordAuthenticationToken.getCredentials()); + return new UsernamePasswordAuthenticationToken(userDetails, null); + } +}