diff --git a/README.md b/README.md index 1e7ba652..8a5776b6 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ # spring-security-authentication +## ID-PW 기반 로그인 구현 +## Basic 인증 구현 +## 인터셉터 분리 +## 인증 로직 - 서비스 로직 간 패키지 분리:wq diff --git a/build.gradle b/build.gradle index 99766160..aae46e10 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/src/main/java/nextstep/app/SecurityAuthenticationApplication.java b/src/main/java/nextstep/app/SecurityAuthenticationApplication.java index 0f8eb47d..7dea1d7d 100644 --- a/src/main/java/nextstep/app/SecurityAuthenticationApplication.java +++ b/src/main/java/nextstep/app/SecurityAuthenticationApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; @SpringBootApplication +@ComponentScan(basePackages = {"nextstep.app", "nextstep.security"}) public class SecurityAuthenticationApplication { public static void main(String[] args) { 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..58d8d84c --- /dev/null +++ b/src/main/java/nextstep/app/config/SecurityConfig.java @@ -0,0 +1,27 @@ +package nextstep.app.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf().disable() // CSRF 비활성화 (테스트 용도) + .authorizeRequests() + .antMatchers("/login", "/members").permitAll() // /login 경로는 인증 없이 접근 허용 + .anyRequest().authenticated(); // 그 외의 요청은 인증 필요 + return http.build(); + } +} 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..c902536f --- /dev/null +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -0,0 +1,30 @@ +package nextstep.app.config; + +import nextstep.security.interceptor.BasicAuthInterceptor; +import nextstep.security.interceptor.LoginInterceptor; +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 WebConfig implements WebMvcConfigurer { + + private final LoginInterceptor loginInterceptor; + private final BasicAuthInterceptor basicAuthInterceptor; + + @Autowired + public WebConfig(LoginInterceptor loginInterceptor, BasicAuthInterceptor basicAuthInterceptor) { + this.loginInterceptor = loginInterceptor; + this.basicAuthInterceptor = basicAuthInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(loginInterceptor) + .addPathPatterns("/login"); // ID/비밀번호 인증에 대한 경로 + registry.addInterceptor(basicAuthInterceptor) + .addPathPatterns("/members"); // Basic 인증에 대한 경로 + } +} diff --git a/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java b/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java index 5a6062cf..881c55d6 100644 --- a/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java +++ b/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java @@ -1,7 +1,7 @@ package nextstep.app.infrastructure; import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; +import nextstep.security.service.MemberRepository; import org.springframework.stereotype.Repository; import java.util.HashMap; diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 0ea94f1b..1de15c32 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,19 +1,15 @@ package nextstep.app.ui; -import nextstep.app.domain.MemberRepository; +import nextstep.security.service.MemberRepository; +import nextstep.security.exception.AuthenticationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - @RestController public class LoginController { - public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - private final MemberRepository memberRepository; public LoginController(MemberRepository memberRepository) { @@ -21,7 +17,7 @@ public LoginController(MemberRepository memberRepository) { } @PostMapping("/login") - public ResponseEntity<Void> login(HttpServletRequest request, HttpSession session) { + public ResponseEntity<Void> login() throws AuthenticationException { 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..2d4caf8c 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -1,7 +1,7 @@ package nextstep.app.ui; import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; +import nextstep.security.service.MemberRepository; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; diff --git a/src/main/java/nextstep/security/SecurityConstants.java b/src/main/java/nextstep/security/SecurityConstants.java new file mode 100644 index 00000000..779bc5ab --- /dev/null +++ b/src/main/java/nextstep/security/SecurityConstants.java @@ -0,0 +1,5 @@ +package nextstep.security; + +public class SecurityConstants { + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; +} diff --git a/src/main/java/nextstep/app/ui/AuthenticationException.java b/src/main/java/nextstep/security/exception/AuthenticationException.java similarity index 64% rename from src/main/java/nextstep/app/ui/AuthenticationException.java rename to src/main/java/nextstep/security/exception/AuthenticationException.java index f809b6e4..1271b52d 100644 --- a/src/main/java/nextstep/app/ui/AuthenticationException.java +++ b/src/main/java/nextstep/security/exception/AuthenticationException.java @@ -1,4 +1,4 @@ -package nextstep.app.ui; +package nextstep.security.exception; public class AuthenticationException extends RuntimeException { } diff --git a/src/main/java/nextstep/security/interceptor/BasicAuthInterceptor.java b/src/main/java/nextstep/security/interceptor/BasicAuthInterceptor.java new file mode 100644 index 00000000..845f1d05 --- /dev/null +++ b/src/main/java/nextstep/security/interceptor/BasicAuthInterceptor.java @@ -0,0 +1,59 @@ +package nextstep.security.interceptor; + +import nextstep.app.domain.Member; +import nextstep.security.service.MemberRepository; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Optional; + +import static nextstep.security.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; + +@Component +public class BasicAuthInterceptor implements HandlerInterceptor { + + private final MemberRepository memberRepository; + + public BasicAuthInterceptor(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + System.out.println("베이직 인터셉터"); + String authHeader = request.getHeader("Authorization"); + if (!(authHeader != null && authHeader.startsWith("Basic "))) { // invalid header + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + String base64Credentials = authHeader.substring("Basic ".length()).trim(); + String credentials = new String(Base64.getDecoder().decode(base64Credentials), StandardCharsets.UTF_8); + if (!credentials.contains(":")) { // invalid header + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + final String[] values = credentials.split(":", 2); + String username = values[0]; + String password = values[1]; + Optional<Member> memberOptional = memberRepository.findByEmail(username); + if (memberOptional.isPresent()) { + Member member = memberOptional.get(); + + // 1-2. 비밀번호 확인 + if (password.equals(member.getPassword())) { + // 1-3. 세션에 인증 정보 저장 + HttpSession session = request.getSession(); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member); + return true; + } + } + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return false; // 다음으로 요청을 진행 + } +} 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 00000000..0af12625 --- /dev/null +++ b/src/main/java/nextstep/security/interceptor/LoginInterceptor.java @@ -0,0 +1,49 @@ +package nextstep.security.interceptor; + +import nextstep.app.domain.Member; +import nextstep.security.service.MemberRepository; +import nextstep.security.exception.AuthenticationException; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import static nextstep.security.SecurityConstants.SPRING_SECURITY_CONTEXT_KEY; + +@Component +public class LoginInterceptor implements HandlerInterceptor { + + private final MemberRepository memberRepository; + + public LoginInterceptor(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + System.out.println("로그인 인터셉터"); + String username = request.getParameter("username"); + String password = request.getParameter("password"); + + if (username == null || password == null) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + Member member = memberRepository.findByEmail(username) + .orElseThrow(AuthenticationException::new); + + if (!password.equals(member.getPassword())) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + // 세션에 인증 정보 저장 + HttpSession session = request.getSession(); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member); + + return true; // 다음으로 요청을 진행 + } +} diff --git a/src/main/java/nextstep/app/domain/MemberRepository.java b/src/main/java/nextstep/security/service/MemberRepository.java similarity index 73% rename from src/main/java/nextstep/app/domain/MemberRepository.java rename to src/main/java/nextstep/security/service/MemberRepository.java index 2eb5cdbb..c2f30e54 100644 --- a/src/main/java/nextstep/app/domain/MemberRepository.java +++ b/src/main/java/nextstep/security/service/MemberRepository.java @@ -1,4 +1,6 @@ -package nextstep.app.domain; +package nextstep.security.service; + +import nextstep.app.domain.Member; import java.util.List; import java.util.Optional; diff --git a/src/test/java/nextstep/app/MemberTest.java b/src/test/java/nextstep/app/MemberTest.java index 58aba17b..ceea5b1f 100644 --- a/src/test/java/nextstep/app/MemberTest.java +++ b/src/test/java/nextstep/app/MemberTest.java @@ -1,7 +1,7 @@ package nextstep.app; import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; +import nextstep.security.service.MemberRepository; import nextstep.app.infrastructure.InmemoryMemberRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName;