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..f0180138 --- /dev/null +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -0,0 +1,94 @@ +package nextstep.app.config; + +import jakarta.servlet.Filter; +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.security.UserDetailService; +import nextstep.security.UserDetails; +import nextstep.security.config.AuthenticationManager; +import nextstep.security.config.AuthenticationProvider; +import nextstep.security.config.BasicAuthenticationProvider; +import nextstep.security.config.DaoAuthenticationProvider; +import nextstep.security.config.DefaultSecurityFilterChain; +import nextstep.security.config.FilterChainProxy; +import nextstep.security.config.ProviderManager; +import nextstep.security.config.SecurityContextHolderFilter; +import nextstep.security.config.SecurityFilterChain; +import nextstep.security.filter.BasicAuthFilter; +import nextstep.security.filter.UsernamePasswordAuthFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.DelegatingFilterProxy; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final MemberRepository memberRepository; + + public WebConfig(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Bean + public DelegatingFilterProxy delegatingFilterProxy() { + return new DelegatingFilterProxy(filterChainProxy()); + } + + @Bean + public FilterChainProxy filterChainProxy() { + List securityFilterChains = List.of(securityFilterChain()); + return new FilterChainProxy(securityFilterChains); + } + + @Bean + public SecurityFilterChain securityFilterChain() { + List securityFilters = List.of( + new SecurityContextHolderFilter(), + new BasicAuthFilter(authenticationManager()), + new UsernamePasswordAuthFilter(authenticationManager()) + ); + return new DefaultSecurityFilterChain(securityFilters); + } + + @Bean + public AuthenticationManager authenticationManager() { + List providers = List.of( + daoAuthenticationProvider(), + basicAuthenticationProvider() + ); + return new ProviderManager(providers); + } + + @Bean + public BasicAuthenticationProvider basicAuthenticationProvider() { + return new BasicAuthenticationProvider(userDetailService()); + } + + @Bean + public DaoAuthenticationProvider daoAuthenticationProvider() { + return new DaoAuthenticationProvider(userDetailService()); + } + + @Bean + public UserDetailService userDetailService() { + return username -> { + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new IllegalArgumentException("해당하는 사용자를 찾을 수 없습니다.")); + 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/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java deleted file mode 100644 index cc0fb95d..00000000 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ /dev/null @@ -1,37 +0,0 @@ -package nextstep.app.ui; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; -import nextstep.app.domain.Member; -import nextstep.app.domain.MemberRepository; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Map; - -@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) { - Map parameterMap = request.getParameterMap(); - String username = parameterMap.get("username")[0]; - String password = parameterMap.get("password")[0]; - - Member member = memberRepository.findByEmail(username) - .filter(it -> it.matchPassword(password)) - .orElseThrow(AuthenticationException::new); - - session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, member); - - 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 15cc0395..b2267629 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -2,10 +2,8 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; -import nextstep.app.util.Base64Convertor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -20,22 +18,8 @@ public MemberController(MemberRepository memberRepository) { } @GetMapping("/members") - public ResponseEntity> list(@RequestHeader("Authorization") String authorization) { - try { - String credentials = authorization.split(" ")[1]; - String decodedString = Base64Convertor.decode(credentials); - String[] usernameAndPassword = decodedString.split(":"); - String username = usernameAndPassword[0]; - String password = usernameAndPassword[1]; - - memberRepository.findByEmail(username) - .filter(it -> it.matchPassword(password)) - .orElseThrow(AuthenticationException::new); - - List members = memberRepository.findAll(); - return ResponseEntity.ok(members); - } catch (Exception e) { - throw new AuthenticationException(); - } + public ResponseEntity> list() { + List members = memberRepository.findAll(); + return ResponseEntity.ok(members); } } diff --git a/src/main/java/nextstep/app/ui/AuthenticationException.java b/src/main/java/nextstep/security/AuthenticationException.java similarity index 55% rename from src/main/java/nextstep/app/ui/AuthenticationException.java rename to src/main/java/nextstep/security/AuthenticationException.java index 61aca430..f45eb78b 100644 --- a/src/main/java/nextstep/app/ui/AuthenticationException.java +++ b/src/main/java/nextstep/security/AuthenticationException.java @@ -1,8 +1,18 @@ -package nextstep.app.ui; +package nextstep.security; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(code = HttpStatus.UNAUTHORIZED) public class AuthenticationException extends RuntimeException { + + public AuthenticationException() { + super(); + } + + public AuthenticationException(String message) { + super(message); + + } + } diff --git a/src/main/java/nextstep/security/ProviderNotFoundException.java b/src/main/java/nextstep/security/ProviderNotFoundException.java new file mode 100644 index 00000000..c4f93244 --- /dev/null +++ b/src/main/java/nextstep/security/ProviderNotFoundException.java @@ -0,0 +1,9 @@ +package nextstep.security; + +public class ProviderNotFoundException extends AuthenticationException { + + public ProviderNotFoundException(String message) { + super(message); + } + +} diff --git a/src/main/java/nextstep/security/UserDetailService.java b/src/main/java/nextstep/security/UserDetailService.java new file mode 100644 index 00000000..7de7b4dd --- /dev/null +++ b/src/main/java/nextstep/security/UserDetailService.java @@ -0,0 +1,7 @@ +package nextstep.security; + +public interface UserDetailService { + + UserDetails getUserByUsername(String username); + +} diff --git a/src/main/java/nextstep/security/UserDetails.java b/src/main/java/nextstep/security/UserDetails.java new file mode 100644 index 00000000..29331ea1 --- /dev/null +++ b/src/main/java/nextstep/security/UserDetails.java @@ -0,0 +1,9 @@ +package nextstep.security; + +public interface UserDetails { + + String getUsername(); + + String getPassword(); + +} diff --git a/src/main/java/nextstep/security/config/AbstractAuthenticationToken.java b/src/main/java/nextstep/security/config/AbstractAuthenticationToken.java new file mode 100644 index 00000000..ac8cfd41 --- /dev/null +++ b/src/main/java/nextstep/security/config/AbstractAuthenticationToken.java @@ -0,0 +1,32 @@ +package nextstep.security.config; + +import nextstep.security.UserDetails; + +import java.security.Principal; + +// Implementations which use this class should be immutable. +public abstract class AbstractAuthenticationToken implements Authentication{ + + private Object details; + + @Override + public String getName() { + if (this.getPrincipal() instanceof UserDetails userDetails) { + return userDetails.getUsername(); + } + if (this.getPrincipal() instanceof Principal principal) { + return principal.getName(); + } + return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString(); + } + + public void setDetails(Object details) { + this.details = details; + } + + @Override + public Object getDetails() { + return this.details; + } + +} diff --git a/src/main/java/nextstep/security/config/Authentication.java b/src/main/java/nextstep/security/config/Authentication.java new file mode 100644 index 00000000..ad134914 --- /dev/null +++ b/src/main/java/nextstep/security/config/Authentication.java @@ -0,0 +1,81 @@ +package nextstep.security.config; + + +import java.io.Serializable; +import java.security.Principal; + +/** + * 일단 요청이 AuthenticationManager의 authenticate(Authentication) 메서드에 의해서 진행된다면 + * 인증 요청이나 authenticated principaldㅔ 대한 토큰을 나타낸다. + + * 일단 request가 authenticated 되면, 이 Authentication 객체는 SecurityContextHolder의 SecurityContext의 threadlocal에 저장된다. + * spring security의 authentication 메커니즘을 사용하지 않고 아래와 같이 Authentication 인스턴스를 생성해서 명시적으로 사용가능하다 + *
+ * SecurityContext context = SecurityContextHolder.createEmptyContext();
+ * context.setAuthentication(anAuthentication);
+ * SecurityContextHolder.setContext(context);
+ * 
+ * + * Authentication 객체가 authenticated 프로퍼티 값이 true로 지정되지 않는한, + * 만나는 security interceptor마다 인증된다. + + * 대부분의 경우에 framework가 security context와 Authentication 객체를 를 투명하게 관리해줄것이다. + **/ +public interface Authentication extends Principal, Serializable { + + /** + * principal이 올바른지 확인하는 crendentials. + * 주로 password이나 AuthenticationManager와 관련된 어느것이든 가능하다. + * caller가 이 credentials를 채운다. + * + * @return the credentials that prove the identity of the principal + */ + Object getCredentials(); + + /** + * 인증 요청에 대한 추가적인 details를 저장한다. + * IP 주소나 인증서 일련번호 등 + * + * @return 인증 요청에 대한 추가적인 details. 사용하지 않으면 null + */ + Object getDetails(); + + /** + * + * 인증되는 principal(주체)의 신원. + * username / password로 인증 요청의 경우, username이 된다. + * AuthenticationManager implementation 은 더많은 정보를 가지고 있는 Authentication를 principal로 반환한다. + * UserDetails 객체를 principal로 사용 + * @return 인증의 대상인 Principal이나 인증된 Principal + */ + Object getPrincipal(); + + /** + * AbstractSecurityInterceptor가 인증 토큰을 AuthenticationManager에게 제시해야 하는지 여부를 나타내는 데 사용된다. + * 일반적으로 AuthenticationManager (AuthenticationProvider중 하나) 는 성공적인 인증 후 불변 인증 토큰을 반환하며, + * 그 경우 토큰이 안전하게 이 method에 true로 반환할 수 있다. + * true를 반환하는 것은 performance를 높이고 AuthenticationManager를 매 요청마다 호출하는것은 더이상 불필요하게 된다. + * + * 보안적인 이유로 이 인터페이스 구현체는 불변이거나 처음 생성때부터 변하지 않는 프로퍼티를 보장하는 방법이 있지 않은한 true를 반환하는 것에 매우 주의해야 한다. + * + * @return true if the token has been authenticated and the + * AbstractSecurityInterceptor does not need to present the token to the + * AuthenticationManager again for re-authentication. + */ + boolean isAuthenticated(); + + /** + *

+ * Implementations should always allow this method to be called with a + * false parameter, as this is used by various classes to specify the + * authentication token should not be trusted. If an implementation wishes to reject + * an invocation with a true parameter (which would indicate the + * authentication token is trusted - a potential security risk) the implementation + * should throw an {@link IllegalArgumentException}. + * @param isAuthenticated 는 토큰을 신뢰해야하는 경우 일때 true를 반환한다. + * 토큰을 신뢰하지 말아야하는 경우 false를 반환한다. + * @throws IllegalArgumentException | authentication token을 신뢰하도록 만드는 시도가 실패했을경우 IllegalArgumentException 발생 + */ + void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; + +} diff --git a/src/main/java/nextstep/security/config/AuthenticationManager.java b/src/main/java/nextstep/security/config/AuthenticationManager.java new file mode 100644 index 00000000..65c598d2 --- /dev/null +++ b/src/main/java/nextstep/security/config/AuthenticationManager.java @@ -0,0 +1,7 @@ +package nextstep.security.config; + +public interface AuthenticationManager { + + Authentication authenticate(Authentication authentication); + +} diff --git a/src/main/java/nextstep/security/config/AuthenticationProvider.java b/src/main/java/nextstep/security/config/AuthenticationProvider.java new file mode 100644 index 00000000..3a306d8d --- /dev/null +++ b/src/main/java/nextstep/security/config/AuthenticationProvider.java @@ -0,0 +1,29 @@ +package nextstep.security.config; + +import nextstep.security.AuthenticationException; + +public interface AuthenticationProvider { + + /** + * AuthenticationManager의 authenticate 메서드와 같은 contract으로 인증 수행 + + * @param authentication : 인증 요청 객체 + * @return credential을 포함한 완전히 authenticated 객체를 반환. + * AuthenticationProvider가 받은 Authentication 객체에 대한 인증을 지원하지 않으면 지원null 을 반환 할 수 있음. + * 그러면 Authentication을 지원하는 그 다음 AuthenticationProvider가 시도된다. + */ + Authentication authenticate(Authentication authentication) throws AuthenticationException; + + /** + * + * AuthenticationProvider가 Authentication를 support 하면 true 반환 + * true를 반환하는것이 AuthenticationProvider가 인증할 수 있을것이라고 보장하는 것이 아님. 그냥 더 자세히 평가를 지원한다는 의미. + * 다른 AuthenticationProvider를 시도해봐야한다는 의미로 AuthenticationProvider는 authenticate()결과를 null로 반환 + * 인증 가능한 AuthenticationProvider 선택은 런타임에 ProviderManager에 의해서 이루어짐 + * + * @param authentication + * @return true if the implementation can more closely evaluate the Authentication class presented + */ + boolean supports(Class authentication); + +} diff --git a/src/main/java/nextstep/security/config/BasicAuthenticationProvider.java b/src/main/java/nextstep/security/config/BasicAuthenticationProvider.java new file mode 100644 index 00000000..af3a1ae5 --- /dev/null +++ b/src/main/java/nextstep/security/config/BasicAuthenticationProvider.java @@ -0,0 +1,42 @@ +package nextstep.security.config; + +import nextstep.security.AuthenticationException; +import nextstep.security.UserDetailService; +import nextstep.security.UserDetails; + +public class BasicAuthenticationProvider implements AuthenticationProvider { + + private final UserDetailService userDetailService; + private final PasswordEncoder passwordEncoder; + + public BasicAuthenticationProvider(UserDetailService userDetailService, PasswordEncoder passwordEncoder) { + this.userDetailService = userDetailService; + this.passwordEncoder = passwordEncoder; + } + + public BasicAuthenticationProvider(UserDetailService userDetailService) { + this.userDetailService = userDetailService; + this.passwordEncoder = null; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (!supports(authentication.getClass())) { + return null; + } + String username = (String) authentication.getPrincipal(); + String password = (String) authentication.getCredentials(); + + UserDetails userDetail = userDetailService.getUserByUsername(username); + if (!userDetail.getPassword().equals(password)) { + throw new AuthenticationException(); + } + return new BasicAuthenticationToken(username, password); + } + + @Override + public boolean supports(Class authentication) { + return BasicAuthenticationToken.class.isAssignableFrom(authentication); + } + +} diff --git a/src/main/java/nextstep/security/config/BasicAuthenticationToken.java b/src/main/java/nextstep/security/config/BasicAuthenticationToken.java new file mode 100644 index 00000000..ccedf0e4 --- /dev/null +++ b/src/main/java/nextstep/security/config/BasicAuthenticationToken.java @@ -0,0 +1,56 @@ +package nextstep.security.config; + +import jakarta.servlet.http.HttpServletRequest; +import nextstep.security.AuthenticationException; +import nextstep.security.util.Base64Convertor; + +public class BasicAuthenticationToken extends AbstractAuthenticationToken { + + private final String principal; + private final String credentials; + private boolean authenticated = false; + + + public BasicAuthenticationToken(String authorizationHeader) { + String authType = authorizationHeader.split(" ")[0]; + String credentials = authorizationHeader.split(" ")[1]; + String decodedString = Base64Convertor.decode(credentials); + + checkAuthType(authType); + String[] usernameAndPassword = decodedString.split(":"); + this.principal = usernameAndPassword[0]; + this.credentials = usernameAndPassword[1]; + } + + public BasicAuthenticationToken(String principal, String credentials) { + this.principal = principal; + this.credentials = credentials; + } + + private void checkAuthType(String authType) { + if (!HttpServletRequest.BASIC_AUTH.equalsIgnoreCase(authType)) { + throw new AuthenticationException(); + } + } + + @Override + public Object getCredentials() { + return this.credentials; + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + @Override + public boolean isAuthenticated() { + return this.authenticated; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + this.authenticated = isAuthenticated; + } + +} diff --git a/src/main/java/nextstep/security/config/DaoAuthenticationProvider.java b/src/main/java/nextstep/security/config/DaoAuthenticationProvider.java new file mode 100644 index 00000000..83d69de6 --- /dev/null +++ b/src/main/java/nextstep/security/config/DaoAuthenticationProvider.java @@ -0,0 +1,42 @@ +package nextstep.security.config; + +import nextstep.security.AuthenticationException; +import nextstep.security.UserDetailService; +import nextstep.security.UserDetails; + +public class DaoAuthenticationProvider implements AuthenticationProvider{ + + private final UserDetailService userDetailService; + private final PasswordEncoder passwordEncoder; + + public DaoAuthenticationProvider(UserDetailService userDetailService, PasswordEncoder passwordEncoder) { + this.userDetailService = userDetailService; + this.passwordEncoder = passwordEncoder; + } + + public DaoAuthenticationProvider(UserDetailService userDetailService) { + this.userDetailService = userDetailService; + this.passwordEncoder = null; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (!supports(authentication.getClass())) { + return null; + } + String username = (String) authentication.getPrincipal(); + String password = (String) authentication.getCredentials(); + + UserDetails user = userDetailService.getUserByUsername(username); + if (!password.equals(user.getPassword())) { + throw new AuthenticationException("Passwords do not match"); + } + return new UsernamePasswordAuthenticationToken(username, password, user); + } + + @Override + public boolean supports(Class authentication) { // todo 왜 instanceOf가 아닌 이렇게 클래스로 비교를 하는걸까? + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); + } + +} 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 00000000..f023dc2d --- /dev/null +++ b/src/main/java/nextstep/security/config/DefaultSecurityFilterChain.java @@ -0,0 +1,37 @@ +package nextstep.security.config; + +import jakarta.servlet.Filter; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.List; + +public class DefaultSecurityFilterChain implements SecurityFilterChain { + + private final RequestMatcher requestMatcher; + + private final List filters; + + public DefaultSecurityFilterChain(List filters) { + this(null, filters); + } + + public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List filters) { + this.requestMatcher = requestMatcher; + this.filters = filters; + } + + @Override + public boolean matches(HttpServletRequest request) { + return true; + } + + @Override + public List getFilters() { + return this.filters; + } + + public RequestMatcher getRequestMatcher() { + return requestMatcher; + } + +} 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 00000000..dfcfadd9 --- /dev/null +++ b/src/main/java/nextstep/security/config/FilterChainProxy.java @@ -0,0 +1,83 @@ +package nextstep.security.config; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; +import java.util.List; + +public class FilterChainProxy extends GenericFilterBean { + + private final List filterChains; + + private VirtualFilterChainDecorator virtualFilterChainDecorator = new VirtualFilterChainDecorator(); + + public FilterChainProxy(List filterChains) { + this.filterChains = filterChains; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + List filters = getFilters(request); + + if (filters == null || filters.isEmpty()) { + filterChain.doFilter(request, response); + return; + } + this.virtualFilterChainDecorator.decorate(filterChain, filters).doFilter(request, response); + } + + private List getFilters(HttpServletRequest request) { + return this.filterChains.stream() + .filter(chain -> chain.matches(request)) + .findAny() + .map(SecurityFilterChain::getFilters) + .orElse(null); + } + + public static final class VirtualFilterChainDecorator { + + public FilterChain decorate(FilterChain original, List filters) { + return new VirtualFilterChain(original, filters); + } + + } + + private static class VirtualFilterChain implements FilterChain { + + private final FilterChain originalChain; + private final List additionalFilters; + private final int size; + + private int currentPosition = 0; + + public VirtualFilterChain(FilterChain originalChain, List additionalFilters) { + this.originalChain = originalChain; + this.additionalFilters = additionalFilters; + this.size = additionalFilters.size(); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + if (this.currentPosition == this.size) { + this.originalChain.doFilter(request, response); + return; + } + this.currentPosition++; + Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1); + + nextFilter.doFilter(request, response, this); + } + + } + +} diff --git a/src/main/java/nextstep/security/config/HttpSessionSecurityContextRepository.java b/src/main/java/nextstep/security/config/HttpSessionSecurityContextRepository.java new file mode 100644 index 00000000..f84e3822 --- /dev/null +++ b/src/main/java/nextstep/security/config/HttpSessionSecurityContextRepository.java @@ -0,0 +1,56 @@ +package nextstep.security.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +public class HttpSessionSecurityContextRepository implements SecurityContextRepository { + + private static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + /** + * 현 request의 security context를 가지고와서 반환한다. + * session이 null이면 context 객체는 null을 반환하거나 session에 저장되어 있는 context 객체는 SecurityContext의 인스턴스가 아니다. + * 새로운 context 객체가 생성되고 리턴된다. + * + * @return + */ + /** + * HttpServletReqeust의 getSession(boolean create): + * false: current seesion이 없으면 null 반환 / true: current session 없으면 새로 만든다. + *

+ * 세션이 잘 유지되기 위해서는 getSession을 response가 커밋되기 전에 호출해야한다. + * cookie를 사용해 세션의 정합성을 보장할때, 새로운 세션을 response가 커밋되고 나서 만든다면 IllegalStateException 발생한다. + * getSession() == getSession(true) + */ + @Override + public SecurityContext loadContext(HttpServletRequest request) { + HttpSession session = request.getSession(false); // false: current seesion이 없으면 null 반환 / true: current session 없으면 새로 만든다. + SecurityContext securityContext = readSecurityContextFromSession(session); + if (securityContext == null) { // 세션에 올바른 security context가 없다면 새로 만든다. + securityContext = SecurityContextHolder.createEmptyContext(); + } + return securityContext; + } + + private SecurityContext readSecurityContextFromSession(HttpSession httpSession) { + if (httpSession == null) { + return null; + } + Object contextFromSession = httpSession.getAttribute(SPRING_SECURITY_CONTEXT_KEY); + if (contextFromSession == null) { + return null; + } + if (!(contextFromSession instanceof SecurityContext)) { + return null; + } + return (SecurityContext) contextFromSession; + } + + @Override + public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { + HttpSession session = request.getSession(); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context); + } + +} diff --git a/src/main/java/nextstep/security/config/PasswordEncoder.java b/src/main/java/nextstep/security/config/PasswordEncoder.java new file mode 100644 index 00000000..73031645 --- /dev/null +++ b/src/main/java/nextstep/security/config/PasswordEncoder.java @@ -0,0 +1,14 @@ +package nextstep.security.config; + +public interface PasswordEncoder { + + String encode(CharSequence rawPassword); + + /** + * @param rawPassword the raw password to encode and match + * @param encodedPassword the encoded password from storage to compare with + * @return true if the raw password, after encoding, matches the encoded password from storage + */ + boolean matches(CharSequence rawPassword, String encodedPassword); + +} diff --git a/src/main/java/nextstep/security/config/ProviderManager.java b/src/main/java/nextstep/security/config/ProviderManager.java new file mode 100644 index 00000000..f8129306 --- /dev/null +++ b/src/main/java/nextstep/security/config/ProviderManager.java @@ -0,0 +1,45 @@ +package nextstep.security.config; + +import nextstep.security.ProviderNotFoundException; + +import java.util.Collections; +import java.util.List; + +public class ProviderManager implements AuthenticationManager { + + private List providers = Collections.emptyList(); + + public ProviderManager(List providers) { + this.providers = providers; + } + + @Override + public Authentication authenticate(Authentication authentication) { + Class authenticationClassToTest = authentication.getClass(); + Authentication result = null; + for (AuthenticationProvider provider : this.providers) { + if (!provider.supports(authenticationClassToTest)) { + continue; + } + result = provider.authenticate(authentication); + if (result != null) { // 인증된 객제가 있다면 + copyDetails(authentication, result); + result.setAuthenticated(true); // todo 책임 확인 한번 필요 + break; + } + } + if (result == null) { + throw new ProviderNotFoundException("지원하는 provider가 없습니다!"); + } + return result; + } + + // 인증된 토큰에 detail 값이 없다면 detail 넣어주기 + private void copyDetails(Authentication source, Authentication dest) { + if ((dest instanceof AbstractAuthenticationToken token) && (dest.getDetails() == null)) { + token.setDetails(source.getDetails()); + } + } + +} + diff --git a/src/main/java/nextstep/security/config/RequestMatcher.java b/src/main/java/nextstep/security/config/RequestMatcher.java new file mode 100644 index 00000000..8f4056db --- /dev/null +++ b/src/main/java/nextstep/security/config/RequestMatcher.java @@ -0,0 +1,11 @@ +package nextstep.security.config; + + +import jakarta.servlet.http.HttpServletRequest; + +public interface RequestMatcher { + + boolean matches(HttpServletRequest request); + + +} diff --git a/src/main/java/nextstep/security/config/SecurityContext.java b/src/main/java/nextstep/security/config/SecurityContext.java new file mode 100644 index 00000000..52bf24b5 --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityContext.java @@ -0,0 +1,13 @@ +package nextstep.security.config; + +public interface SecurityContext { + + /** + * 최근에 인증된 principal을 얻거나 authentication request token을 얻는다. + * @return Authentication이나 null을 반환. 인증 정보가 없다면 + */ + Authentication getAuthentication(); + + void setAuthentication(Authentication authentication); + +} diff --git a/src/main/java/nextstep/security/config/SecurityContextHolder.java b/src/main/java/nextstep/security/config/SecurityContextHolder.java new file mode 100644 index 00000000..8896635b --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityContextHolder.java @@ -0,0 +1,31 @@ +package nextstep.security.config; + +public class SecurityContextHolder { + + private static final ThreadLocal contextHolder = new ThreadLocal<>(); + + // 현재 스레드의 SecurityContext 반환 + public static SecurityContext getContext() { + SecurityContext securityContext = contextHolder.get(); + if (securityContext == null) { + securityContext = new SecurityContextImpl(); + contextHolder.set(securityContext); + } + return securityContext; + } + + // 현재 스레드의 SecurityContext 설정 + public static void setContext(SecurityContext securityContext) { + contextHolder.set(securityContext); + } + + //현재 스레드의 SecurityContext 초기화. + public static void clearContext() { + contextHolder.remove(); + } + + public static SecurityContext createEmptyContext() { + return new SecurityContextImpl(); + } + +} diff --git a/src/main/java/nextstep/security/config/SecurityContextHolderFilter.java b/src/main/java/nextstep/security/config/SecurityContextHolderFilter.java new file mode 100644 index 00000000..6731de91 --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityContextHolderFilter.java @@ -0,0 +1,46 @@ +package nextstep.security.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; + +/** + * SecurityContextHolderFilter는 요청 시작 시 SecurityContextRepository로부터 인증 정보를 SecurityContextHolder로 이동시킴 + * 요청 종료 후 SecurityContextHolder를 정리. + * 요청 시작 시: + * SecurityContextRepository.loadContext() 호출. + * 로드한 인증 정보를 SecurityContextHolder에 설정. + * 요청 종료 시: + * SecurityContextHolder.getContext()를 가져와 SecurityContextRepository.saveContext() 호출. + * SecurityContextHolder.clearContext()로 정리. + */ +public class SecurityContextHolderFilter extends GenericFilterBean { + + private final SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository(); + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { + boolean isHttpServlet = servletRequest instanceof HttpServletRequest && servletResponse instanceof HttpServletResponse; + + if (isHttpServlet) { + HttpServletRequest request = (HttpServletRequest) servletRequest; + SecurityContext securityContext = securityContextRepository.loadContext(request); + try { + SecurityContextHolder.setContext(securityContext); + chain.doFilter(servletRequest, servletResponse); + } finally { + SecurityContextHolder.clearContext(); + } + return; + } + throw new ServletException("SecurityContextHolderFilter only supports HTTP requests"); + + } + +} diff --git a/src/main/java/nextstep/security/config/SecurityContextImpl.java b/src/main/java/nextstep/security/config/SecurityContextImpl.java new file mode 100644 index 00000000..e43dc181 --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityContextImpl.java @@ -0,0 +1,17 @@ +package nextstep.security.config; + +public class SecurityContextImpl implements SecurityContext{ + + private Authentication authentication; + + @Override + public Authentication getAuthentication() { + return this.authentication; + } + + @Override + public void setAuthentication(Authentication authentication) { + this.authentication = authentication; + } + +} diff --git a/src/main/java/nextstep/security/config/SecurityContextRepository.java b/src/main/java/nextstep/security/config/SecurityContextRepository.java new file mode 100644 index 00000000..ddb5f926 --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityContextRepository.java @@ -0,0 +1,30 @@ +package nextstep.security.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * 요청들 사이에 SecurityContext를 영속시키는데 사용되는 전략. + + * SecurityContextPersistenceFilter에 의해 사용된다. + * 현재 실행되는 스레드에 사용되는 context를 획득하고 + * 요청이 끝나고 thread-local 스토리지에서 일단 저장되면 저장하는데 사용된다. + + * persistence 매커니즘이 구현에 따라 달라지지만, HttpSession을 사용해 context를 저장하는데 사용한다. + */ +public interface SecurityContextRepository { + + /** + * 공급된 요청에서 security context를 획득한다. + * 인증 안된 사용자에게는 빈 컨텍스트 impl이 반환된다. + * null을 반환해서는 안된다. + * + */ + SecurityContext loadContext(HttpServletRequest request); + + /** + * stores the security context on completion of a request. + * request에 맞는 context가 찾아지면 true 아니면 false를 반환함 + */ + void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response); +} 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 00000000..8857b94d --- /dev/null +++ b/src/main/java/nextstep/security/config/SecurityFilterChain.java @@ -0,0 +1,14 @@ +package nextstep.security.config; + +import jakarta.servlet.Filter; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.List; + +public interface SecurityFilterChain { + + List getFilters(); + + boolean matches(HttpServletRequest request); + +} diff --git a/src/main/java/nextstep/security/config/UsernamePasswordAuthenticationToken.java b/src/main/java/nextstep/security/config/UsernamePasswordAuthenticationToken.java new file mode 100644 index 00000000..e979b830 --- /dev/null +++ b/src/main/java/nextstep/security/config/UsernamePasswordAuthenticationToken.java @@ -0,0 +1,47 @@ +package nextstep.security.config; + +import nextstep.security.UserDetails; + +public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { + + private final String principal; + private final String credentials; + private boolean authenticated = false; + + public UsernamePasswordAuthenticationToken(String principal, String credentials) { + this.principal = principal; + this.credentials = credentials; + } + + public UsernamePasswordAuthenticationToken(String principal, String credentials, UserDetails userDetails) { + this.principal = principal; + this.credentials = credentials; + setDetails(userDetails); + } + + @Override + public Object getCredentials() { + return this.credentials; + } + + @Override + public Object getDetails() { + return super.getDetails(); + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + @Override + public boolean isAuthenticated() { + return this.authenticated; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + this.authenticated = isAuthenticated; + } + +} diff --git a/src/main/java/nextstep/security/filter/BasicAuthFilter.java b/src/main/java/nextstep/security/filter/BasicAuthFilter.java new file mode 100644 index 00000000..4ff2c096 --- /dev/null +++ b/src/main/java/nextstep/security/filter/BasicAuthFilter.java @@ -0,0 +1,85 @@ +package nextstep.security.filter; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.AuthenticationException; +import nextstep.security.config.Authentication; +import nextstep.security.config.AuthenticationManager; +import nextstep.security.config.BasicAuthenticationToken; +import nextstep.security.config.SecurityContext; +import nextstep.security.config.SecurityContextHolder; + +import java.util.List; + +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + +public class BasicAuthFilter implements Filter { + + private static final List TARGET_URL = List.of("/members"); + + private final AuthenticationManager authenticationManager; + + public BasicAuthFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException { + boolean isHttpServlet = servletRequest instanceof HttpServletRequest && servletResponse instanceof HttpServletResponse; + if (isHttpServlet) { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + try { + boolean notTarget = isNotTarget(request); + if (notTarget) { + filterChain.doFilter(request, response); + return; + } + // 타겟일 경우 + checkAuthentication(request); + filterChain.doFilter(request, response); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + return; + } + throw new ServletException("BasicAuthFilter only supports HTTP requests"); + } + + private boolean isNotTarget(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + return TARGET_URL.stream() + .filter(requestURI::startsWith) + .findAny() + .isEmpty(); + } + + private void checkAuthentication(HttpServletRequest request) { + String authorizationHeader = getAuthorizationHeader(request); + if (authorizationHeader == null) throw new AuthenticationException("Missing authorization header"); + + Authentication authentication = authenticationManager.authenticate(new BasicAuthenticationToken(authorizationHeader)); + if (!authentication.isAuthenticated()) { + throw new AuthenticationException(); + } + saveToSecurityContext(authentication); + } + + private void saveToSecurityContext(Authentication authentication) { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(authentication); + SecurityContextHolder.setContext(securityContext); + } + + private String getAuthorizationHeader(HttpServletRequest request) { + return request.getHeader(AUTHORIZATION); + } + + +} diff --git a/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java b/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java new file mode 100644 index 00000000..949e1aff --- /dev/null +++ b/src/main/java/nextstep/security/filter/UsernamePasswordAuthFilter.java @@ -0,0 +1,82 @@ +package nextstep.security.filter; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.config.Authentication; +import nextstep.security.config.AuthenticationManager; +import nextstep.security.config.HttpSessionSecurityContextRepository; +import nextstep.security.config.SecurityContext; +import nextstep.security.config.SecurityContextHolder; +import nextstep.security.config.SecurityContextRepository; +import nextstep.security.config.UsernamePasswordAuthenticationToken; + +import java.io.IOException; +import java.util.List; + +public class UsernamePasswordAuthFilter implements Filter { + + private static final List targetURIList = List.of("/login"); + + private final AuthenticationManager authenticationManager; + private final SecurityContextRepository securityContextRepository; + + public UsernamePasswordAuthFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + this.securityContextRepository = new HttpSessionSecurityContextRepository(); + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + boolean isHttpServlet = servletRequest instanceof HttpServletRequest && servletResponse instanceof HttpServletResponse; + if (isHttpServlet) { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + boolean isNotUsernamePasswordAuthTarget = checkIfAuthTarget(request); + if (isNotUsernamePasswordAuthTarget) { + filterChain.doFilter(request, response); + return; + } + processLogin(request, response); + return; + } + throw new ServletException("UsernamePasswordAuthFilter only supports HTTP requests"); + } + + private boolean checkIfAuthTarget(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + return targetURIList.stream() + .filter(requestURI::startsWith) + .findAny() + .isEmpty(); + } + + private void processLogin(HttpServletRequest request, HttpServletResponse response) { + try { + String username = request.getParameter("username"); + String password = request.getParameter("password"); + Authentication authentication = new UsernamePasswordAuthenticationToken(username, password); + + Authentication authenticationResult = this.authenticationManager.authenticate(authentication); + if (authenticationResult == null || !authenticationResult.isAuthenticated()) { + throw new ServletException("AuthenticationManager should not return null"); + } + addMemberToSession(authenticationResult, request, response); + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } + + private void addMemberToSession(Authentication authentication, HttpServletRequest request, HttpServletResponse response) { + SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + securityContext.setAuthentication(authentication); + SecurityContextHolder.setContext(securityContext); + securityContextRepository.saveContext(securityContext, request, response); + } + +} 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..b9225f9b --- /dev/null +++ b/src/main/java/nextstep/security/interceptor/BasicAuthInterceptor.java @@ -0,0 +1,49 @@ +package nextstep.security.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.AuthenticationException; +import nextstep.security.UserDetailService; +import nextstep.security.UserDetails; +import nextstep.security.util.Base64Convertor; +import org.springframework.web.servlet.HandlerInterceptor; + +public class BasicAuthInterceptor implements HandlerInterceptor { + + private final UserDetailService userDetailService; + + public BasicAuthInterceptor(UserDetailService userDetailService) { + this.userDetailService = userDetailService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + try { + String authorizationHeader = request.getHeader("Authorization"); + String authType = authorizationHeader.split(" ")[0]; + String credentials = authorizationHeader.split(" ")[1]; + String decodedString = Base64Convertor.decode(credentials); + checkAuthType(authType); + + String[] usernameAndPassword = decodedString.split(":"); + String username = usernameAndPassword[0]; + String password = usernameAndPassword[1]; + + UserDetails userDetail = userDetailService.getUserByUsername(username); + if (!userDetail.getPassword().equals(password)) { + throw new AuthenticationException(); + } + return true; + } catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + } + + private void checkAuthType(String authType) { + if (!authType.equalsIgnoreCase(HttpServletRequest.BASIC_AUTH)) { + throw new AuthenticationException(); + } + } + +} diff --git a/src/main/java/nextstep/security/interceptor/FormLoginInterceptor.java b/src/main/java/nextstep/security/interceptor/FormLoginInterceptor.java new file mode 100644 index 00000000..29337763 --- /dev/null +++ b/src/main/java/nextstep/security/interceptor/FormLoginInterceptor.java @@ -0,0 +1,44 @@ +package nextstep.security.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import nextstep.security.AuthenticationException; +import nextstep.security.UserDetailService; +import nextstep.security.UserDetails; +import org.springframework.web.servlet.HandlerInterceptor; + +public class FormLoginInterceptor implements HandlerInterceptor { + + private static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; + + private final UserDetailService userDetailService; + + public FormLoginInterceptor(UserDetailService userDetailService) { + this.userDetailService = userDetailService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + try { + String username = request.getParameter("username"); + String password = request.getParameter("password"); + + UserDetails userDetail = userDetailService.getUserByUsername(username); + if (!userDetail.getPassword().equals(password)) { + throw new AuthenticationException(); + } + + addMemberToSession(request, userDetail); + }catch (Exception e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + return false; + } + + private void addMemberToSession(HttpServletRequest request, UserDetails userDetail) { + HttpSession session = request.getSession(); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, userDetail); + } + +} diff --git a/src/main/java/nextstep/app/util/Base64Convertor.java b/src/main/java/nextstep/security/util/Base64Convertor.java similarity index 64% rename from src/main/java/nextstep/app/util/Base64Convertor.java rename to src/main/java/nextstep/security/util/Base64Convertor.java index 12eb1831..1c1f1e38 100644 --- a/src/main/java/nextstep/app/util/Base64Convertor.java +++ b/src/main/java/nextstep/security/util/Base64Convertor.java @@ -1,8 +1,13 @@ -package nextstep.app.util; +package nextstep.security.util; import java.util.Base64; -public class Base64Convertor { +public final class Base64Convertor { + + private Base64Convertor() { + throw new AssertionError(); + } + public static String encode(String value) { return Base64.getEncoder().encodeToString(value.getBytes()); } @@ -10,4 +15,5 @@ public static String encode(String value) { public static String decode(String value) { return new String(Base64.getDecoder().decode(value)); } + } diff --git a/src/test/java/nextstep/app/BasicAuthTest.java b/src/test/java/nextstep/app/BasicAuthTest.java index 7a4acdfe..3a9cb363 100644 --- a/src/test/java/nextstep/app/BasicAuthTest.java +++ b/src/test/java/nextstep/app/BasicAuthTest.java @@ -2,7 +2,7 @@ import nextstep.app.domain.Member; import nextstep.app.domain.MemberRepository; -import nextstep.app.util.Base64Convertor; +import nextstep.security.util.Base64Convertor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -63,4 +63,16 @@ void members_fail() throws Exception { loginResponse.andDo(print()); loginResponse.andExpect(status().isUnauthorized()); } + + @Test + @DisplayName("Authentication 헤더가 없으면 예외가 발생한다.") + void throwException_when_authentication_header_doesnt_exist() throws Exception { + + ResultActions loginResponse = mockMvc.perform(get("/members") + .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)); + + loginResponse.andDo(print()); + loginResponse.andExpect(status().isUnauthorized()); + } + } diff --git a/src/test/java/nextstep/app/FormLoginTest.java b/src/test/java/nextstep/app/FormLoginTest.java index ea9ccf87..70183a6e 100644 --- a/src/test/java/nextstep/app/FormLoginTest.java +++ b/src/test/java/nextstep/app/FormLoginTest.java @@ -15,11 +15,13 @@ import static org.assertj.core.api.Assertions.assertThat; 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 @AutoConfigureMockMvc class FormLoginTest { + private final Member TEST_MEMBER = new Member("a@a.com", "password", "a", ""); @Autowired @@ -42,6 +44,7 @@ void login_success() throws Exception { .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); + loginResponse.andDo(print()); loginResponse.andExpect(status().isOk()); HttpSession session = loginResponse.andReturn().getRequest().getSession(); @@ -58,6 +61,7 @@ void login_fail_with_no_user() throws Exception { .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); + response.andDo(print()); response.andExpect(status().isUnauthorized()); } @@ -70,6 +74,29 @@ void login_fail_with_invalid_password() throws Exception { .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) ); + response.andDo(print()); response.andExpect(status().isUnauthorized()); } + +// @DisplayName("로그인 후 세션을 통해 회원 목록 조회") +// @Test +// void login_after_members() throws Exception { +// MockHttpSession session = new MockHttpSession(); +// ResultActions loginResponse = mockMvc.perform(post("/login") +// .param("username", TEST_MEMBER.getEmail()) +// .param("password", TEST_MEMBER.getPassword()) +// .session(session) +// .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) +// ).andDo(print()); +// +// loginResponse.andExpect(status().isOk()); +// +// ResultActions membersResponse = mockMvc.perform(get("/members") +// .session(session) +// .contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE) +// ); +// +// membersResponse.andExpect(status().isOk()); +// } + } diff --git a/src/test/java/nextstep/app/SecurityAuthenticationApplicationTests.java b/src/test/java/nextstep/app/SecurityAuthenticationApplicationTests.java deleted file mode 100644 index 5b47bde8..00000000 --- a/src/test/java/nextstep/app/SecurityAuthenticationApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package nextstep.app; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class SecurityAuthenticationApplicationTests { - - @Test - void contextLoads() { - } - -}