diff --git a/README.md b/README.md index 1e7ba65..454a7f5 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 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 0f8eb47..1ecd05f 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/application/UserDetailServiceImpl.java b/src/main/java/nextstep/app/application/UserDetailServiceImpl.java new file mode 100644 index 0000000..4eaaa4b --- /dev/null +++ b/src/main/java/nextstep/app/application/UserDetailServiceImpl.java @@ -0,0 +1,28 @@ +package nextstep.app.application; + +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) { + return memberRepository.findByEmail(username) + .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 0000000..70ab41c --- /dev/null +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -0,0 +1,29 @@ +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/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 0ea94f1..00dd61e 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/BasicAuthInterceptor.java b/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java new file mode 100644 index 0000000..14284ab --- /dev/null +++ b/src/main/java/nextstep/security/authentication/BasicAuthInterceptor.java @@ -0,0 +1,45 @@ +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/FormLoginAuthInterceptor.java b/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java new file mode 100644 index 0000000..6692a9b --- /dev/null +++ b/src/main/java/nextstep/security/authentication/FormLoginAuthInterceptor.java @@ -0,0 +1,67 @@ +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/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 f809b6e..63a5166 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 0000000..145058a --- /dev/null +++ b/src/main/java/nextstep/security/userdetail/UserDetail.java @@ -0,0 +1,25 @@ +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; + } + + 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 new file mode 100644 index 0000000..1a6ea92 --- /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); +} diff --git a/src/main/java/nextstep/security/util/BasicTokenDecoder.java b/src/main/java/nextstep/security/util/BasicTokenDecoder.java new file mode 100644 index 0000000..68da04c --- /dev/null +++ b/src/main/java/nextstep/security/util/BasicTokenDecoder.java @@ -0,0 +1,38 @@ +package nextstep.security.util; + +import static nextstep.security.util.SecurityConstants.BASIC_TOKEN_PREFIX; + +import java.nio.charset.StandardCharsets; +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 { + + @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/util/SecurityConstants.java b/src/main/java/nextstep/security/util/SecurityConstants.java new file mode 100644 index 0000000..4466064 --- /dev/null +++ b/src/main/java/nextstep/security/util/SecurityConstants.java @@ -0,0 +1,11 @@ +package nextstep.security.util; + +public class SecurityConstants { + + private SecurityConstants() { + } + + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + public static final String BASIC_TOKEN_PREFIX = "Basic "; +} diff --git a/src/main/java/nextstep/security/util/TokenDecoder.java b/src/main/java/nextstep/security/util/TokenDecoder.java new file mode 100644 index 0000000..522018d --- /dev/null +++ b/src/main/java/nextstep/security/util/TokenDecoder.java @@ -0,0 +1,8 @@ +package nextstep.security.util; + +import nextstep.security.userdetail.UserDetail; + +public interface TokenDecoder { + + UserDetail decodeToken(String token); +}