diff --git a/README.md b/README.md
index 1e7ba652..393378e3 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,62 @@
# spring-security-authentication
+
+
+아이디와 비밀번호를 기반으로 로그인 기능을 구현하고
+Basic 인증을 사용하여 사용자를 식별할 수 있도록 스프링 프레임워크를 사용하여 웹 앱으로 구현한다.
+
+- ``Spring security``의 내부 구조를 분석해 직접 구현 (단, ``Filter`` 대신 ``Interceptor`` 활용)
+
+---
+# 구현 요구 사항
+
+1. 아이디와 비밀번호 기반 로그인 인증 구현
+ + 로그인 요청 시 사용자가 입력한 아이디와 패스워드를 확인하여 인증한다.
+ + 로그인 성공 시 ``Session`` 을 사용하여 인증 정보를 저장한다.
+
+
+2. Basic 인증 구현
+ + 사용자 목록 조회 기능 (인증 진행 후 인가 진행)
+ - 인증 : Basic 인증을 사용하여 사용자를 식별한다.
+ + 이를 위해 요청의 **Authorization 헤더**에서 Basic 인증 정보를 추출 후 decode 하여 인증을 처리한다.
+ + 인증 성공 시 ``Session``을 사용하여 인증 정보를 저장한다.
+ + (다만, 인가를 위한 인증 및 권한 정보는 ThreadLocal 에 저장하여 활용)
+ - 인가 : ``Member``로 등록되어 있는, 인증된 사용자만 가능하도록 한다.
+ + 인증 ``Interceptor``가 통과되면 인가 ``Interceptor`` 진행
+ + ThreadLocal 에서 조회하여 인증 정보가 있으면 인가
+
+
+3. 인터셉터 분리
+ + ``HandlerInterceptor``를 사용하여 인증 관련 로직을 ``Controller``에서 분리한다.
+ + 앞서 구현한 두 인증 방식(아이디 비밀번호 로그인 방식과 Basic 인증 방식) 모두 인터셉터에서 처리되도록 구현한다.
+ + 가급적이면 하나의 인터셉터는 하나의 작업만 수행하도록 설계한다.
+ + 아이디/패스워드 기반 Authentication ``Interceptor``
+ + Basic 인증 기반 Authentication ``Interceptor``
+ + Authorization ``Interceptor``
+
+
+
+4. 인증 로직과 서비스 로직 간의 패키지 분리
+ + **서비스 코드와 인증 코드를 명확히 분리**하여 관리하도록 한다.
+ + 서비스 관련 코드는 ``app`` 패키지에 위치시키고, 인증 관련 코드는 ``security`` 패키지에 위치시킨다.
+ + 리팩터링 과정에서 패키지 간의 양방향 참조가 발생한다면 단방향 참조로 리팩터링한다.
+ + ``app`` 패키지는 ``security`` 패키지에 의존할 수 있지만, **``security`` 패키지는 ``app`` 패키지에 의존하지 않도록** 한다.
+ + 인증 관련 작업은 ``security`` 패키지에서 전담하도록 설계하여, 서비스 로직이 인증 세부 사항에 의존하지 않게 만든다.
+
+ ```
+ 패키지 간 의존성을 최소화하고, 변경에 강한 구조를 만드는 목적.
+ security 패키지를 독립적이고 재사용 가능하게 설계하려면, 직접적인 의존성을 피하기 위해 인터페이스를 구현하게 한다. (DIP)
+ ```
+
+
+
+
+
+
+---
+# API 정의
+### 로그인
+ - /login [POST] 아이디와 비밀번호를 확인하여 인증. (인증 후 Session에 인증 정보 저장)
+
+### 사용자 조회
+ - /member [GET] 사용자 목록 조회. (단, 인증 성공 후 인증 정보가 있을 경우만 인가)
+
diff --git a/build.gradle b/build.gradle
index 99766160..b1e5d784 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,7 +14,17 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
+// implementation 'org.springframework.boot:spring-boot-starter-security'
+// implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+// runtimeOnly 'com.h2database:h2'
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
+// testImplementation 'org.springframework.security:spring-security-test'
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+ //테스트에서 lombok 사용
+ testCompileOnly 'org.projectlombok:lombok'
+ testAnnotationProcessor 'org.projectlombok:lombok'
}
tasks.named('test') {
diff --git a/src/main/java/nextstep/app/SecurityAuthenticationApplication.java b/src/main/java/nextstep/app/SecurityAuthenticationApplication.java
index 0f8eb47d..9a8b666d 100644
--- a/src/main/java/nextstep/app/SecurityAuthenticationApplication.java
+++ b/src/main/java/nextstep/app/SecurityAuthenticationApplication.java
@@ -2,12 +2,12 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
+@ComponentScan(basePackages = {"nextstep.app", "nextstep.util"})
public class SecurityAuthenticationApplication {
-
public static void main(String[] args) {
SpringApplication.run(SecurityAuthenticationApplication.class, args);
}
-
}
diff --git a/src/main/java/nextstep/app/WebConfig.java b/src/main/java/nextstep/app/WebConfig.java
new file mode 100644
index 00000000..6d36b20e
--- /dev/null
+++ b/src/main/java/nextstep/app/WebConfig.java
@@ -0,0 +1,19 @@
+package nextstep.app;
+
+import nextstep.security.web.authentication.BasicAuthenticationInterceptor;
+import nextstep.security.web.authentication.UsernamePasswordAuthenticationInterceptor;
+import nextstep.security.web.authorization.AuthorizationInterceptor;
+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 {
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ registry.addInterceptor(new UsernamePasswordAuthenticationInterceptor()).addPathPatterns("/login");
+ // /members 경로는 인증 후 인가 처리
+ registry.addInterceptor(new BasicAuthenticationInterceptor()).addPathPatterns("/members");
+ registry.addInterceptor(new AuthorizationInterceptor()).addPathPatterns("/members");
+ }
+}
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 00000000..445dfd70
--- /dev/null
+++ b/src/main/java/nextstep/app/service/MemberService.java
@@ -0,0 +1,29 @@
+package nextstep.app.service;
+
+import lombok.RequiredArgsConstructor;
+import nextstep.app.domain.Member;
+import nextstep.app.domain.MemberRepository;
+import nextstep.security.core.userdetails.User;
+import nextstep.security.core.userdetails.UserDetails;
+import nextstep.security.core.userdetails.UserDetailsService;
+import nextstep.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+
+@Service
+@RequiredArgsConstructor
+public class MemberService implements UserDetailsService {
+ private final MemberRepository memberRepository;
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ Member member = memberRepository.findByEmail(username)
+ .orElseThrow(() -> new UsernameNotFoundException(username));
+
+ return User.builder()
+ .username(member.getEmail())
+ .password(member.getPassword())
+ .roles("MEMBER")
+ .build();
+ }
+}
diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java
index 0ea94f1b..64a73f2a 100644
--- a/src/main/java/nextstep/app/ui/LoginController.java
+++ b/src/main/java/nextstep/app/ui/LoginController.java
@@ -1,6 +1,6 @@
package nextstep.app.ui;
-import nextstep.app.domain.MemberRepository;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -10,23 +10,13 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
+@Slf4j
@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) {
+ log.info("Login request received");
return ResponseEntity.ok().build();
}
-
- @ExceptionHandler(AuthenticationException.class)
- public ResponseEntity handleAuthenticationException() {
- return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
- }
}
diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java
index c8cc74d6..299b140c 100644
--- a/src/main/java/nextstep/app/ui/MemberController.java
+++ b/src/main/java/nextstep/app/ui/MemberController.java
@@ -1,5 +1,6 @@
package nextstep.app.ui;
+import lombok.RequiredArgsConstructor;
import nextstep.app.domain.Member;
import nextstep.app.domain.MemberRepository;
import org.springframework.http.ResponseEntity;
@@ -9,14 +10,11 @@
import java.util.List;
@RestController
+@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
- public MemberController(MemberRepository memberRepository) {
- this.memberRepository = memberRepository;
- }
-
@GetMapping("/members")
public ResponseEntity> list() {
List members = memberRepository.findAll();
diff --git a/src/main/java/nextstep/security/authentication/AuthenticationCredentialsNotFoundException.java b/src/main/java/nextstep/security/authentication/AuthenticationCredentialsNotFoundException.java
new file mode 100644
index 00000000..0ea0aaeb
--- /dev/null
+++ b/src/main/java/nextstep/security/authentication/AuthenticationCredentialsNotFoundException.java
@@ -0,0 +1,9 @@
+package nextstep.security.authentication;
+
+import nextstep.security.core.AuthenticationException;
+
+public class AuthenticationCredentialsNotFoundException extends AuthenticationException {
+ public AuthenticationCredentialsNotFoundException(String msg) {
+ super(msg);
+ }
+}
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..604b166e
--- /dev/null
+++ b/src/main/java/nextstep/security/authentication/AuthenticationManager.java
@@ -0,0 +1,12 @@
+package nextstep.security.authentication;
+
+import nextstep.security.core.Authentication;
+import nextstep.security.core.AuthenticationException;
+
+public class AuthenticationManager {
+ private final AuthenticationProvider authenticationProvider = new AuthenticationProvider();
+
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException{
+ return authenticationProvider.authenticate(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..03b0eac5
--- /dev/null
+++ b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java
@@ -0,0 +1,46 @@
+package nextstep.security.authentication;
+
+import nextstep.util.ApplicationContextProvider;
+import nextstep.security.core.Authentication;
+import nextstep.security.core.AuthenticationException;
+import nextstep.security.core.userdetails.UserDetails;
+import nextstep.security.core.userdetails.UserDetailsService;
+
+import java.util.Objects;
+
+public class AuthenticationProvider {
+
+ private final UserDetailsService userDetailsService;
+
+ public AuthenticationProvider() {
+ this.userDetailsService = ApplicationContextProvider.getApplicationContext().getBean(UserDetailsService.class);
+ }
+
+ public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+ // 사용자 정보 load
+ UserDetails loadedUser = retrieveUser(authentication.getPrincipal());
+
+ // 패스워드 체크
+ authenticationChecks(authentication, loadedUser);
+
+ // 인증 정보 생성
+ return createSuccessAuthentication(loadedUser);
+ }
+
+
+ private UserDetails retrieveUser(Object username) {
+ return userDetailsService.loadUserByUsername((String) username);
+ }
+
+ private void authenticationChecks(Authentication authentication, UserDetails loadedUser) {
+ if (!Objects.equals(authentication.getCredentials().toString(), loadedUser.getPassword())) {
+ throw new BadCredentialsException("Bad credentials");
+ }
+ }
+
+ private Authentication createSuccessAuthentication(UserDetails user) {
+ Authentication authentication = new Authentication(user, null, user.getAuthorities());
+ authentication.setAuthenticated(true);
+ return authentication;
+ }
+}
diff --git a/src/main/java/nextstep/security/authentication/BadCredentialsException.java b/src/main/java/nextstep/security/authentication/BadCredentialsException.java
new file mode 100644
index 00000000..be46a7a0
--- /dev/null
+++ b/src/main/java/nextstep/security/authentication/BadCredentialsException.java
@@ -0,0 +1,13 @@
+package nextstep.security.authentication;
+
+import nextstep.security.core.AuthenticationException;
+
+public class BadCredentialsException extends AuthenticationException {
+ public BadCredentialsException(String msg) {
+ super(msg);
+ }
+
+ public BadCredentialsException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/src/main/java/nextstep/security/authorization/AccessDeniedException.java b/src/main/java/nextstep/security/authorization/AccessDeniedException.java
new file mode 100644
index 00000000..9b48f514
--- /dev/null
+++ b/src/main/java/nextstep/security/authorization/AccessDeniedException.java
@@ -0,0 +1,12 @@
+package nextstep.security.authorization;
+
+/**
+ * Thrown if an Authentication object does not hold a required authority.
+ */
+public class AccessDeniedException extends RuntimeException {
+
+ public AccessDeniedException(String msg) {
+ super(msg);
+ }
+
+}
diff --git a/src/main/java/nextstep/security/authorization/AuthorizationDecision.java b/src/main/java/nextstep/security/authorization/AuthorizationDecision.java
new file mode 100644
index 00000000..f91966e8
--- /dev/null
+++ b/src/main/java/nextstep/security/authorization/AuthorizationDecision.java
@@ -0,0 +1,10 @@
+package nextstep.security.authorization;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class AuthorizationDecision {
+ private final boolean granted;
+}
diff --git a/src/main/java/nextstep/security/authorization/AuthorizationManager.java b/src/main/java/nextstep/security/authorization/AuthorizationManager.java
new file mode 100644
index 00000000..094be54b
--- /dev/null
+++ b/src/main/java/nextstep/security/authorization/AuthorizationManager.java
@@ -0,0 +1,17 @@
+package nextstep.security.authorization;
+
+import nextstep.security.core.Authentication;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.function.Supplier;
+
+public class AuthorizationManager {
+ public AuthorizationDecision check(Supplier authentication, HttpServletRequest request) {
+ boolean granted = isGranted(authentication.get());
+ return new AuthorizationDecision(granted);
+ }
+
+ private boolean isGranted(Authentication authentication) {
+ return authentication != null && authentication.isAuthenticated();
+ }
+}
diff --git a/src/main/java/nextstep/security/context/SecurityContext.java b/src/main/java/nextstep/security/context/SecurityContext.java
new file mode 100644
index 00000000..8d63e6d4
--- /dev/null
+++ b/src/main/java/nextstep/security/context/SecurityContext.java
@@ -0,0 +1,16 @@
+package nextstep.security.context;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import nextstep.security.core.Authentication;
+
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+@Setter
+public class SecurityContext {
+ private Authentication authentication;
+}
diff --git a/src/main/java/nextstep/security/context/SecurityContextHolder.java b/src/main/java/nextstep/security/context/SecurityContextHolder.java
new file mode 100644
index 00000000..2e163b23
--- /dev/null
+++ b/src/main/java/nextstep/security/context/SecurityContextHolder.java
@@ -0,0 +1,34 @@
+package nextstep.security.context;
+
+import org.springframework.util.Assert;
+
+import java.util.function.Supplier;
+
+public class SecurityContextHolder {
+ private static final ThreadLocal> contextHolder = new ThreadLocal<>();
+
+ public static SecurityContext getContext() {
+ Supplier result = contextHolder.get();
+ if (result == null) {
+ SecurityContext context = createEmptyContext();
+ result = () -> context;
+ contextHolder.set(result);
+ }
+
+ return result.get();
+ }
+
+ public static void setContext(SecurityContext context) {
+ Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
+ contextHolder.set(() -> context); //map.set(this, value);
+ }
+
+ public static void clearContext() {
+ contextHolder.remove();
+ }
+
+ private static SecurityContext createEmptyContext() {
+ return new SecurityContext();
+ }
+}
+
diff --git a/src/main/java/nextstep/security/core/Authentication.java b/src/main/java/nextstep/security/core/Authentication.java
new file mode 100644
index 00000000..e3d889f1
--- /dev/null
+++ b/src/main/java/nextstep/security/core/Authentication.java
@@ -0,0 +1,33 @@
+package nextstep.security.core;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+import nextstep.security.core.authority.GrantedAuthority;
+import nextstep.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+
+
+@Getter
+@Setter
+@AllArgsConstructor
+public class Authentication {
+ private final Object principal;
+ private Object credentials;
+ private final Collection authorities;
+ private boolean authenticated = false;
+
+ public Authentication(Object principal, Object credentials, Collection authorities) {
+ this.principal = principal;
+ this.credentials = credentials;
+ this.authorities = authorities;
+ }
+
+ public String getName(){
+ if (principal instanceof UserDetails){
+ return ((UserDetails)principal).getUsername();
+ }
+ return principal.toString();
+ }
+}
diff --git a/src/main/java/nextstep/security/core/AuthenticationException.java b/src/main/java/nextstep/security/core/AuthenticationException.java
new file mode 100644
index 00000000..b63b8b3d
--- /dev/null
+++ b/src/main/java/nextstep/security/core/AuthenticationException.java
@@ -0,0 +1,11 @@
+package nextstep.security.core;
+
+public class AuthenticationException extends RuntimeException {
+ public AuthenticationException(String msg) {
+ super(msg);
+ }
+ public AuthenticationException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+}
diff --git a/src/main/java/nextstep/security/core/authority/GrantedAuthority.java b/src/main/java/nextstep/security/core/authority/GrantedAuthority.java
new file mode 100644
index 00000000..3469ae14
--- /dev/null
+++ b/src/main/java/nextstep/security/core/authority/GrantedAuthority.java
@@ -0,0 +1,12 @@
+package nextstep.security.core.authority;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+@AllArgsConstructor
+@Getter
+@Setter
+public class GrantedAuthority {
+ private final String role;
+}
diff --git a/src/main/java/nextstep/security/core/userdetails/User.java b/src/main/java/nextstep/security/core/userdetails/User.java
new file mode 100644
index 00000000..bce321e2
--- /dev/null
+++ b/src/main/java/nextstep/security/core/userdetails/User.java
@@ -0,0 +1,32 @@
+package nextstep.security.core.userdetails;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+import nextstep.security.core.authority.GrantedAuthority;
+import org.springframework.util.Assert;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@Builder
+@Getter
+@Setter
+public class User implements UserDetails {
+ private String password;
+ private final String username;
+ private final Set authorities;
+
+
+ public static class UserBuilder {
+ public UserBuilder roles(String... roles) {
+ this.authorities = new HashSet<>();
+ for (String role : roles) {
+ Assert.isTrue(!role.startsWith("ROLE_"),
+ () -> role + " cannot start with ROLE_ (it is automatically added)");
+ authorities.add(new GrantedAuthority("ROLE_" + role));
+ }
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/nextstep/security/core/userdetails/UserDetails.java b/src/main/java/nextstep/security/core/userdetails/UserDetails.java
new file mode 100644
index 00000000..dfe9c294
--- /dev/null
+++ b/src/main/java/nextstep/security/core/userdetails/UserDetails.java
@@ -0,0 +1,12 @@
+package nextstep.security.core.userdetails;
+
+import nextstep.security.core.authority.GrantedAuthority;
+
+import java.util.Collection;
+
+public interface UserDetails {
+
+ String getUsername();
+ String getPassword();
+ Collection getAuthorities();
+}
diff --git a/src/main/java/nextstep/security/core/userdetails/UserDetailsService.java b/src/main/java/nextstep/security/core/userdetails/UserDetailsService.java
new file mode 100644
index 00000000..b1f7923f
--- /dev/null
+++ b/src/main/java/nextstep/security/core/userdetails/UserDetailsService.java
@@ -0,0 +1,5 @@
+package nextstep.security.core.userdetails;
+
+public interface UserDetailsService {
+ UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
+}
diff --git a/src/main/java/nextstep/security/core/userdetails/UsernameNotFoundException.java b/src/main/java/nextstep/security/core/userdetails/UsernameNotFoundException.java
new file mode 100644
index 00000000..130f730c
--- /dev/null
+++ b/src/main/java/nextstep/security/core/userdetails/UsernameNotFoundException.java
@@ -0,0 +1,12 @@
+package nextstep.security.core.userdetails;
+
+import nextstep.security.core.AuthenticationException;
+
+public class UsernameNotFoundException extends AuthenticationException {
+ public UsernameNotFoundException(String msg) {
+ super(msg);
+ }
+ public UsernameNotFoundException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/src/main/java/nextstep/security/web/authentication/AuthenticationInterceptor.java b/src/main/java/nextstep/security/web/authentication/AuthenticationInterceptor.java
new file mode 100644
index 00000000..8bb123a1
--- /dev/null
+++ b/src/main/java/nextstep/security/web/authentication/AuthenticationInterceptor.java
@@ -0,0 +1,47 @@
+package nextstep.security.web.authentication;
+
+import nextstep.security.authentication.AuthenticationManager;
+import nextstep.security.context.SecurityContext;
+import nextstep.security.context.SecurityContextHolder;
+import nextstep.security.core.Authentication;
+import nextstep.security.core.AuthenticationException;
+import nextstep.security.core.userdetails.UserDetails;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public abstract class AuthenticationInterceptor implements HandlerInterceptor {
+ public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
+ private final AuthenticationManager authenticationManager = new AuthenticationManager();
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ Authentication authRequest = (Authentication) request.getAttribute("authRequest");
+ Authentication authResponse;
+
+ try {
+ // 인증
+ authResponse = authenticationManager.authenticate(authRequest);
+
+ //인증 정보 저장
+ SecurityContextHolder.setContext(new SecurityContext(authResponse));
+
+ } catch (AuthenticationException e) {
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
+ return false;
+ }
+
+ // 인증 정보를 세션에 저장
+ UserDetails principal = (UserDetails) authResponse.getPrincipal();
+ request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, principal);
+
+ return true;
+ }
+
+ @Override
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
+ // SecurityContext 초기화
+ SecurityContextHolder.clearContext();
+ }
+}
diff --git a/src/main/java/nextstep/security/web/authentication/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/web/authentication/BasicAuthenticationInterceptor.java
new file mode 100644
index 00000000..3d1ca83e
--- /dev/null
+++ b/src/main/java/nextstep/security/web/authentication/BasicAuthenticationInterceptor.java
@@ -0,0 +1,77 @@
+package nextstep.security.web.authentication;
+
+import nextstep.security.authentication.BadCredentialsException;
+import nextstep.security.context.SecurityContextHolder;
+import nextstep.security.core.Authentication;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+public class BasicAuthenticationInterceptor extends AuthenticationInterceptor {
+
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ Authentication authRequest;
+ // 인증 정보 추출
+ try {
+ authRequest = convert(request);
+ } catch (BadCredentialsException e) {
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
+ return false;
+ }
+
+ // 인증 정보가 없을 경우 로그인 페이지로 리다이렉트
+ if (authRequest == null) {
+ response.sendRedirect(request.getContextPath() + "/login");
+ return false;
+ }
+
+ //인증이 필요할 경우 인증
+ if (authenticationIsRequired((String) authRequest.getPrincipal())) {
+ request.setAttribute("authRequest", authRequest);
+ return super.preHandle(request, response, handler);
+ }
+
+ return true;
+ }
+
+ @Override
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
+ super.afterCompletion(request, response, handler, ex);
+ }
+
+ private Authentication convert(HttpServletRequest request) {
+ String header = request.getHeader("Authorization");
+
+ //정상 요청 여부 확인
+ if (header == null) return null;
+ header = header.trim();
+ if (!StringUtils.startsWithIgnoreCase(header, "Basic")) return null;
+ if (header.equalsIgnoreCase("Basic"))
+ throw new BadCredentialsException("Empty basic authentication token");
+
+ //BASE64 decoding and parsing
+ byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
+ byte[] decoded = Base64.getDecoder().decode(base64Token);
+ String token = new String(decoded, StandardCharsets.UTF_8);
+ int delim = token.indexOf(":");
+ if (delim == -1) {
+ throw new BadCredentialsException("Invalid basic authentication token");
+ }
+
+ return new Authentication(token.substring(0, delim), token.substring(delim + 1), null);
+ }
+
+ private boolean authenticationIsRequired(String username) {
+ Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
+ return existingAuth == null || !existingAuth.getName().equals(username) || !existingAuth.isAuthenticated(); //모두 불만족하면 true
+ }
+
+}
+//1. Basic 인증을 사용하여 사용자를 식별한다. 요청의 Authorization 헤더에서 Basic 인증 정보를 추출
+//2. 인증 성공 시 Session 을 사용하여 인증 정보를 저장한다.
+//3. 인가 필터로 -> Member 로 등록되어있는 사용자만 가능하도록 한다
\ No newline at end of file
diff --git a/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java b/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java
new file mode 100644
index 00000000..9b8dc58a
--- /dev/null
+++ b/src/main/java/nextstep/security/web/authentication/UsernamePasswordAuthenticationInterceptor.java
@@ -0,0 +1,27 @@
+package nextstep.security.web.authentication;
+
+import lombok.extern.slf4j.Slf4j;
+import nextstep.security.core.Authentication;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+
+@Slf4j
+public class UsernamePasswordAuthenticationInterceptor extends AuthenticationInterceptor {
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ // request parameter 로 전달된 정보를 인증 정보로 변환
+ Authentication authRequest = new Authentication(request.getParameter("username"), request.getParameter("password"), null);
+
+ // 인증
+ request.setAttribute("authRequest", authRequest);
+ return super.preHandle(request, response, handler);
+ }
+
+ @Override
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
+ super.afterCompletion(request, response, handler, ex);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/nextstep/security/web/authorization/AuthorizationInterceptor.java b/src/main/java/nextstep/security/web/authorization/AuthorizationInterceptor.java
new file mode 100644
index 00000000..82080891
--- /dev/null
+++ b/src/main/java/nextstep/security/web/authorization/AuthorizationInterceptor.java
@@ -0,0 +1,45 @@
+package nextstep.security.web.authorization;
+
+import nextstep.security.authentication.AuthenticationCredentialsNotFoundException;
+import nextstep.security.authorization.AccessDeniedException;
+import nextstep.security.authorization.AuthorizationDecision;
+import nextstep.security.authorization.AuthorizationManager;
+import nextstep.security.context.SecurityContextHolder;
+import nextstep.security.core.Authentication;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class AuthorizationInterceptor implements HandlerInterceptor {
+
+ private final AuthorizationManager authorizationManager = new AuthorizationManager();
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+
+ try {
+ // 인가 : AuthorizationManger 에게 Authentication 전달하여 권한 확인
+ AuthorizationDecision decision = authorizationManager.check(this::getAuthentication, request);
+
+ // 인가 권한이 안맞다면 예외 발생
+ if (decision != null && !decision.isGranted()) {
+ throw new AccessDeniedException("Unauthorized");
+ }
+
+ } catch (AuthenticationCredentialsNotFoundException | AccessDeniedException e) {
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
+ return false;
+ }
+
+ return true;
+ }
+
+ private Authentication getAuthentication() {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (authentication == null) {
+ throw new AuthenticationCredentialsNotFoundException("Unauthenticated");
+ }
+ return authentication;
+ }
+}
diff --git a/src/main/java/nextstep/util/ApplicationContextProvider.java b/src/main/java/nextstep/util/ApplicationContextProvider.java
new file mode 100644
index 00000000..299aa72d
--- /dev/null
+++ b/src/main/java/nextstep/util/ApplicationContextProvider.java
@@ -0,0 +1,21 @@
+package nextstep.util;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ApplicationContextProvider implements ApplicationContextAware {
+
+ private static ApplicationContext context;
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ context = applicationContext;
+ }
+
+ public static ApplicationContext getApplicationContext() {
+ return context;
+ }
+}
diff --git a/src/test/java/nextstep/app/LoginTest.java b/src/test/java/nextstep/app/LoginTest.java
index 717bcc8a..e83007d6 100644
--- a/src/test/java/nextstep/app/LoginTest.java
+++ b/src/test/java/nextstep/app/LoginTest.java
@@ -1,5 +1,6 @@
package nextstep.app;
+import lombok.extern.slf4j.Slf4j;
import nextstep.app.domain.Member;
import nextstep.app.infrastructure.InmemoryMemberRepository;
import org.junit.jupiter.api.DisplayName;
@@ -9,7 +10,6 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
-import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import javax.servlet.http.HttpSession;
@@ -18,6 +18,7 @@
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+@Slf4j
@SpringBootTest
@AutoConfigureMockMvc
class LoginTest {
diff --git a/src/test/java/nextstep/app/MemberTest.java b/src/test/java/nextstep/app/MemberTest.java
index 58aba17b..5df7ef6b 100644
--- a/src/test/java/nextstep/app/MemberTest.java
+++ b/src/test/java/nextstep/app/MemberTest.java
@@ -42,7 +42,7 @@ void members() throws Exception {
String encoded = Base64Utils.encodeToString(token.getBytes());
ResultActions loginResponse = mockMvc.perform(get("/members")
- .header("Authorization", "Basic " + encoded)
+ .header("Authorization", "Basic " + encoded) //Basic YUBhLmNvbTpwYXNzd29yZA==
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
);