From 75c6bb65c504042210bd611d14752024c86db5e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=98=84?= Date: Sun, 3 Nov 2024 14:53:45 +0900 Subject: [PATCH 1/9] =?UTF-8?q?docs=20:=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=EB=B0=94=ED=83=95=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 | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 1e7ba652..454a7f56 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ # spring-security-authentication + +## 기능 요구 사항 + +1. 아이디 비밀번호 기반 로그인 기능 구현 +2. Basic 인증 및 사용자를 식별하는 기능 구현 + +## 구현 기능 목록 + +### 아이디 비밀번호 기반 로그인 기능 구현 + + 1.사용자가 입력한 아이디와 비밀번호를 바탕으로 사용자 정보를 읽어 온 후 인증 + 2.로그인 성공 시 Session에 인증 정보를 저장 + +### Basic 인증 구현 + + 1. Basic Token을 디코딩하는 기능 + 2. 디코딩된 내용을 바탕으로 사용자를 식별하는 기능 + +### 리팩토링 사항 + + 인증 로직과 서비스 로직 사이의 패키지 분리 + 패키지 사이의 의존성이 단반향으로 흐르도록 변경 + + \ No newline at end of file From 5f3d15e74ba245abf53b00b9b85db9eca443ba5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=98=84?= Date: Sun, 3 Nov 2024 14:57:14 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat=20:=20=20FormLoginInterceptor=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 username, password를 바탕으로 사용자를 식별하는 FormLoginInterceptor 구현 --- .../application/UserDetailServiceImpl.java | 31 +++++++++++ .../java/nextstep/app/config/WebConfig.java | 23 ++++++++ .../java/nextstep/app/ui/LoginController.java | 20 +------ .../FormLoginAuthInterceptor.java | 54 +++++++++++++++++++ .../exception}/AuthenticationException.java | 3 +- .../security/userdetail/UserDetail.java | 21 ++++++++ .../userdetail/UserDetailService.java | 6 +++ 7 files changed, 139 insertions(+), 19 deletions(-) create mode 100644 src/main/java/nextstep/app/application/UserDetailServiceImpl.java create mode 100644 src/main/java/nextstep/app/config/WebConfig.java create mode 100644 src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java rename src/main/java/nextstep/{app/ui => security/exception}/AuthenticationException.java (63%) create mode 100644 src/main/java/nextstep/security/userdetail/UserDetail.java create mode 100644 src/main/java/nextstep/security/userdetail/UserDetailService.java diff --git a/src/main/java/nextstep/app/application/UserDetailServiceImpl.java b/src/main/java/nextstep/app/application/UserDetailServiceImpl.java new file mode 100644 index 00000000..3400a761 --- /dev/null +++ b/src/main/java/nextstep/app/application/UserDetailServiceImpl.java @@ -0,0 +1,31 @@ +package nextstep.app.application; + +import java.util.Objects; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.security.userdetail.UserDetail; +import nextstep.security.userdetail.UserDetailService; +import org.springframework.stereotype.Service; + +@Service +public class UserDetailServiceImpl implements UserDetailService { + + private final MemberRepository memberRepository; + + public UserDetailServiceImpl(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public UserDetail getUserDetail(String username, String password) { + return memberRepository.findByEmail(username) + .filter(member -> Objects.equals(member.getPassword(), password)) + .map(this::convertToUserDetail) + .orElse(null); + } + + + public UserDetail convertToUserDetail(Member member) { + return new UserDetail(member.getEmail(), member.getPassword()); + } +} 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..3326f66d --- /dev/null +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -0,0 +1,23 @@ +package nextstep.app.config; + +import nextstep.security.authentication.FormLoginAuthInterceptor; +import nextstep.security.userdetail.UserDetailService; +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 FormLoginAuthInterceptor(userDetailService)) + .addPathPatterns("/login"); + } +} diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 0ea94f1b..00dd61ef 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,32 +1,16 @@ package nextstep.app.ui; -import nextstep.app.domain.MemberRepository; -import org.springframework.http.HttpStatus; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; 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) { return ResponseEntity.ok().build(); } - - @ExceptionHandler(AuthenticationException.class) - public ResponseEntity handleAuthenticationException() { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } } diff --git a/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java b/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java new file mode 100644 index 00000000..c8dec0e6 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java @@ -0,0 +1,54 @@ +package nextstep.security.authentication; + +import java.util.Objects; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import nextstep.security.exception.AuthenticationException; +import nextstep.security.userdetail.UserDetail; +import nextstep.security.userdetail.UserDetailService; +import org.springframework.util.ObjectUtils; +import org.springframework.web.servlet.HandlerInterceptor; + +public class FormLoginAuthInterceptor implements HandlerInterceptor { + + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + private final UserDetailService userDetailService; + + public FormLoginAuthInterceptor(UserDetailService userDetailService) { + this.userDetailService = userDetailService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + try { + HttpSession session = request.getSession(); + + if (session.getAttribute(SPRING_SECURITY_CONTEXT_KEY) != null) { + session.removeAttribute(SPRING_SECURITY_CONTEXT_KEY); + } + + String username = request.getParameter("username"); + String password = request.getParameter("password"); + + if (ObjectUtils.isEmpty(username) || ObjectUtils.isEmpty(password)) { + throw new AuthenticationException(); + } + + UserDetail userDetail = userDetailService.getUserDetail(username, password); + + if (Objects.isNull(userDetail)) { + throw new AuthenticationException(); + } + + request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetail); + + return true; + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + } +} diff --git a/src/main/java/nextstep/app/ui/AuthenticationException.java b/src/main/java/nextstep/security/exception/AuthenticationException.java similarity index 63% rename from src/main/java/nextstep/app/ui/AuthenticationException.java rename to src/main/java/nextstep/security/exception/AuthenticationException.java index f809b6e4..63a51665 100644 --- a/src/main/java/nextstep/app/ui/AuthenticationException.java +++ b/src/main/java/nextstep/security/exception/AuthenticationException.java @@ -1,4 +1,5 @@ -package nextstep.app.ui; +package nextstep.security.exception; public class AuthenticationException extends RuntimeException { + } diff --git a/src/main/java/nextstep/security/userdetail/UserDetail.java b/src/main/java/nextstep/security/userdetail/UserDetail.java new file mode 100644 index 00000000..0f9dbdc1 --- /dev/null +++ b/src/main/java/nextstep/security/userdetail/UserDetail.java @@ -0,0 +1,21 @@ +package nextstep.security.userdetail; + +public class UserDetail { + + private final String username; + + private final String password; + + public UserDetail(String username, String password) { + this.username = username; + this.password = password; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } +} diff --git a/src/main/java/nextstep/security/userdetail/UserDetailService.java b/src/main/java/nextstep/security/userdetail/UserDetailService.java new file mode 100644 index 00000000..abc2c90b --- /dev/null +++ b/src/main/java/nextstep/security/userdetail/UserDetailService.java @@ -0,0 +1,6 @@ +package nextstep.security.userdetail; + +public interface UserDetailService { + + UserDetail getUserDetail(String username, String password); +} From 94741fb42ae11e8c8e962a6fa544cd7a45302981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=98=84?= Date: Sun, 3 Nov 2024 14:58:10 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat=20:=20BasicTokenDecoder=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 Basic Token을 디코딩하여 사용자 정보를 가져오는 BasicTokenDecoder 구현 --- .../nextstep/app/util/BasicTokenDecoder.java | 39 +++++++++++++++++++ .../security/authentication/TokenDecoder.java | 8 ++++ 2 files changed, 47 insertions(+) create mode 100644 src/main/java/nextstep/app/util/BasicTokenDecoder.java create mode 100644 src/main/java/nextstep/security/authentication/TokenDecoder.java diff --git a/src/main/java/nextstep/app/util/BasicTokenDecoder.java b/src/main/java/nextstep/app/util/BasicTokenDecoder.java new file mode 100644 index 00000000..cde77bc1 --- /dev/null +++ b/src/main/java/nextstep/app/util/BasicTokenDecoder.java @@ -0,0 +1,39 @@ +package nextstep.app.util; + +import java.nio.charset.StandardCharsets; +import nextstep.security.authentication.TokenDecoder; +import nextstep.security.exception.AuthenticationException; +import nextstep.security.userdetail.UserDetail; +import org.springframework.stereotype.Component; +import org.springframework.util.Base64Utils; + +@Component +public class BasicTokenDecoder implements TokenDecoder { + + public static final String BASIC_TOKEN_PREFIX = "Basic "; + + @Override + public UserDetail decodeToken(String token) { + String base64Token = token.substring(BASIC_TOKEN_PREFIX.length()); + String decodedToken = new String(Base64Utils.decodeFromString(base64Token), + StandardCharsets.UTF_8); + + validateBasicToken(token); + + String[] parts = decodedToken.split(":"); + if (parts.length != 2) { + throw new AuthenticationException(); + } + return new UserDetail(parts[0], parts[1]); + } + + private void validateBasicToken(String authorization) { + if (authorization == null) { + throw new AuthenticationException(); + } + + if (!authorization.startsWith(BASIC_TOKEN_PREFIX)) { + throw new AuthenticationException(); + } + } +} diff --git a/src/main/java/nextstep/security/authentication/TokenDecoder.java b/src/main/java/nextstep/security/authentication/TokenDecoder.java new file mode 100644 index 00000000..bfca4fdd --- /dev/null +++ b/src/main/java/nextstep/security/authentication/TokenDecoder.java @@ -0,0 +1,8 @@ +package nextstep.security.authentication; + +import nextstep.security.userdetail.UserDetail; + +public interface TokenDecoder { + + UserDetail decodeToken(String token); +} From 7a887357bbdc9f7ab125ae67105fe49b2b3a7610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=98=84?= Date: Sun, 3 Nov 2024 14:59:05 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat=20:=20BasicAuthInterceptor=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 Authorization Header의 Basic 토큰을 가져와 사용자를 식별하는 BasicAuthInterceptor 구현 --- .../java/nextstep/app/config/WebConfig.java | 8 +++- .../authentication/BasicAuthInterceptor.java | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index 3326f66d..ff21c0da 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -1,6 +1,8 @@ package nextstep.app.config; +import nextstep.security.authentication.BasicAuthInterceptor; import nextstep.security.authentication.FormLoginAuthInterceptor; +import nextstep.security.authentication.TokenDecoder; import nextstep.security.userdetail.UserDetailService; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; @@ -10,14 +12,18 @@ public class WebConfig implements WebMvcConfigurer { private final UserDetailService userDetailService; + private final TokenDecoder tokenDecoder; - public WebConfig(UserDetailService userDetailService) { + public WebConfig(UserDetailService userDetailService, TokenDecoder tokenDecoder) { this.userDetailService = userDetailService; + this.tokenDecoder = tokenDecoder; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new FormLoginAuthInterceptor(userDetailService)) .addPathPatterns("/login"); + registry.addInterceptor(new BasicAuthInterceptor(tokenDecoder, userDetailService)) + .addPathPatterns("/members"); } } diff --git a/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java b/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java new file mode 100644 index 00000000..390f190b --- /dev/null +++ b/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java @@ -0,0 +1,41 @@ +package nextstep.security.authentication; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import nextstep.security.userdetail.UserDetail; +import nextstep.security.userdetail.UserDetailService; +import org.springframework.http.HttpHeaders; +import org.springframework.web.servlet.HandlerInterceptor; + +public class BasicAuthInterceptor implements HandlerInterceptor { + + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + private final TokenDecoder tokenDecoder; + private final UserDetailService userDetailService; + + + public BasicAuthInterceptor(TokenDecoder tokenDecoder, UserDetailService userDetailService) { + this.tokenDecoder = tokenDecoder; + this.userDetailService = userDetailService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + try { + String token = request.getHeader(HttpHeaders.AUTHORIZATION); + + UserDetail decodedUserInfo = tokenDecoder.decodeToken(token); + + UserDetail userDetail = userDetailService.getUserDetail(decodedUserInfo.getUsername(), + decodedUserInfo.getPassword()); + + request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetail); + return true; + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + } +} From f0c36e864f1639ced3b69082d68b6c7a14547947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=98=84?= Date: Sun, 3 Nov 2024 15:19:57 +0900 Subject: [PATCH 5/9] =?UTF-8?q?refactor=20:=20Security=EC=9A=A9=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=20=EC=83=81=EC=88=98=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Basic Token Prefix, Context_key 등 여러 클래스에서 사용하는 상수들을 하나의 클래스에서 관리할 수 있도록 변경 --- src/main/java/nextstep/app/util/BasicTokenDecoder.java | 4 ++-- .../security/authentication/BasicAuthInterceptor.java | 5 +++-- .../security/authentication/FormLoginAuthInterceptor.java | 4 ++-- .../security/authentication/SecurityConstants.java | 8 ++++++++ 4 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 src/main/java/nextstep/security/authentication/SecurityConstants.java diff --git a/src/main/java/nextstep/app/util/BasicTokenDecoder.java b/src/main/java/nextstep/app/util/BasicTokenDecoder.java index cde77bc1..934b63ae 100644 --- a/src/main/java/nextstep/app/util/BasicTokenDecoder.java +++ b/src/main/java/nextstep/app/util/BasicTokenDecoder.java @@ -1,5 +1,7 @@ package nextstep.app.util; +import static nextstep.security.authentication.SecurityConstants.BASIC_TOKEN_PREFIX; + import java.nio.charset.StandardCharsets; import nextstep.security.authentication.TokenDecoder; import nextstep.security.exception.AuthenticationException; @@ -10,8 +12,6 @@ @Component public class BasicTokenDecoder implements TokenDecoder { - public static final String BASIC_TOKEN_PREFIX = "Basic "; - @Override public UserDetail decodeToken(String token) { String base64Token = token.substring(BASIC_TOKEN_PREFIX.length()); diff --git a/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java b/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java index 390f190b..340d66da 100644 --- a/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java +++ b/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java @@ -1,7 +1,10 @@ package nextstep.security.authentication; +import static nextstep.security.authentication.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import nextstep.security.exception.AuthenticationException; import nextstep.security.userdetail.UserDetail; import nextstep.security.userdetail.UserDetailService; import org.springframework.http.HttpHeaders; @@ -9,8 +12,6 @@ public class BasicAuthInterceptor implements HandlerInterceptor { - public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - private final TokenDecoder tokenDecoder; private final UserDetailService userDetailService; diff --git a/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java b/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java index c8dec0e6..d1431d7b 100644 --- a/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java +++ b/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java @@ -1,5 +1,7 @@ package nextstep.security.authentication; +import static nextstep.security.authentication.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; + import java.util.Objects; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -12,8 +14,6 @@ public class FormLoginAuthInterceptor implements HandlerInterceptor { - public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - private final UserDetailService userDetailService; public FormLoginAuthInterceptor(UserDetailService userDetailService) { diff --git a/src/main/java/nextstep/security/authentication/SecurityConstants.java b/src/main/java/nextstep/security/authentication/SecurityConstants.java new file mode 100644 index 00000000..0e29cba2 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/SecurityConstants.java @@ -0,0 +1,8 @@ +package nextstep.security.authentication; + +public class SecurityConstants { + + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + public static final String BASIC_TOKEN_PREFIX = "Basic "; +} From fd4fcab4f6eab2f46341b2191b46333d761c77f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=98=84?= Date: Sun, 3 Nov 2024 15:21:27 +0900 Subject: [PATCH 6/9] =?UTF-8?q?refactor=20:=20=EC=9D=B8=EC=A6=9D=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20app=20->=20security=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit userDetailService의 구현체에서 관리하던 사용자 인증 로직을 security 하위 패키지에 있는 클래스들이 사용하도록 변경 refactor : 잘못된 줄바꿈 수정 불필요한 줄바꿈 제거 --- .../nextstep/app/application/UserDetailServiceImpl.java | 5 +---- .../security/authentication/BasicAuthInterceptor.java | 7 +++++-- .../security/authentication/FormLoginAuthInterceptor.java | 6 +++++- src/main/java/nextstep/security/userdetail/UserDetail.java | 4 ++++ .../nextstep/security/userdetail/UserDetailService.java | 2 +- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/nextstep/app/application/UserDetailServiceImpl.java b/src/main/java/nextstep/app/application/UserDetailServiceImpl.java index 3400a761..4eaaa4ba 100644 --- a/src/main/java/nextstep/app/application/UserDetailServiceImpl.java +++ b/src/main/java/nextstep/app/application/UserDetailServiceImpl.java @@ -1,6 +1,5 @@ package nextstep.app.application; -import java.util.Objects; import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; import nextstep.security.userdetail.UserDetail; @@ -17,14 +16,12 @@ public UserDetailServiceImpl(MemberRepository memberRepository) { } @Override - public UserDetail getUserDetail(String username, String password) { + public UserDetail getUserDetail(String username) { return memberRepository.findByEmail(username) - .filter(member -> Objects.equals(member.getPassword(), password)) .map(this::convertToUserDetail) .orElse(null); } - public UserDetail convertToUserDetail(Member member) { return new UserDetail(member.getEmail(), member.getPassword()); } diff --git a/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java b/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java index 340d66da..f3c46602 100644 --- a/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java +++ b/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java @@ -29,8 +29,11 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons UserDetail decodedUserInfo = tokenDecoder.decodeToken(token); - UserDetail userDetail = userDetailService.getUserDetail(decodedUserInfo.getUsername(), - decodedUserInfo.getPassword()); + UserDetail userDetail = userDetailService.getUserDetail(decodedUserInfo.getUsername()); + + if (!userDetail.verifyPassword(decodedUserInfo.getPassword())) { + throw new AuthenticationException(); + } request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetail); return true; diff --git a/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java b/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java index d1431d7b..62561170 100644 --- a/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java +++ b/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java @@ -37,12 +37,16 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons throw new AuthenticationException(); } - UserDetail userDetail = userDetailService.getUserDetail(username, password); + UserDetail userDetail = userDetailService.getUserDetail(username); if (Objects.isNull(userDetail)) { throw new AuthenticationException(); } + if (!userDetail.verifyPassword(password)) { + throw new AuthenticationException(); + } + request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetail); return true; diff --git a/src/main/java/nextstep/security/userdetail/UserDetail.java b/src/main/java/nextstep/security/userdetail/UserDetail.java index 0f9dbdc1..145058ab 100644 --- a/src/main/java/nextstep/security/userdetail/UserDetail.java +++ b/src/main/java/nextstep/security/userdetail/UserDetail.java @@ -18,4 +18,8 @@ public String getUsername() { public String getPassword() { return password; } + + public boolean verifyPassword(String password) { + return this.password.equals(password); + } } diff --git a/src/main/java/nextstep/security/userdetail/UserDetailService.java b/src/main/java/nextstep/security/userdetail/UserDetailService.java index abc2c90b..1a6ea929 100644 --- a/src/main/java/nextstep/security/userdetail/UserDetailService.java +++ b/src/main/java/nextstep/security/userdetail/UserDetailService.java @@ -2,5 +2,5 @@ public interface UserDetailService { - UserDetail getUserDetail(String username, String password); + UserDetail getUserDetail(String username); } From fccac9db88f0ba091c638242796c63a8e44e1447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=98=84?= Date: Sun, 3 Nov 2024 15:32:33 +0900 Subject: [PATCH 7/9] =?UTF-8?q?refactor=20:=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=EA=B3=BC=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=EB=A1=9C=EC=A7=81=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FormLoginAuthInterceptor의 parameter 정보와 세션정보를 검증하는 로직과 사용자가 맞는지 인증하는 로직을 별도의 메서드 분리 feat : SecurityConstants 빈 생성자 추가 해당 클래스가 오로지 상수용 클래스로만 사용될 수 있도록 private 빈 생성자 추가 --- .../authentication/BasicAuthInterceptor.java | 1 - .../FormLoginAuthInterceptor.java | 45 +++++++++++-------- .../authentication/SecurityConstants.java | 3 ++ 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java b/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java index f3c46602..bb99a3a5 100644 --- a/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java +++ b/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java @@ -28,7 +28,6 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons String token = request.getHeader(HttpHeaders.AUTHORIZATION); UserDetail decodedUserInfo = tokenDecoder.decodeToken(token); - UserDetail userDetail = userDetailService.getUserDetail(decodedUserInfo.getUsername()); if (!userDetail.verifyPassword(decodedUserInfo.getPassword())) { diff --git a/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java b/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java index 62561170..cb5d9256 100644 --- a/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java +++ b/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java @@ -24,29 +24,13 @@ public FormLoginAuthInterceptor(UserDetailService userDetailService) { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { try { - HttpSession session = request.getSession(); - - if (session.getAttribute(SPRING_SECURITY_CONTEXT_KEY) != null) { - session.removeAttribute(SPRING_SECURITY_CONTEXT_KEY); - } + validateParamAndSession(request); String username = request.getParameter("username"); String password = request.getParameter("password"); - if (ObjectUtils.isEmpty(username) || ObjectUtils.isEmpty(password)) { - throw new AuthenticationException(); - } - UserDetail userDetail = userDetailService.getUserDetail(username); - - if (Objects.isNull(userDetail)) { - throw new AuthenticationException(); - } - - if (!userDetail.verifyPassword(password)) { - throw new AuthenticationException(); - } - + verifyUserDetail(userDetail, password); request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetail); return true; @@ -55,4 +39,29 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons return false; } } + + private void validateParamAndSession(HttpServletRequest request) { + HttpSession session = request.getSession(); + + String username = request.getParameter("username"); + String password = request.getParameter("password"); + + if (session.getAttribute(SPRING_SECURITY_CONTEXT_KEY) != null) { + session.removeAttribute(SPRING_SECURITY_CONTEXT_KEY); + } + + if (ObjectUtils.isEmpty(username) || ObjectUtils.isEmpty(password)) { + throw new AuthenticationException(); + } + } + + private void verifyUserDetail(UserDetail userDetail, String password) { + if (Objects.isNull(userDetail)) { + throw new AuthenticationException(); + } + + if (!userDetail.verifyPassword(password)) { + throw new AuthenticationException(); + } + } } diff --git a/src/main/java/nextstep/security/authentication/SecurityConstants.java b/src/main/java/nextstep/security/authentication/SecurityConstants.java index 0e29cba2..4c7b9f93 100644 --- a/src/main/java/nextstep/security/authentication/SecurityConstants.java +++ b/src/main/java/nextstep/security/authentication/SecurityConstants.java @@ -2,6 +2,9 @@ public class SecurityConstants { + private SecurityConstants() { + } + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; public static final String BASIC_TOKEN_PREFIX = "Basic "; From 4fa20530e92fd249b9b79a9738e494dc7967313c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=98=84?= Date: Sun, 3 Nov 2024 15:37:49 +0900 Subject: [PATCH 8/9] =?UTF-8?q?refactor=20:=20Decoder=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20Component=20Scan?= =?UTF-8?q?=20=EB=B2=94=EC=9C=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BasicTokenDecoder가 app이 아닌 sercurity 패키지에 속하도록 변경하고 컴포넌트 스캔 범위를 app, security 패키지를 사용할 수 있게 SecurityAuthenticationApplication 위치 변경 --- .../{app => }/SecurityAuthenticationApplication.java | 2 +- src/main/java/nextstep/app/config/WebConfig.java | 2 +- .../security/authentication/BasicAuthInterceptor.java | 3 ++- .../security/authentication/FormLoginAuthInterceptor.java | 2 +- .../nextstep/{app => security}/util/BasicTokenDecoder.java | 5 ++--- .../security/{authentication => util}/SecurityConstants.java | 2 +- .../security/{authentication => util}/TokenDecoder.java | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) rename src/main/java/nextstep/{app => }/SecurityAuthenticationApplication.java (93%) rename src/main/java/nextstep/{app => security}/util/BasicTokenDecoder.java (86%) rename src/main/java/nextstep/security/{authentication => util}/SecurityConstants.java (84%) rename src/main/java/nextstep/security/{authentication => util}/TokenDecoder.java (75%) diff --git a/src/main/java/nextstep/app/SecurityAuthenticationApplication.java b/src/main/java/nextstep/SecurityAuthenticationApplication.java similarity index 93% rename from src/main/java/nextstep/app/SecurityAuthenticationApplication.java rename to src/main/java/nextstep/SecurityAuthenticationApplication.java index 0f8eb47d..1ecd05fe 100644 --- a/src/main/java/nextstep/app/SecurityAuthenticationApplication.java +++ b/src/main/java/nextstep/SecurityAuthenticationApplication.java @@ -1,4 +1,4 @@ -package nextstep.app; +package nextstep; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java index ff21c0da..70ab41c7 100644 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -2,8 +2,8 @@ import nextstep.security.authentication.BasicAuthInterceptor; import nextstep.security.authentication.FormLoginAuthInterceptor; -import nextstep.security.authentication.TokenDecoder; import nextstep.security.userdetail.UserDetailService; +import nextstep.security.util.TokenDecoder; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; diff --git a/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java b/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java index bb99a3a5..14284abe 100644 --- a/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java +++ b/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java @@ -1,12 +1,13 @@ package nextstep.security.authentication; -import static nextstep.security.authentication.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; +import static nextstep.security.util.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import nextstep.security.exception.AuthenticationException; import nextstep.security.userdetail.UserDetail; import nextstep.security.userdetail.UserDetailService; +import nextstep.security.util.TokenDecoder; import org.springframework.http.HttpHeaders; import org.springframework.web.servlet.HandlerInterceptor; diff --git a/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java b/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java index cb5d9256..6692a9bf 100644 --- a/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java +++ b/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java @@ -1,6 +1,6 @@ package nextstep.security.authentication; -import static nextstep.security.authentication.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; +import static nextstep.security.util.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; import java.util.Objects; import javax.servlet.http.HttpServletRequest; diff --git a/src/main/java/nextstep/app/util/BasicTokenDecoder.java b/src/main/java/nextstep/security/util/BasicTokenDecoder.java similarity index 86% rename from src/main/java/nextstep/app/util/BasicTokenDecoder.java rename to src/main/java/nextstep/security/util/BasicTokenDecoder.java index 934b63ae..68da04cd 100644 --- a/src/main/java/nextstep/app/util/BasicTokenDecoder.java +++ b/src/main/java/nextstep/security/util/BasicTokenDecoder.java @@ -1,9 +1,8 @@ -package nextstep.app.util; +package nextstep.security.util; -import static nextstep.security.authentication.SecurityConstants.BASIC_TOKEN_PREFIX; +import static nextstep.security.util.SecurityConstants.BASIC_TOKEN_PREFIX; import java.nio.charset.StandardCharsets; -import nextstep.security.authentication.TokenDecoder; import nextstep.security.exception.AuthenticationException; import nextstep.security.userdetail.UserDetail; import org.springframework.stereotype.Component; diff --git a/src/main/java/nextstep/security/authentication/SecurityConstants.java b/src/main/java/nextstep/security/util/SecurityConstants.java similarity index 84% rename from src/main/java/nextstep/security/authentication/SecurityConstants.java rename to src/main/java/nextstep/security/util/SecurityConstants.java index 4c7b9f93..44660643 100644 --- a/src/main/java/nextstep/security/authentication/SecurityConstants.java +++ b/src/main/java/nextstep/security/util/SecurityConstants.java @@ -1,4 +1,4 @@ -package nextstep.security.authentication; +package nextstep.security.util; public class SecurityConstants { diff --git a/src/main/java/nextstep/security/authentication/TokenDecoder.java b/src/main/java/nextstep/security/util/TokenDecoder.java similarity index 75% rename from src/main/java/nextstep/security/authentication/TokenDecoder.java rename to src/main/java/nextstep/security/util/TokenDecoder.java index bfca4fdd..522018df 100644 --- a/src/main/java/nextstep/security/authentication/TokenDecoder.java +++ b/src/main/java/nextstep/security/util/TokenDecoder.java @@ -1,4 +1,4 @@ -package nextstep.security.authentication; +package nextstep.security.util; import nextstep.security.userdetail.UserDetail; From 902289ccc53c805f8600a16ad1da363756f430cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=ED=98=84?= Date: Tue, 5 Nov 2024 10:07:17 +0900 Subject: [PATCH 9/9] =?UTF-8?q?refactor=20:=20Interceptor=EB=A5=BC=20Filte?= =?UTF-8?q?r=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Interceptor -> filter 변경 인증 객체 Authentication을 사용하여 인증하도록 변경 Authentrication Manager와 Provider를 사용하여 필요한 인증방식을 사용하도록 변경 --- .../java/nextstep/app/config/WebConfig.java | 29 ------- .../authentication/Authentication.java | 10 +++ .../authentication/AuthenticationManager.java | 6 ++ .../AuthenticationProvider.java | 10 +++ .../authentication/BasicAuthInterceptor.java | 45 ---------- .../DaoAuthenticationProvider.java | 31 +++++++ .../FormLoginAuthInterceptor.java | 67 --------------- .../authentication/ProviderManager.java | 22 +++++ .../UsernamePasswordAuthenticationToken.java | 40 +++++++++ .../filter/BasicAuthFilter.java | 45 ++++++++++ .../filter/FormLoginAuthFilter.java | 83 +++++++++++++++++++ .../config/DefaultSecurityFilterChain.java | 24 ++++++ .../config/DelegatingFilterProxy.java | 24 ++++++ .../security/config/FilterChainProxy.java | 73 ++++++++++++++++ .../security/config/SecurityConfig.java | 49 +++++++++++ .../security/config/SecurityFilterChain.java | 12 +++ .../security/util/SecurityConstants.java | 4 + 17 files changed, 433 insertions(+), 141 deletions(-) delete mode 100644 src/main/java/nextstep/app/config/WebConfig.java 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 delete mode 100644 src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java create mode 100644 src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java delete mode 100644 src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java create mode 100644 src/main/java/nextstep/security/authentication/ProviderManager.java create mode 100644 src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java create mode 100644 src/main/java/nextstep/security/authentication/filter/BasicAuthFilter.java create mode 100644 src/main/java/nextstep/security/authentication/filter/FormLoginAuthFilter.java create mode 100644 src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java create mode 100644 src/main/java/nextstep/security/config/DelegatingFilterProxy.java create mode 100644 src/main/java/nextstep/security/config/FilterChainProxy.java create mode 100644 src/main/java/nextstep/security/config/SecurityConfig.java create mode 100644 src/main/java/nextstep/security/config/SecurityFilterChain.java diff --git a/src/main/java/nextstep/app/config/WebConfig.java b/src/main/java/nextstep/app/config/WebConfig.java deleted file mode 100644 index 70ab41c7..00000000 --- a/src/main/java/nextstep/app/config/WebConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package nextstep.app.config; - -import nextstep.security.authentication.BasicAuthInterceptor; -import nextstep.security.authentication.FormLoginAuthInterceptor; -import nextstep.security.userdetail.UserDetailService; -import nextstep.security.util.TokenDecoder; -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; - private final TokenDecoder tokenDecoder; - - public WebConfig(UserDetailService userDetailService, TokenDecoder tokenDecoder) { - this.userDetailService = userDetailService; - this.tokenDecoder = tokenDecoder; - } - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new FormLoginAuthInterceptor(userDetailService)) - .addPathPatterns("/login"); - registry.addInterceptor(new BasicAuthInterceptor(tokenDecoder, userDetailService)) - .addPathPatterns("/members"); - } -} 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..ed153308 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/Authentication.java @@ -0,0 +1,10 @@ +package nextstep.security.authentication; + +public interface Authentication { + + Object getCredentials(); + + Object getPrincipal(); + + 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..17fc6760 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationManager.java @@ -0,0 +1,6 @@ +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..2f8ddf25 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java @@ -0,0 +1,10 @@ +package nextstep.security.authentication; + +import nextstep.security.exception.AuthenticationException; + +public interface AuthenticationProvider { + + Authentication authenticate(Authentication authentication) throws AuthenticationException; + + boolean supports(Class authentication); +} diff --git a/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java b/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java deleted file mode 100644 index 14284abe..00000000 --- a/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java +++ /dev/null @@ -1,45 +0,0 @@ -package nextstep.security.authentication; - -import static nextstep.security.util.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import nextstep.security.exception.AuthenticationException; -import nextstep.security.userdetail.UserDetail; -import nextstep.security.userdetail.UserDetailService; -import nextstep.security.util.TokenDecoder; -import org.springframework.http.HttpHeaders; -import org.springframework.web.servlet.HandlerInterceptor; - -public class BasicAuthInterceptor implements HandlerInterceptor { - - private final TokenDecoder tokenDecoder; - private final UserDetailService userDetailService; - - - public BasicAuthInterceptor(TokenDecoder tokenDecoder, UserDetailService userDetailService) { - this.tokenDecoder = tokenDecoder; - this.userDetailService = userDetailService; - } - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, - Object handler) throws Exception { - try { - String token = request.getHeader(HttpHeaders.AUTHORIZATION); - - UserDetail decodedUserInfo = tokenDecoder.decodeToken(token); - UserDetail userDetail = userDetailService.getUserDetail(decodedUserInfo.getUsername()); - - if (!userDetail.verifyPassword(decodedUserInfo.getPassword())) { - throw new AuthenticationException(); - } - - request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetail); - return true; - } catch (Exception e) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return false; - } - } -} diff --git a/src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java b/src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java new file mode 100644 index 00000000..1ea1638c --- /dev/null +++ b/src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java @@ -0,0 +1,31 @@ +package nextstep.security.authentication; + +import nextstep.security.exception.AuthenticationException; +import nextstep.security.userdetail.UserDetail; +import nextstep.security.userdetail.UserDetailService; + +public class DaoAuthenticationProvider implements AuthenticationProvider { + + private final UserDetailService userDetailService; + + public DaoAuthenticationProvider(UserDetailService userDetailService) { + this.userDetailService = userDetailService; + } + + @Override + public Authentication authenticate(Authentication authentication) + throws AuthenticationException { + UserDetail userDetail = userDetailService.getUserDetail( + authentication.getPrincipal().toString()); + if (userDetail.verifyPassword(authentication.getCredentials().toString())) { + throw new AuthenticationException(); + } + return UsernamePasswordAuthenticationToken.authenticated(userDetail.getUsername(), + userDetail.getPassword()); + } + + @Override + public boolean supports(Class authentication) { + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java b/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java deleted file mode 100644 index 6692a9bf..00000000 --- a/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java +++ /dev/null @@ -1,67 +0,0 @@ -package nextstep.security.authentication; - -import static nextstep.security.util.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; - -import java.util.Objects; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import nextstep.security.exception.AuthenticationException; -import nextstep.security.userdetail.UserDetail; -import nextstep.security.userdetail.UserDetailService; -import org.springframework.util.ObjectUtils; -import org.springframework.web.servlet.HandlerInterceptor; - -public class FormLoginAuthInterceptor implements HandlerInterceptor { - - private final UserDetailService userDetailService; - - public FormLoginAuthInterceptor(UserDetailService userDetailService) { - this.userDetailService = userDetailService; - } - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, - Object handler) throws Exception { - try { - validateParamAndSession(request); - - String username = request.getParameter("username"); - String password = request.getParameter("password"); - - UserDetail userDetail = userDetailService.getUserDetail(username); - verifyUserDetail(userDetail, password); - request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetail); - - return true; - } catch (Exception e) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return false; - } - } - - private void validateParamAndSession(HttpServletRequest request) { - HttpSession session = request.getSession(); - - String username = request.getParameter("username"); - String password = request.getParameter("password"); - - if (session.getAttribute(SPRING_SECURITY_CONTEXT_KEY) != null) { - session.removeAttribute(SPRING_SECURITY_CONTEXT_KEY); - } - - if (ObjectUtils.isEmpty(username) || ObjectUtils.isEmpty(password)) { - throw new AuthenticationException(); - } - } - - private void verifyUserDetail(UserDetail userDetail, String password) { - if (Objects.isNull(userDetail)) { - throw new AuthenticationException(); - } - - if (!userDetail.verifyPassword(password)) { - throw new AuthenticationException(); - } - } -} diff --git a/src/main/java/nextstep/security/authentication/ProviderManager.java b/src/main/java/nextstep/security/authentication/ProviderManager.java new file mode 100644 index 00000000..b6e40e42 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/ProviderManager.java @@ -0,0 +1,22 @@ +package nextstep.security.authentication; + +import java.util.List; + +public class ProviderManager implements AuthenticationManager { + + private final List authenticationProviders; + + public ProviderManager(List authenticationProviders) { + this.authenticationProviders = authenticationProviders; + } + + @Override + public Authentication authenticate(Authentication authentication) { + for (AuthenticationProvider provider : authenticationProviders) { + if (provider.supports(authentication.getClass())) { + return provider.authenticate(authentication); + } + } + return null; + } +} diff --git a/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java new file mode 100644 index 00000000..ad983974 --- /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; + + public UsernamePasswordAuthenticationToken(Object principal, Object credentials, + boolean authenticated) { + this.principal = principal; + this.credentials = credentials; + this.authenticated = authenticated; + } + + public static UsernamePasswordAuthenticationToken unauthenticated(String principal, + String credentials) { + return new UsernamePasswordAuthenticationToken(principal, credentials, false); + } + + public static UsernamePasswordAuthenticationToken authenticated(String principal, + String credentials) { + return new UsernamePasswordAuthenticationToken(principal, credentials, true); + } + + @Override + public Object getCredentials() { + return this.credentials; + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + @Override + public boolean isAuthenticated() { + return this.authenticated; + } +} diff --git a/src/main/java/nextstep/security/authentication/filter/BasicAuthFilter.java b/src/main/java/nextstep/security/authentication/filter/BasicAuthFilter.java new file mode 100644 index 00000000..9e3d08b9 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/filter/BasicAuthFilter.java @@ -0,0 +1,45 @@ +package nextstep.security.authentication.filter; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; +import nextstep.security.userdetail.UserDetail; +import nextstep.security.util.TokenDecoder; +import org.springframework.http.HttpHeaders; +import org.springframework.web.filter.OncePerRequestFilter; + +public class BasicAuthFilter extends OncePerRequestFilter { + + private final TokenDecoder tokenDecoder; + private final AuthenticationManager authenticationManager; + + public BasicAuthFilter(TokenDecoder tokenDecoder, AuthenticationManager authenticationManager) { + this.tokenDecoder = tokenDecoder; + this.authenticationManager = authenticationManager; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + try { + + String token = request.getHeader(HttpHeaders.AUTHORIZATION); + + UserDetail decodedUserInfo = tokenDecoder.decodeToken(token); + + UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken.unauthenticated( + decodedUserInfo.getUsername(), decodedUserInfo.getPassword()); + + authenticationManager.authenticate(authentication); + + filterChain.doFilter(request, response); + + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } +} diff --git a/src/main/java/nextstep/security/authentication/filter/FormLoginAuthFilter.java b/src/main/java/nextstep/security/authentication/filter/FormLoginAuthFilter.java new file mode 100644 index 00000000..9b387c9c --- /dev/null +++ b/src/main/java/nextstep/security/authentication/filter/FormLoginAuthFilter.java @@ -0,0 +1,83 @@ +package nextstep.security.authentication.filter; + +import static nextstep.security.util.SecurityConstants.LOGIN_REQUEST_URI; +import static nextstep.security.util.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; + +import java.io.IOException; +import java.util.Objects; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; +import nextstep.security.exception.AuthenticationException; +import nextstep.security.userdetail.UserDetail; +import org.springframework.util.ObjectUtils; +import org.springframework.web.filter.GenericFilterBean; + +public class FormLoginAuthFilter extends GenericFilterBean { + + private final AuthenticationManager authenticationManager; + + public FormLoginAuthFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) throws IOException, ServletException { + try { + var request = (HttpServletRequest) servletRequest; + + if (!Objects.equals(request.getRequestURI(), LOGIN_REQUEST_URI)) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + validateParamAndSession(request); + + String username = request.getParameter("username"); + String password = request.getParameter("password"); + + UsernamePasswordAuthenticationToken authentication = UsernamePasswordAuthenticationToken.unauthenticated( + username, password); + + Authentication authenticate = authenticationManager.authenticate(authentication); + + filterChain.doFilter(servletRequest, servletResponse); + } catch (Exception e) { + var response = (HttpServletResponse) servletResponse; + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } + + private void validateParamAndSession(HttpServletRequest request) { + HttpSession session = request.getSession(); + + String username = request.getParameter("username"); + String password = request.getParameter("password"); + + if (session.getAttribute(SPRING_SECURITY_CONTEXT_KEY) != null) { + session.removeAttribute(SPRING_SECURITY_CONTEXT_KEY); + } + + if (ObjectUtils.isEmpty(username) || ObjectUtils.isEmpty(password)) { + throw new AuthenticationException(); + } + } + + private void verifyUserDetail(UserDetail userDetail, String password) { + if (Objects.isNull(userDetail)) { + throw new AuthenticationException(); + } + + if (!userDetail.verifyPassword(password)) { + throw new AuthenticationException(); + } + } +} diff --git a/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java b/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java new file mode 100644 index 00000000..d1d2d112 --- /dev/null +++ b/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java @@ -0,0 +1,24 @@ +package nextstep.security.config; + +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; + +public class DefaultSecurityFilterChain implements SecurityFilterChain { + + private final List filters; + + public DefaultSecurityFilterChain(List filters) { + this.filters = filters; + } + + @Override + public boolean matches(HttpServletRequest request) { + return true; + } + + @Override + public List getFilters() { + return filters; + } +} diff --git a/src/main/java/nextstep/security/config/DelegatingFilterProxy.java b/src/main/java/nextstep/security/config/DelegatingFilterProxy.java new file mode 100644 index 00000000..65be0e89 --- /dev/null +++ b/src/main/java/nextstep/security/config/DelegatingFilterProxy.java @@ -0,0 +1,24 @@ +package nextstep.security.config; + +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/config/FilterChainProxy.java b/src/main/java/nextstep/security/config/FilterChainProxy.java new file mode 100644 index 00000000..242124d4 --- /dev/null +++ b/src/main/java/nextstep/security/config/FilterChainProxy.java @@ -0,0 +1,73 @@ +package nextstep.security.config; + +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 securityFilterChains; + + public FilterChainProxy(List securityFilterChains) { + this.securityFilterChains = securityFilterChains; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) throws IOException, ServletException { + var filters = getFilters(servletRequest); + + VirtualFilterChain virtualFilterChain = new VirtualFilterChain(filterChain, filters); + virtualFilterChain.doFilter(servletRequest, servletResponse); + } + + private List getFilters(ServletRequest servletRequest) { + var request = (HttpServletRequest) servletRequest; + + for (SecurityFilterChain chain : securityFilterChains) { + if (chain.matches(request)) { + return chain.getFilters(); + } + } + return null; + } + + private static final class VirtualFilterChain implements FilterChain { + + private final FilterChain originFilterChain; + + private final List additionalFilters; + + private final int size; + + private int currentPosition; + + private VirtualFilterChain(FilterChain originFilterChain, List additionalFilters) { + this.originFilterChain = originFilterChain; + this.additionalFilters = additionalFilters; + this.size = additionalFilters.size(); + } + + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + if (this.currentPosition == this.size) { + this.originFilterChain.doFilter(servletRequest, servletResponse); + return; + } + this.currentPosition++; + + var nexFilter = this.additionalFilters.get(this.currentPosition - 1); + nexFilter.doFilter(servletRequest, servletResponse, this); + + } + } + +} diff --git a/src/main/java/nextstep/security/config/SecurityConfig.java b/src/main/java/nextstep/security/config/SecurityConfig.java new file mode 100644 index 00000000..4d5e3d34 --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityConfig.java @@ -0,0 +1,49 @@ +package nextstep.security.config; + +import java.util.List; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.DaoAuthenticationProvider; +import nextstep.security.authentication.ProviderManager; +import nextstep.security.authentication.filter.BasicAuthFilter; +import nextstep.security.authentication.filter.FormLoginAuthFilter; +import nextstep.security.userdetail.UserDetailService; +import nextstep.security.util.TokenDecoder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SecurityConfig { + + public SecurityConfig(UserDetailService userDetailService, TokenDecoder tokenDecoder) { + this.userDetailService = userDetailService; + this.tokenDecoder = tokenDecoder; + } + + private final UserDetailService userDetailService; + private final TokenDecoder tokenDecoder; + + @Bean + public DelegatingFilterProxy delegatingFilterProxy( + AuthenticationManager authenticationManager) { + return new DelegatingFilterProxy( + filterChainProxy(List.of(securityFilterChain(authenticationManager)))); + } + + @Bean + public FilterChainProxy filterChainProxy(List securityFilterChains) { + return new FilterChainProxy(securityFilterChains); + } + + @Bean + public SecurityFilterChain securityFilterChain(AuthenticationManager authenticationManager) { + return new DefaultSecurityFilterChain( + List.of(new FormLoginAuthFilter(authenticationManager), + new BasicAuthFilter(tokenDecoder, authenticationManager))); + } + + @Bean + public AuthenticationManager authenticationManager() { + return new ProviderManager(List.of(new DaoAuthenticationProvider(userDetailService))); + } + +} diff --git a/src/main/java/nextstep/security/config/SecurityFilterChain.java b/src/main/java/nextstep/security/config/SecurityFilterChain.java new file mode 100644 index 00000000..39ef43ba --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityFilterChain.java @@ -0,0 +1,12 @@ +package nextstep.security.config; + +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; + +public interface SecurityFilterChain { + + boolean matches(HttpServletRequest request); + + List getFilters(); +} diff --git a/src/main/java/nextstep/security/util/SecurityConstants.java b/src/main/java/nextstep/security/util/SecurityConstants.java index 44660643..f5649bfe 100644 --- a/src/main/java/nextstep/security/util/SecurityConstants.java +++ b/src/main/java/nextstep/security/util/SecurityConstants.java @@ -8,4 +8,8 @@ private SecurityConstants() { public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; public static final String BASIC_TOKEN_PREFIX = "Basic "; + + public static final String LOGIN_REQUEST_URI = "/login"; + + public static final String MEMBER_REQUEST_URI = "/members"; }