diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java new file mode 100644 index 0000000..b9aecc7 --- /dev/null +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -0,0 +1,66 @@ +package nextstep.app; + +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authentication.BasicAuthenticationFilter; +import nextstep.security.authentication.UsernamePasswordAuthenticationFilter; +import nextstep.security.userdetails.UserDetails; +import nextstep.security.userdetails.UserDetailsService; +import nextstep.security.config.*; +import nextstep.security.context.SecurityContextHolderFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class SecurityConfig { + + private final MemberRepository memberRepository; + public SecurityConfig(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Bean + public DelegatingFilterProxy delegatingFilterProxy() { + return new DelegatingFilterProxy(filterChainProxy(List.of(securityFilterChain()))); + } + + @Bean + public FilterChainProxy filterChainProxy(List securityFilterChains) { + return new FilterChainProxy(securityFilterChains); + } + + + @Bean + public SecurityFilterChain securityFilterChain() { + return new DefaultSecurityFilterChain( + List.of( + + new SecurityContextHolderFilter(), + new UsernamePasswordAuthenticationFilter(userDetailsService()), + new BasicAuthenticationFilter(userDetailsService()) + ) + ); + } + + @Bean + public UserDetailsService userDetailsService() { + return username -> { + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new AuthenticationException("존재하지 않는 사용자입니다.")); + return new UserDetails() { + @Override + public String getUsername() { + return member.getEmail(); + } + @Override + public String getPassword() { + return member.getPassword(); + } + }; + }; + } + +} diff --git a/src/main/java/nextstep/app/WebConfig.java b/src/main/java/nextstep/app/WebConfig.java new file mode 100644 index 0000000..ff55c6e --- /dev/null +++ b/src/main/java/nextstep/app/WebConfig.java @@ -0,0 +1,50 @@ +package nextstep.app; + +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.security.authentication.AuthenticationException; +import nextstep.security.authentication.BasicAuthenticationInterceptor; +import nextstep.security.authentication.FormLoginInterceptor; +import nextstep.security.userdetails.UserDetails; +import nextstep.security.userdetails.UserDetailsService; +import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +//@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final MemberRepository memberRepository; + + public WebConfig(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + System.out.println("WebConfig.addInterceptors"); + registry.addInterceptor(new FormLoginInterceptor(userDetailsService())) + .addPathPatterns("/login"); + registry.addInterceptor(new BasicAuthenticationInterceptor(userDetailsService())) + .addPathPatterns("/members"); + } + + @Bean + public UserDetailsService userDetailsService() { + return username -> { + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new AuthenticationException("존재하지 않는 사용자입니다.")); + return new UserDetails() { + @Override + public String getUsername() { + return member.getEmail(); + } + + @Override + public String getPassword() { + return member.getPassword(); + } + }; + }; + } +} diff --git a/src/main/java/nextstep/app/domain/LoginService.java b/src/main/java/nextstep/app/domain/LoginService.java new file mode 100644 index 0000000..31642f7 --- /dev/null +++ b/src/main/java/nextstep/app/domain/LoginService.java @@ -0,0 +1,24 @@ +package nextstep.app.domain; + +import org.springframework.stereotype.Service; + +@Service +public class LoginService { + + private final MemberRepository memberRepository; + + public LoginService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + /** + * @return null 로그인 실패 + */ + public Member login(String email, String password) { + + return memberRepository.findByEmail(email) + .filter(m -> m.getPassword().equals(password)) + .orElse(null); + } +} + diff --git a/src/main/java/nextstep/app/domain/Member.java b/src/main/java/nextstep/app/domain/Member.java index 6cafa9c..f37a0f3 100644 --- a/src/main/java/nextstep/app/domain/Member.java +++ b/src/main/java/nextstep/app/domain/Member.java @@ -28,4 +28,10 @@ public String getName() { public String getImageUrl() { return imageUrl; } + + public void checkPassword(String password) { + if (!this.password.equals(password)) { + throw new IllegalArgumentException("비밀번호가 일치하지 않습니다."); + } + } } diff --git a/src/main/java/nextstep/app/ui/AuthenticationException.java b/src/main/java/nextstep/app/ui/AuthenticationException.java deleted file mode 100644 index f809b6e..0000000 --- a/src/main/java/nextstep/app/ui/AuthenticationException.java +++ /dev/null @@ -1,4 +0,0 @@ -package nextstep.app.ui; - -public class AuthenticationException extends RuntimeException { -} diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 0ea94f1..d47689d 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,15 +1,12 @@ package nextstep.app.ui; import nextstep.app.domain.MemberRepository; +import nextstep.security.authentication.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"; @@ -20,10 +17,17 @@ public LoginController(MemberRepository memberRepository) { this.memberRepository = memberRepository; } - @PostMapping("/login") - public ResponseEntity login(HttpServletRequest request, HttpSession session) { - return ResponseEntity.ok().build(); - } +// @PostMapping("/login") +// public ResponseEntity login(HttpServletRequest request, HttpSession session) { +// Map parameterMap = request.getParameterMap(); +// String username = parameterMap.get("username")[0]; +// String password = parameterMap.get("password")[0]; +// Member member = memberRepository.findByEmail(username) +// .orElseThrow(AuthenticationException::new); +// member.checkPassword(password); +// session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member); +// return ResponseEntity.ok().build(); +// } @ExceptionHandler(AuthenticationException.class) public ResponseEntity handleAuthenticationException() { diff --git a/src/main/java/nextstep/app/ui/MemberController.java b/src/main/java/nextstep/app/ui/MemberController.java index c8cc74d..4dfd347 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -2,10 +2,14 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; +import nextstep.security.authentication.AuthenticationException; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import javax.servlet.http.HttpServletRequest; import java.util.List; @RestController @@ -18,9 +22,33 @@ public MemberController(MemberRepository memberRepository) { } @GetMapping("/members") - public ResponseEntity> list() { + public ResponseEntity> list(HttpServletRequest request) { +// String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); +// String[] split = authorization.split(" "); +// String type = split[0]; +// String credential = split[1]; +// if (!"Basic".equalsIgnoreCase(type)) { +// throw new AuthenticationException(); +// } +// try { +// String decodedCredential = new String(Base64Utils.decodeFromString(credential)); +// String[] emailAndPassword = decodedCredential.split(":"); +// String email = emailAndPassword[0]; +// String password = emailAndPassword[1]; +// Member member = memberRepository.findByEmail(email).orElseThrow(AuthenticationException::new); +// member.checkPassword(password); +// List members = memberRepository.findAll(); +// return ResponseEntity.ok(members); +// } catch (Exception e) { +// throw new AuthenticationException(); +// } List members = memberRepository.findAll(); return ResponseEntity.ok(members); } -} + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException() { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + +} \ No newline at end of file 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 0000000..ed3c6eb --- /dev/null +++ b/src/main/java/nextstep/security/authentication/Authentication.java @@ -0,0 +1,11 @@ +package nextstep.security.authentication; + +public interface Authentication { + + Object getCredentials(); + + Object getPrincipal(); + + Object isAuthenticated(); + +} diff --git a/src/main/java/nextstep/security/authentication/AuthenticationException.java b/src/main/java/nextstep/security/authentication/AuthenticationException.java new file mode 100644 index 0000000..7fc4aae --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationException.java @@ -0,0 +1,12 @@ +package nextstep.security.authentication; + +public class AuthenticationException extends RuntimeException { + + public AuthenticationException() { + super("인증에 실패하였습니다."); + } + public AuthenticationException(String message) { + super(message); + } + +} 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 0000000..47973f6 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationManager.java @@ -0,0 +1,7 @@ +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 0000000..5bb7ec0 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java @@ -0,0 +1,9 @@ +package nextstep.security.authentication; + +public interface AuthenticationProvider { + + Authentication authenticate(Authentication authentication) throws AuthenticationException; + + boolean supports(Class authentication); + +} diff --git a/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java new file mode 100644 index 0000000..97a4c5c --- /dev/null +++ b/src/main/java/nextstep/security/authentication/BasicAuthenticationFilter.java @@ -0,0 +1,87 @@ +package nextstep.security.authentication; + +import nextstep.security.userdetails.UserDetailsService; +import nextstep.security.context.SecurityContext; +import nextstep.security.context.SecurityContextHolder; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; + +public class BasicAuthenticationFilter extends OncePerRequestFilter { + + public static final String AUTHENTICATION_SCHEME_BASIC = "Basic"; + + private final AuthenticationManager authenticationManager; + + public BasicAuthenticationFilter(UserDetailsService userDetailsService) { + System.out.println("BasicAuthenticationFilter.BasicAuthenticationFilter"); + this.authenticationManager = new ProviderManager(List.of(new DaoAuthenticationProvider(userDetailsService))); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { + System.out.println("BasicAuthenticationFilter.doFilterInternal"); + try { + Authentication authentication = convert(request); + if (authentication == null) { + System.out.println("authentication null"); + filterChain.doFilter(request, response); + return; + } + Authentication authenticate = this.authenticationManager.authenticate(authentication); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authenticate); + SecurityContextHolder.setContext(context); + + filterChain.doFilter(request, response); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } + + private Authentication convert(HttpServletRequest request) { + String header = request.getHeader(HttpHeaders.AUTHORIZATION); + if (header == null) { + return null; + } + header = header.trim(); + if (!StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) { + return null; + } + if (header.equalsIgnoreCase(AUTHENTICATION_SCHEME_BASIC)) { + throw new AuthenticationException(); + } + byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8); + byte[] decoded = decode(base64Token); + String token = new String(decoded, StandardCharsets.UTF_8); + int delim = token.indexOf(":"); + if (delim == -1) { + throw new AuthenticationException(); + } + return UsernamePasswordAuthenticationToken + .unauthenticated(token.substring(0, delim), token.substring(delim + 1)); + } + + private byte[] decode(byte[] base64Token) { + try { + return Base64.getDecoder().decode(base64Token); + } catch (IllegalArgumentException ex) { + throw new AuthenticationException(); + } + } + + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException() { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } +} diff --git a/src/main/java/nextstep/security/authentication/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/authentication/BasicAuthenticationInterceptor.java new file mode 100644 index 0000000..8ee3fc2 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/BasicAuthenticationInterceptor.java @@ -0,0 +1,60 @@ +package nextstep.security.authentication; + +import nextstep.security.userdetails.UserDetails; +import nextstep.security.userdetails.UserDetailsService; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.Base64Utils; +import org.springframework.web.bind.annotation.ExceptionHandler; +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 { + + private final UserDetailsService userDetailsService; + + public BasicAuthenticationInterceptor(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + try { + checkAuthentication(request); + return true; + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + } + + private void checkAuthentication(HttpServletRequest request) { + String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + String[] split = authorization.split(" "); + String type = split[0]; + String credential = split[1]; + if (!"Basic".equalsIgnoreCase(type)) { + throw new AuthenticationException(); + } + String decodedCredential = new String(Base64Utils.decodeFromString(credential)); + String[] emailAndPassword = decodedCredential.split(":"); + String email = emailAndPassword[0]; + String password = emailAndPassword[1]; + UserDetails userDetails = userDetailsService.loadUserByUsername(email); + if (!Objects.equals(userDetails.getPassword(), password)) { + throw new AuthenticationException(); + } + } + + @ExceptionHandler(AuthenticationException.class) + public ResponseEntity handleAuthenticationException() { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + +} 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 0000000..3333a27 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java @@ -0,0 +1,33 @@ +package nextstep.security.authentication; + +import nextstep.security.userdetails.UserDetails; +import nextstep.security.userdetails.UserDetailsService; + +import java.util.Objects; + +public class DaoAuthenticationProvider implements AuthenticationProvider{ + + private final UserDetailsService userDetailsService; + + public DaoAuthenticationProvider(UserDetailsService userDetailsService) { + System.out.println("DaoAuthenticationProvider.DaoAuthenticationProvider"); + this.userDetailsService = userDetailsService; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + System.out.println("DaoAuthenticationProvider.authenticate"); + UserDetails userDetails = userDetailsService.loadUserByUsername(authentication.getPrincipal().toString()); + if(!Objects.equals(userDetails.getPassword(), authentication.getCredentials())) { + System.out.println("userDetails null"); + throw new AuthenticationException(); + } + return UsernamePasswordAuthenticationToken.authenticated(userDetails.getUsername(), userDetails.getPassword()); + } + + @Override + public boolean supports(Class authentication) { + System.out.println("DaoAuthenticationProvider.supports"); + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/nextstep/security/authentication/FormLoginInterceptor.java b/src/main/java/nextstep/security/authentication/FormLoginInterceptor.java new file mode 100644 index 0000000..f5e406c --- /dev/null +++ b/src/main/java/nextstep/security/authentication/FormLoginInterceptor.java @@ -0,0 +1,40 @@ +package nextstep.security.authentication; + +import nextstep.security.userdetails.UserDetails; +import nextstep.security.userdetails.UserDetailsService; +import org.springframework.web.servlet.HandlerInterceptor; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.Map; +import java.util.Objects; + +public class FormLoginInterceptor implements HandlerInterceptor { + + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + private final UserDetailsService userDetailsService; + + public FormLoginInterceptor(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + try { + Map parameterMap = request.getParameterMap(); + String username = parameterMap.get("username")[0]; + String password = parameterMap.get("password")[0]; + + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + if (!Objects.equals(userDetails.getPassword(), password)) { + throw new AuthenticationException(); + } + HttpSession session = request.getSession(); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetails); + } catch (AuthenticationException e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + return false; + } +} \ No newline at end of file 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 0000000..3e4d425 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/ProviderManager.java @@ -0,0 +1,26 @@ +package nextstep.security.authentication; + +import java.util.List; + +public class ProviderManager implements AuthenticationManager{ + + private final List providers; + + public ProviderManager(List providers) { + System.out.println("ProviderManager.ProviderManager"); + this.providers = providers; + } + + @Override + public Authentication authenticate(Authentication authentication) { + System.out.println("ProviderManager.authenticate"); + for (AuthenticationProvider provider : providers) { + System.out.println("provider = " + provider); + if(provider.supports(authentication.getClass())) { + System.out.println("ProviderManager.authenticate"); + return provider.authenticate(authentication); + } + } + return null; + } +} diff --git a/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationFilter.java b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationFilter.java new file mode 100644 index 0000000..9394f35 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationFilter.java @@ -0,0 +1,73 @@ +package nextstep.security.authentication; + +import nextstep.security.userdetails.UserDetailsService; +import nextstep.security.context.HttpSessionSecurityContextRepository; +import nextstep.security.context.SecurityContext; +import nextstep.security.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +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 java.io.IOException; +import java.util.List; +import java.util.Map; + +public class UsernamePasswordAuthenticationFilter extends GenericFilterBean { + + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + private static final String DEFAULT_REQUEST_URI = "/login"; + + private final AuthenticationManager authenticationManager; + private final HttpSessionSecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository(); + + public UsernamePasswordAuthenticationFilter(UserDetailsService userDetailsService) { + System.out.println("UsernamePasswordAuthenticationFilter.UsernamePasswordAuthenticationFilter"); + this.authenticationManager = new ProviderManager(List.of(new DaoAuthenticationProvider(userDetailsService))); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + System.out.println("UsernamePasswordAuthenticationFilter.doFilter"); + if (!DEFAULT_REQUEST_URI.equals(((HttpServletRequest) request).getRequestURI())) { + System.out.println("DEFAULT_REQUEST_URI = " + DEFAULT_REQUEST_URI); + chain.doFilter(request, response); + return; + } + try { + Authentication authentication = convert(request); + if(authentication == null) { + chain.doFilter(request, response); + return; + } + + Authentication authenticate = authenticationManager.authenticate(authentication); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authenticate); + SecurityContextHolder.setContext(context); + +// HttpSession session = ((HttpServletRequest) request).getSession(); +// session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, authenticate); + securityContextRepository.saveContext(context, (HttpServletRequest) request, (HttpServletResponse) response); + + } catch (AuthenticationException e) { + System.out.println("UsernamePasswordAuthenticationFilter-Exception"); + ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } + + private Authentication convert(ServletRequest request) { + System.out.println("UsernamePasswordAuthenticationFilter.convert"); + try { + Map parameterMap = request.getParameterMap(); + String username = parameterMap.get("username")[0]; + String password = parameterMap.get("password")[0]; + return UsernamePasswordAuthenticationToken.unauthenticated(username, password); + } catch (Exception e) { + 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 0000000..abf95fb --- /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) { + System.out.println("UsernamePasswordAuthenticationToken.UsernamePasswordAuthenticationToken"); + this.principal = principal; + this.credentials = credentials; + this.authenticated = authenticated; + } + + public static UsernamePasswordAuthenticationToken unauthenticated(String principal, String credentials) { + System.out.println("UsernamePasswordAuthenticationToken.unauthenticated"); + return new UsernamePasswordAuthenticationToken(principal, credentials, false); + } + + public static UsernamePasswordAuthenticationToken authenticated(String principal, String credentials) { + System.out.println("UsernamePasswordAuthenticationToken.authenticated"); + return new UsernamePasswordAuthenticationToken(principal, credentials, true); + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public Object isAuthenticated() { + return authenticated; + } +} 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 0000000..26e2c58 --- /dev/null +++ b/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java @@ -0,0 +1,24 @@ +package nextstep.security.config; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +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 0000000..efbaa5e --- /dev/null +++ b/src/main/java/nextstep/security/config/DelegatingFilterProxy.java @@ -0,0 +1,18 @@ +package nextstep.security.config; +import org.springframework.web.filter.GenericFilterBean; +import javax.servlet.*; +import java.io.IOException; +public class DelegatingFilterProxy extends GenericFilterBean { + private final Filter delegate; + + public DelegatingFilterProxy(Filter delegate) { + System.out.println("DelegatingFilterProxy.DelegatingFilterProxy"); + this.delegate = delegate; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + System.out.println("DelegatingFilterProxy.doFilter"); + delegate.doFilter(request, response, chain); + } +} 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 0000000..31d8b58 --- /dev/null +++ b/src/main/java/nextstep/security/config/FilterChainProxy.java @@ -0,0 +1,69 @@ +package nextstep.security.config; +import org.springframework.web.filter.GenericFilterBean; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.List; +public class FilterChainProxy extends GenericFilterBean { + private final List filterChains; + + public FilterChainProxy(List filterChains) { + System.out.println("FilterChainProxy.FilterChainProxy"); + this.filterChains = filterChains; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + System.out.println("FilterChainProxy.doFilter"); + List filters = getFilters((HttpServletRequest) request); + VirtualFilterChain virtualFilterChain = new VirtualFilterChain(chain, filters); + virtualFilterChain.doFilter(request, response); + } + + private List getFilters(HttpServletRequest request) { + System.out.println("FilterChainProxy.getFilters"); + System.out.println("request = " + request); + for (SecurityFilterChain chain : this.filterChains) { + System.out.println("chain = " + chain); //DefaultSecurityFilterChain + if (chain.matches(request)) { + System.out.println("chain.getFilters() = " + chain.getFilters()); + return chain.getFilters(); + } + } + return null; + } + + private static final class VirtualFilterChain implements FilterChain { + + private final FilterChain originalChain; + private final List additionalFilters; + private final int size; + private int currentPosition = 0; + + private VirtualFilterChain(FilterChain chain, List additionalFilters) { + System.out.println("VirtualFilterChain.VirtualFilterChain"); + for (Filter additionalFilter : additionalFilters) { + System.out.println("additionalFilter = " + additionalFilter); + } + this.originalChain = chain; + this.additionalFilters = additionalFilters; + this.size = additionalFilters.size(); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + System.out.println("VirtualFilterChain.doFilter"); + if (this.currentPosition == this.size) { + System.out.println("currentPosition = " + currentPosition); + System.out.println("size = " + size); + System.out.println("originalChain = " + originalChain); + this.originalChain.doFilter(request, response); + return; + } + this.currentPosition++; + Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1); + System.out.println("nextFilter = " + nextFilter); + nextFilter.doFilter(request, response, this); + } + } +} \ No newline at end of file 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 0000000..534bea6 --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityFilterChain.java @@ -0,0 +1,13 @@ +package nextstep.security.config; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +public interface SecurityFilterChain { + + boolean matches(HttpServletRequest request); + + List getFilters(); + +} diff --git a/src/main/java/nextstep/security/context/HttpSessionSecurityContextRepository.java b/src/main/java/nextstep/security/context/HttpSessionSecurityContextRepository.java new file mode 100644 index 0000000..37d2475 --- /dev/null +++ b/src/main/java/nextstep/security/context/HttpSessionSecurityContextRepository.java @@ -0,0 +1,29 @@ +package nextstep.security.context; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +public class HttpSessionSecurityContextRepository { + + public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + public SecurityContext loadContext(HttpServletRequest request) { + System.out.println("HttpSessionSecurityContextRepository.loadContext"); + HttpSession session = request.getSession(false); + if(session == null) { + System.out.println("session null"); + + return null; + } + System.out.println("session.getAttribute(SPRING_SECURITY_CONTEXT_KEY) not null"); + return (SecurityContext) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY); + } + + public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { + System.out.println("HttpSessionSecurityContextRepository.saveContext"); + HttpSession session = request.getSession(); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context); + } + +} 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 0000000..076035e --- /dev/null +++ b/src/main/java/nextstep/security/context/SecurityContext.java @@ -0,0 +1,25 @@ +package nextstep.security.context; + +import nextstep.security.authentication.Authentication; + +import java.io.Serializable; + +public class SecurityContext implements Serializable { + + private Authentication authentication; + + public SecurityContext() { + } + + public SecurityContext(Authentication authentication) { + this.authentication = authentication; + } + + public Authentication getAuthentication() { + return authentication; + } + + public void setAuthentication(Authentication authentication) { + this.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 0000000..e675ba7 --- /dev/null +++ b/src/main/java/nextstep/security/context/SecurityContextHolder.java @@ -0,0 +1,38 @@ +package nextstep.security.context; + +public class SecurityContextHolder { + + private static final ThreadLocal contextHolder; + + static { + System.out.println("SecurityContextHolder.static initializer"); + contextHolder = new ThreadLocal<>(); + } + + public static void clearContext() { + System.out.println("SecurityContextHolder.clearContext"); + contextHolder.remove(); + } + + public static SecurityContext getContext() { + System.out.println("SecurityContextHolder.getContext"); + SecurityContext ctx = contextHolder.get(); + if (ctx == null) { + ctx = createEmptyContext(); + contextHolder.set(ctx); + } + return ctx; + } + + public static void setContext(SecurityContext context) { + System.out.println("SecurityContextHolder.setContext"); + if (context != null) { + System.out.println("SecurityContextHolder.setContext - not null"); + contextHolder.set(context); + } + } + public static SecurityContext createEmptyContext() { + System.out.println("SecurityContextHolder.createEmptyContext"); + return new SecurityContext(); + } +} diff --git a/src/main/java/nextstep/security/context/SecurityContextHolderFilter.java b/src/main/java/nextstep/security/context/SecurityContextHolderFilter.java new file mode 100644 index 0000000..1f16a33 --- /dev/null +++ b/src/main/java/nextstep/security/context/SecurityContextHolderFilter.java @@ -0,0 +1,26 @@ +package nextstep.security.context; + +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +public class SecurityContextHolderFilter extends GenericFilterBean { + + private final HttpSessionSecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository(); + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + System.out.println("SecurityContextHolderFilter.doFilter"); + SecurityContext context = securityContextRepository.loadContext((HttpServletRequest) servletRequest); + SecurityContextHolder.setContext(context); + + filterChain.doFilter(servletRequest, servletResponse); + + SecurityContextHolder.clearContext(); + } +} diff --git a/src/main/java/nextstep/security/userdetails/UserDetails.java b/src/main/java/nextstep/security/userdetails/UserDetails.java new file mode 100644 index 0000000..f780bf2 --- /dev/null +++ b/src/main/java/nextstep/security/userdetails/UserDetails.java @@ -0,0 +1,9 @@ +package nextstep.security.userdetails; + +public interface UserDetails { + + String getUsername(); + + String getPassword(); + +} diff --git a/src/main/java/nextstep/security/userdetails/UserDetailsService.java b/src/main/java/nextstep/security/userdetails/UserDetailsService.java new file mode 100644 index 0000000..7b0bd10 --- /dev/null +++ b/src/main/java/nextstep/security/userdetails/UserDetailsService.java @@ -0,0 +1,5 @@ +package nextstep.security.userdetails; + +public interface UserDetailsService { + UserDetails loadUserByUsername(String username); +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..5c804f8 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ +#logging.level.org.springframework=TRACE \ No newline at end of file diff --git a/src/test/java/nextstep/app/LoginTest.java b/src/test/java/nextstep/app/LoginTest.java index 717bcc8..92e8078 100644 --- a/src/test/java/nextstep/app/LoginTest.java +++ b/src/test/java/nextstep/app/LoginTest.java @@ -8,14 +8,18 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpSession; 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.Cookie; import javax.servlet.http.HttpSession; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @@ -65,4 +69,23 @@ void login_fail_with_invalid_password() throws Exception { response.andExpect(status().isUnauthorized()); } + + @DisplayName("로그인 후 세션을 통해 회원 목록 조회") + @Test + void login_after_members() throws Exception { + ResultActions loginResponse = mockMvc.perform(post("/login") + .param("username", TEST_MEMBER.getEmail()) + .param("password", TEST_MEMBER.getPassword()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ).andDo(print()); + loginResponse.andExpect(status().isOk()); + MvcResult loginResult = loginResponse.andReturn(); + HttpSession session = loginResult.getRequest().getSession(); + String sessionId = session.getId(); + ResultActions membersResponse = mockMvc.perform(get("/members") + .cookie(new Cookie("JSESSIONID", sessionId)) + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) + ); + membersResponse.andExpect(status().isOk()); + } }