From 1da2ab9168ab538a6cba4c8ba85a627bdc04f9f6 Mon Sep 17 00:00:00 2001 From: johan1103 Date: Mon, 4 Nov 2024 03:20:55 +0900 Subject: [PATCH] =?UTF-8?q?1st=20step=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 26 ++++++++++ .../nextstep/app/service/MemberService.java | 19 +++++++ .../java/nextstep/app/ui/LoginController.java | 1 - .../security/config/WebMvcConfig.java | 31 +++++++++++ .../security/constants/Constants.java | 13 +++++ .../BasicAuthenticationInterceptor.java | 25 +++++++++ .../interceptor/LoginInterceptor.java | 31 +++++++++++ .../MemberValidationInterceptor.java | 38 ++++++++++++++ .../security/model/EmailPasswordAuth.java | 52 +++++++++++++++++++ .../service/MemberValidationService.java | 5 ++ 10 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 src/main/java/nextstep/app/service/MemberService.java create mode 100644 src/main/java/nextstep/security/config/WebMvcConfig.java create mode 100644 src/main/java/nextstep/security/constants/Constants.java create mode 100644 src/main/java/nextstep/security/interceptor/BasicAuthenticationInterceptor.java create mode 100644 src/main/java/nextstep/security/interceptor/LoginInterceptor.java create mode 100644 src/main/java/nextstep/security/interceptor/MemberValidationInterceptor.java create mode 100644 src/main/java/nextstep/security/model/EmailPasswordAuth.java create mode 100644 src/main/java/nextstep/security/service/MemberValidationService.java diff --git a/README.md b/README.md index 1e7ba65..8a81213 100644 --- a/README.md +++ b/README.md @@ -1 +1,27 @@ # spring-security-authentication + +## 기능 요구 사항 ++ POST /login 경로로 로그인 요청을 한다. ++ GET /member 요청 시 사용자 목록을 조회한다. ++ 아이디와 비밀번호를 기반으로 로그인 기능을 구현한다. ++ 세션으로 사용자의 인증 정보를 저장한다. ++ Basic인증을 사용하여 사용자를 식별한다. ++ 요청의 Authorization 헤더에서 Basic 인증 정보를 저장한다. ++ Member Test의 모든 테스트가 통과해야 한다. + + +## 프로그래밍 요구사항 ++ 자바 코드 컨벤션을 지키면서 프로그래밍한다. + + 기본적으로 Google Java Style Guide를 원칙으로 한다. + + 단, 들여쓰기는 '2 spaces'가 아닌 '4 spaces'로 한다. ++ indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다. + + 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. + + 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다. ++ 3항 연산자를 쓰지 않는다. ++ 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다. + + 함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다. ++ else 예약어를 쓰지 않는다. + + else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. + + 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다. ++ JUnit 5와 AssertJ를 이용하여 정리한 기능 목록이 정상적으로 작동하는지 테스트 코드로 확인한다. + diff --git a/src/main/java/nextstep/app/service/MemberService.java b/src/main/java/nextstep/app/service/MemberService.java new file mode 100644 index 0000000..4e8e952 --- /dev/null +++ b/src/main/java/nextstep/app/service/MemberService.java @@ -0,0 +1,19 @@ +package nextstep.app.service; + +import nextstep.app.domain.MemberRepository; +import nextstep.security.service.MemberValidationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MemberService implements MemberValidationService { + private final MemberRepository memberRepository; + @Autowired + public MemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + @Override + public boolean isValidMember(String email, String password) { + return memberRepository.findByEmail(email).isPresent(); + } +} diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 0ea94f1..5e387e5 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -12,7 +12,6 @@ @RestController public class LoginController { - public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; private final MemberRepository memberRepository; diff --git a/src/main/java/nextstep/security/config/WebMvcConfig.java b/src/main/java/nextstep/security/config/WebMvcConfig.java new file mode 100644 index 0000000..d4dd1da --- /dev/null +++ b/src/main/java/nextstep/security/config/WebMvcConfig.java @@ -0,0 +1,31 @@ +package nextstep.security.config; +import nextstep.security.interceptor.BasicAuthenticationInterceptor; +import nextstep.security.interceptor.LoginInterceptor; +import nextstep.security.interceptor.MemberValidationInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +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 WebMvcConfig implements WebMvcConfigurer { + private final BasicAuthenticationInterceptor basicAuthenticationInterceptor; + private final MemberValidationInterceptor memberValidationInterceptor; + private final LoginInterceptor loginInterceptor; + + @Autowired + public WebMvcConfig(BasicAuthenticationInterceptor basicAuthenticationInterceptor, + MemberValidationInterceptor memberValidationInterceptor, + LoginInterceptor loginInterceptor) { + this.basicAuthenticationInterceptor = basicAuthenticationInterceptor; + this.memberValidationInterceptor = memberValidationInterceptor; + this.loginInterceptor = loginInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(loginInterceptor).addPathPatterns("/login/**"); + registry.addInterceptor(basicAuthenticationInterceptor).order(0).addPathPatterns("/members/**"); + registry.addInterceptor(memberValidationInterceptor).order(1).addPathPatterns("/members/**"); + } +} diff --git a/src/main/java/nextstep/security/constants/Constants.java b/src/main/java/nextstep/security/constants/Constants.java new file mode 100644 index 0000000..ba7a015 --- /dev/null +++ b/src/main/java/nextstep/security/constants/Constants.java @@ -0,0 +1,13 @@ +package nextstep.security.constants; + +public class Constants { + public static class Http { + public static final String AUTHORIZATION = "Authorization"; + } + public static class Auth { + public static final String BASIC = "Basic "; + public static final String EMAIL = "email"; + public static final String PASSWORD = "password"; + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + } +} 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 0000000..a92433c --- /dev/null +++ b/src/main/java/nextstep/security/interceptor/BasicAuthenticationInterceptor.java @@ -0,0 +1,25 @@ +package nextstep.security.interceptor; + +import nextstep.security.constants.Constants; +import nextstep.security.model.EmailPasswordAuth; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Objects; + +@Component +public class BasicAuthenticationInterceptor implements HandlerInterceptor { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + System.out.println("BasicAuthenticationInterceptor preHandle"); + String authorization = request.getHeader(Constants.Http.AUTHORIZATION); + if (Objects.isNull(EmailPasswordAuth.from(authorization))) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + return false; + } + return true; + } +} diff --git a/src/main/java/nextstep/security/interceptor/LoginInterceptor.java b/src/main/java/nextstep/security/interceptor/LoginInterceptor.java new file mode 100644 index 0000000..56fa4f4 --- /dev/null +++ b/src/main/java/nextstep/security/interceptor/LoginInterceptor.java @@ -0,0 +1,31 @@ +package nextstep.security.interceptor; + +import nextstep.security.constants.Constants; +import nextstep.security.model.EmailPasswordAuth; +import nextstep.security.service.MemberValidationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Component +public class LoginInterceptor implements HandlerInterceptor { + private final MemberValidationService memberValidationService; + @Autowired + public LoginInterceptor(MemberValidationService memberValidationService) { + this.memberValidationService = memberValidationService; + } + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String email = request.getParameter(Constants.Auth.EMAIL); + String password = request.getParameter(Constants.Auth.PASSWORD); + if(memberValidationService.isValidMember(email, password)) { + request.getSession().setAttribute(Constants.Auth.SPRING_SECURITY_CONTEXT_KEY, + new EmailPasswordAuth(email, password)); + return true; + } + return false; + } +} diff --git a/src/main/java/nextstep/security/interceptor/MemberValidationInterceptor.java b/src/main/java/nextstep/security/interceptor/MemberValidationInterceptor.java new file mode 100644 index 0000000..89069fb --- /dev/null +++ b/src/main/java/nextstep/security/interceptor/MemberValidationInterceptor.java @@ -0,0 +1,38 @@ +package nextstep.security.interceptor; + +import nextstep.security.constants.Constants; +import nextstep.security.model.EmailPasswordAuth; +import nextstep.security.service.MemberValidationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Objects; + +@Component +public class MemberValidationInterceptor implements HandlerInterceptor { + private final MemberValidationService memberValidationService; + @Autowired + public MemberValidationInterceptor(MemberValidationService memberValidationService) { + this.memberValidationService = memberValidationService; + } + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + System.out.println("MemberValidationInterceptor preHandle"); + String authorization = request.getHeader(Constants.Http.AUTHORIZATION); + EmailPasswordAuth emailPasswordAuth = EmailPasswordAuth.from(authorization); + if (Objects.isNull(emailPasswordAuth)) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + return false; + } + if(memberValidationService.isValidMember(emailPasswordAuth.getEmail(), + emailPasswordAuth.getPassword())) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + return false; + } + return true; + } +} diff --git a/src/main/java/nextstep/security/model/EmailPasswordAuth.java b/src/main/java/nextstep/security/model/EmailPasswordAuth.java new file mode 100644 index 0000000..04f41f4 --- /dev/null +++ b/src/main/java/nextstep/security/model/EmailPasswordAuth.java @@ -0,0 +1,52 @@ +package nextstep.security.model; + +import nextstep.security.constants.Constants; +import org.springframework.util.Base64Utils; + +import java.util.Objects; + +public class EmailPasswordAuth { + private String email; + private String password; + + public String getEmail() { + return email; + } + public String getPassword() { + return password; + } + + public EmailPasswordAuth(String email, String password) { + this.email = email; + this.password = password; + } + + public static EmailPasswordAuth from(String basicAuth) { + if(Objects.isNull(basicAuth) || !basicAuth.startsWith(Constants.Auth.BASIC)) { + return null; + } + String base64Credentials = basicAuth.substring(Constants.Auth.BASIC.length()).trim(); + try { + // Base64 문자열을 디코딩 + byte[] decoded = Base64Utils.decodeFromString(base64Credentials); + String credentials = new String(decoded); + + // 사용자 이름과 비밀번호가 ':'로 구분되는지 확인 + String[] parts = credentials.split(":", 2); + if(parts.length == 2) { + return new EmailPasswordAuth(parts[0], parts[1]); + } + return null; + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return null; + } + } + + public static EmailPasswordAuth from(String email, String password) { + if(email == null || password == null) { + return null; + } + return new EmailPasswordAuth(email, password); + } +} diff --git a/src/main/java/nextstep/security/service/MemberValidationService.java b/src/main/java/nextstep/security/service/MemberValidationService.java new file mode 100644 index 0000000..6d8799e --- /dev/null +++ b/src/main/java/nextstep/security/service/MemberValidationService.java @@ -0,0 +1,5 @@ +package nextstep.security.service; + +public interface MemberValidationService { + boolean isValidMember(String email, String password); +}