diff --git a/README.md b/README.md index 1e7ba65..ad879a8 100644 --- a/README.md +++ b/README.md @@ -1 +1,16 @@ # spring-security-authentication +### 아이디와 비밀번호 기반 로그인 구현 + - 아이디와 비밀번호 확인 기능 + - session 에 인증정보 저장 + +### Basic 인증 구현 + - Basic 유저 정보 추출 + - 인증 기능 + - session 에 인증정보 저장 + +### 인터셉터 분리 + - LoginController에서 IdPasswordAuthInterceptor로 인증 방식 분리 + - MemeberController에서 BasicAuthInterceptor로 인증 방식 분리 + +### 인증 로직과 서비스 로직 간의 패키지 분리 + - 패키지 분리 및 리팩토링 \ No newline at end of file diff --git a/src/main/java/nextstep/app/config/SecurityConfig.java b/src/main/java/nextstep/app/config/SecurityConfig.java new file mode 100644 index 0000000..08d2377 --- /dev/null +++ b/src/main/java/nextstep/app/config/SecurityConfig.java @@ -0,0 +1,63 @@ +package nextstep.app.config; + +import nextstep.app.domain.Member; +import nextstep.app.domain.MemberRepository; +import nextstep.app.ui.AuthenticationException; +import nextstep.security.DefaultSecurityFilterChain; +import nextstep.security.DelegatingFilterProxy; +import nextstep.security.FilterChainProxy; +import nextstep.security.SecurityFilterChain; +import nextstep.security.filter.BasicAuthenticationFilter; +import nextstep.security.filter.UsernamePasswordAuthenticationFilter; +import nextstep.security.model.UserDetails; +import nextstep.security.service.UserDetailsService; +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 BasicAuthenticationFilter(userDetailService()), + new UsernamePasswordAuthenticationFilter(userDetailService()) + )); + } + @Bean + public UserDetailsService userDetailService() { + 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/Member.java b/src/main/java/nextstep/app/domain/Member.java index 6cafa9c..5fe125e 100644 --- a/src/main/java/nextstep/app/domain/Member.java +++ b/src/main/java/nextstep/app/domain/Member.java @@ -1,6 +1,7 @@ package nextstep.app.domain; -public class Member { + +public class Member{ private final String email; private final String password; private final String name; diff --git a/src/main/java/nextstep/app/ui/AuthenticationException.java b/src/main/java/nextstep/app/ui/AuthenticationException.java index f809b6e..95a3112 100644 --- a/src/main/java/nextstep/app/ui/AuthenticationException.java +++ b/src/main/java/nextstep/app/ui/AuthenticationException.java @@ -1,4 +1,11 @@ package nextstep.app.ui; public class AuthenticationException extends RuntimeException { + public AuthenticationException() { + super("인증에 실패하셨습니다."); + } + + public AuthenticationException(String message) { + super(message); + } } diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 0ea94f1..f4d054b 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,27 +1,18 @@ package nextstep.app.ui; -import nextstep.app.domain.MemberRepository; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; @RestController public class LoginController { - public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT"; - private final MemberRepository memberRepository; - - public LoginController(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } @PostMapping("/login") - public ResponseEntity login(HttpServletRequest request, HttpSession session) { + public ResponseEntity login() { 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 c8cc74d..8d771ce 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -5,12 +5,14 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; - +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; import java.util.List; @RestController public class MemberController { + private final MemberRepository memberRepository; public MemberController(MemberRepository memberRepository) { @@ -18,9 +20,9 @@ public MemberController(MemberRepository memberRepository) { } @GetMapping("/members") - public ResponseEntity> list() { + public ResponseEntity> list(HttpServletRequest request, HttpSession session) { List members = memberRepository.findAll(); + return ResponseEntity.ok(members); } - } diff --git a/src/main/java/nextstep/security/DefaultSecurityFilterChain.java b/src/main/java/nextstep/security/DefaultSecurityFilterChain.java new file mode 100644 index 0000000..2b396a4 --- /dev/null +++ b/src/main/java/nextstep/security/DefaultSecurityFilterChain.java @@ -0,0 +1,23 @@ +package nextstep.security; + +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/DelegatingFilterProxy.java b/src/main/java/nextstep/security/DelegatingFilterProxy.java new file mode 100644 index 0000000..b5ebaf5 --- /dev/null +++ b/src/main/java/nextstep/security/DelegatingFilterProxy.java @@ -0,0 +1,19 @@ +package nextstep.security; + +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) { + this.delegate = delegate; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + delegate.doFilter(servletRequest, servletResponse, filterChain); + } +} diff --git a/src/main/java/nextstep/security/FilterChainProxy.java b/src/main/java/nextstep/security/FilterChainProxy.java new file mode 100644 index 0000000..e15d855 --- /dev/null +++ b/src/main/java/nextstep/security/FilterChainProxy.java @@ -0,0 +1,54 @@ +package nextstep.security; + +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 List filterChains; + + public FilterChainProxy(List filterChains) { + this.filterChains = filterChains; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + List filters = getFilters((HttpServletRequest) servletRequest); + VirtualFilterChain virtualFilterChain = new VirtualFilterChain(filterChain, filters); + virtualFilterChain.doFilter(servletRequest, servletResponse); + } + + private List getFilters(HttpServletRequest request) { + for (SecurityFilterChain securityFilterChain : filterChains) { + if (securityFilterChain.matches(request)) { + return securityFilterChain.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) { + this.originalChain = chain; + 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/SecurityFilterChain.java b/src/main/java/nextstep/security/SecurityFilterChain.java new file mode 100644 index 0000000..6fa9aef --- /dev/null +++ b/src/main/java/nextstep/security/SecurityFilterChain.java @@ -0,0 +1,10 @@ +package nextstep.security; + +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/authentication/Authentication.java b/src/main/java/nextstep/security/authentication/Authentication.java new file mode 100644 index 0000000..516a5e9 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/Authentication.java @@ -0,0 +1,7 @@ +package nextstep.security.authentication; + +public interface Authentication { + Object getCredentials(); // token password + Object getPrincipal(); /// token userName + boolean 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..f2da636 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationException.java @@ -0,0 +1,11 @@ +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..ac74b1e --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationManager.java @@ -0,0 +1,5 @@ +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..b75b7fd --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationProvider.java @@ -0,0 +1,9 @@ +package nextstep.security.authentication; + +import nextstep.app.ui.AuthenticationException; + +public interface AuthenticationProvider { + Authentication authenticate(Authentication authentication) throws AuthenticationException; + + boolean supports(Class authentication); +} 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..ccb9e51 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/DaoAuthenticationProvider.java @@ -0,0 +1,29 @@ +package nextstep.security.authentication; + + +import nextstep.security.model.UserDetails; +import nextstep.security.service.UserDetailsService; + +import java.util.Objects; + +public class DaoAuthenticationProvider implements AuthenticationProvider { + private final UserDetailsService userDetailsService; + + public DaoAuthenticationProvider(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + UserDetails userDetails = userDetailsService.loadUserByUsername(authentication.getPrincipal().toString()); + if (!Objects.equals(userDetails.getPassword(), authentication.getCredentials())) { + throw new AuthenticationException(); + } + return UsernamePasswordAuthenticationToken.authenticated(userDetails.getUsername(), userDetails.getPassword()); + } + + @Override + public boolean supports(Class authentication) { + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); + } +} 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..d92266a --- /dev/null +++ b/src/main/java/nextstep/security/authentication/ProviderManager.java @@ -0,0 +1,21 @@ +package nextstep.security.authentication; + +import java.util.List; + +public class ProviderManager implements AuthenticationManager { + private final List providers; + + public ProviderManager(List providers) { + this.providers = providers; + } + + @Override + public Authentication authenticate(Authentication authentication) { + for (AuthenticationProvider provider : providers) { + if (provider.supports(authentication.getClass())) { + return provider.authenticate(authentication); + } + } + 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..c0bf98b --- /dev/null +++ b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java @@ -0,0 +1,38 @@ +package nextstep.security.authentication; + +public class UsernamePasswordAuthenticationToken implements Authentication { + + private final Object principal; + private final Object credentials; + private final boolean authenticated; + + private UsernamePasswordAuthenticationToken(Object principal, Object credentials, boolean authenticated) { + this.principal = principal; + this.credentials = credentials; + this.authenticated = authenticated; + } + + public static UsernamePasswordAuthenticationToken unauthenticated(String principal, String credentials) { + return new UsernamePasswordAuthenticationToken(principal, credentials, false); + } + + + public static UsernamePasswordAuthenticationToken authenticated(String principal, String credentials) { + return new UsernamePasswordAuthenticationToken(principal, credentials, true); + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public boolean isAuthenticated() { + return authenticated; + } +} diff --git a/src/main/java/nextstep/security/filter/BasicAuthenticationFilter.java b/src/main/java/nextstep/security/filter/BasicAuthenticationFilter.java new file mode 100644 index 0000000..1102d5d --- /dev/null +++ b/src/main/java/nextstep/security/filter/BasicAuthenticationFilter.java @@ -0,0 +1,76 @@ +package nextstep.security.filter; + +import nextstep.app.ui.AuthenticationException; +import nextstep.security.authentication.*; +import nextstep.security.service.UserDetailsService; +import org.springframework.http.HttpHeaders; +import org.springframework.util.StringUtils; +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) { + this.authenticationManager = new ProviderManager( + List.of(new DaoAuthenticationProvider(userDetailsService)) + ); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { + try { + Authentication authentication = convert(request); + if (authentication == null) { + filterChain.doFilter(request, response); + return; + } + + this.authenticationManager.authenticate(authentication); + + 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(); + } + } +} diff --git a/src/main/java/nextstep/security/filter/UsernamePasswordAuthenticationFilter.java b/src/main/java/nextstep/security/filter/UsernamePasswordAuthenticationFilter.java new file mode 100644 index 0000000..80a044b --- /dev/null +++ b/src/main/java/nextstep/security/filter/UsernamePasswordAuthenticationFilter.java @@ -0,0 +1,67 @@ +package nextstep.security.filter; + +import nextstep.app.ui.AuthenticationException; +import nextstep.security.authentication.*; +import nextstep.security.model.UserDetails; +import nextstep.security.service.UserDetailsService; +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 javax.servlet.http.HttpSession; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +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; + + public UsernamePasswordAuthenticationFilter(UserDetailsService userDetailsService) { + this.authenticationManager = new ProviderManager( + List.of(new DaoAuthenticationProvider(userDetailsService)) + ); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + if (!DEFAULT_REQUEST_URI.equals(((HttpServletRequest) request).getRequestURI())) { + chain.doFilter(request, response); + return; + } + + try { + Authentication authentication = convert(request); + if (authentication == null) { + chain.doFilter(request, response); + return; + } + + Authentication authenticate = this.authenticationManager.authenticate(authentication); + + HttpSession session = ((HttpServletRequest) request).getSession(); + session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, authenticate); + + } catch (Exception e) { + ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } + + private Authentication convert(ServletRequest request) { + 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; + } + + } +} \ No newline at end of file diff --git a/src/main/java/nextstep/security/model/UserDetails.java b/src/main/java/nextstep/security/model/UserDetails.java new file mode 100644 index 0000000..d59bf39 --- /dev/null +++ b/src/main/java/nextstep/security/model/UserDetails.java @@ -0,0 +1,6 @@ +package nextstep.security.model; + +public interface UserDetails { + String getUsername(); + String getPassword(); +} diff --git a/src/main/java/nextstep/security/service/UserDetailsService.java b/src/main/java/nextstep/security/service/UserDetailsService.java new file mode 100644 index 0000000..0e88f33 --- /dev/null +++ b/src/main/java/nextstep/security/service/UserDetailsService.java @@ -0,0 +1,9 @@ +package nextstep.security.service; + +import nextstep.security.model.UserDetails; + + +public interface UserDetailsService { + UserDetails loadUserByUsername(String username); + +}