diff --git a/README.md b/README.md index 1e7ba65..9444f1d 100644 --- a/README.md +++ b/README.md @@ -1 +1,7 @@ # spring-security-authentication + +## 구현 기능 목록 +* refactor: formLoginAuthInterceptor +* feat: 로그인 기능 구현 +* feat: 인증 검사 인터셉터 추가 +* feat: basic 인증 인터셉터 추가 \ 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..005feb6 --- /dev/null +++ b/src/main/java/nextstep/app/config/SecurityConfig.java @@ -0,0 +1,48 @@ +package nextstep.app.config; + +import nextstep.security.DefaultSecurityFilterChain; +import nextstep.security.filter.AuthorizationFilter; +import nextstep.security.filter.BasicAuthenticationFilter; +import nextstep.security.filter.FilterChainProxy; +import nextstep.security.filter.FormLoginAuthFilter; +import nextstep.security.manager.ProviderManager; +import nextstep.security.provider.UsernamePasswordProvider; +import nextstep.security.service.UserDetailsService; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.GenericFilterBean; + +import java.util.List; + +@Configuration +public class SecurityConfig { + + private final UserDetailsService userDetailsService; + + public SecurityConfig(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Bean + public GenericFilterBean delegatingFilterProxy() { + return new FilterChainProxy(List.of( + new DefaultSecurityFilterChain("/login", List.of( + new FormLoginAuthFilter(new ProviderManager(List.of(new UsernamePasswordProvider(userDetailsService)))), + new AuthorizationFilter() + )), + new DefaultSecurityFilterChain("/members", List.of( + new BasicAuthenticationFilter(new ProviderManager(List.of(new UsernamePasswordProvider(userDetailsService)))), + new AuthorizationFilter() + )) + )); + } + + @Bean + public FilterRegistrationBean delegatingFilterProxyFilterRegistrationBean(GenericFilterBean delegatingFilterProxy) { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(delegatingFilterProxy); + registrationBean.addUrlPatterns("/*"); + return registrationBean; + } +} 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 0000000..4910bf0 --- /dev/null +++ b/src/main/java/nextstep/app/config/WebConfig.java @@ -0,0 +1,25 @@ +package nextstep.app.config; + +import nextstep.security.interceptor.AuthorizationInterceptor; +import nextstep.security.interceptor.BasicAuthenticationInterceptor; +import nextstep.security.interceptor.FormLoginAuthInterceptor; +import nextstep.security.service.UserDetailsService; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +//@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final UserDetailsService userDetailsService; + + public WebConfig(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new BasicAuthenticationInterceptor(userDetailsService)); + registry.addInterceptor(new FormLoginAuthInterceptor(userDetailsService)).addPathPatterns("/login"); + registry.addInterceptor(new AuthorizationInterceptor()).addPathPatterns("/members"); + } +} diff --git a/src/main/java/nextstep/app/domain/Member.java b/src/main/java/nextstep/app/domain/Member.java index 6cafa9c..e20f931 100644 --- a/src/main/java/nextstep/app/domain/Member.java +++ b/src/main/java/nextstep/app/domain/Member.java @@ -1,6 +1,8 @@ package nextstep.app.domain; -public class Member { +import nextstep.security.userdetails.UserDetails; + +public class Member implements UserDetails { private final String email; private final String password; private final String name; @@ -13,10 +15,12 @@ public Member(String email, String password, String name, String imageUrl) { this.imageUrl = imageUrl; } + @Override public String getEmail() { return email; } + @Override public String getPassword() { return password; } diff --git a/src/main/java/nextstep/app/domain/MemberService.java b/src/main/java/nextstep/app/domain/MemberService.java new file mode 100644 index 0000000..dce6e30 --- /dev/null +++ b/src/main/java/nextstep/app/domain/MemberService.java @@ -0,0 +1,22 @@ +package nextstep.app.domain; + +import nextstep.security.service.UserDetailsService; +import nextstep.security.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +@Service +public class MemberService implements UserDetailsService { + + private final MemberRepository memberRepository; + + public MemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public UserDetails loadUserByEmailAndPassword(String email, String password) { + return memberRepository.findByEmail(email) + .filter(v -> v.getPassword().equals(password)) + .orElse(null); + } +} diff --git a/src/main/java/nextstep/app/ui/LoginController.java b/src/main/java/nextstep/app/ui/LoginController.java index 0ea94f1..ef549f4 100644 --- a/src/main/java/nextstep/app/ui/LoginController.java +++ b/src/main/java/nextstep/app/ui/LoginController.java @@ -1,6 +1,5 @@ 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; @@ -14,12 +13,6 @@ 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) { 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..b226762 100644 --- a/src/main/java/nextstep/app/ui/MemberController.java +++ b/src/main/java/nextstep/app/ui/MemberController.java @@ -22,5 +22,4 @@ public ResponseEntity> list() { 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..9f65a41 --- /dev/null +++ b/src/main/java/nextstep/security/DefaultSecurityFilterChain.java @@ -0,0 +1,26 @@ +package nextstep.security; + +import javax.servlet.Filter; +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +public class DefaultSecurityFilterChain implements SecurityFilterChain { + + private final String path; + private final List filters; + + public DefaultSecurityFilterChain(String path, List filters) { + this.path = path; + this.filters = filters; + } + + @Override + public boolean matches(HttpServletRequest request) { + return request.getRequestURI().startsWith(path); + } + + @Override + public List getFilters() { + return filters; + } +} diff --git a/src/main/java/nextstep/security/SecurityFilterChain.java b/src/main/java/nextstep/security/SecurityFilterChain.java new file mode 100644 index 0000000..42ac819 --- /dev/null +++ b/src/main/java/nextstep/security/SecurityFilterChain.java @@ -0,0 +1,12 @@ +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..e409fbd --- /dev/null +++ b/src/main/java/nextstep/security/authentication/Authentication.java @@ -0,0 +1,6 @@ +package nextstep.security.authentication; + +public interface Authentication { + Object getCredentials(); + Object getPrincipal(); +} 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..acc78e0 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/UsernamePasswordAuthenticationToken.java @@ -0,0 +1,21 @@ +package nextstep.security.authentication; + +public class UsernamePasswordAuthenticationToken implements Authentication { + private final Object principal; + private final Object credentials; + + public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { + this.principal = principal; + this.credentials = credentials; + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public Object getPrincipal() { + return principal; + } +} diff --git a/src/main/java/nextstep/security/context/UserContextHolder.java b/src/main/java/nextstep/security/context/UserContextHolder.java new file mode 100644 index 0000000..4715afa --- /dev/null +++ b/src/main/java/nextstep/security/context/UserContextHolder.java @@ -0,0 +1,28 @@ +package nextstep.security.context; + +import nextstep.security.userdetails.UserDetails; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +import static nextstep.security.interceptor.BasicAuthenticationInterceptor.SPRING_SECURITY_CONTEXT; + +public final class UserContextHolder { + + private UserContextHolder() {} + + public static UserDetails getUser() { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + return null; + } + return (UserDetails) attributes.getAttribute(SPRING_SECURITY_CONTEXT, RequestAttributes.SCOPE_SESSION); + } + + public static void setUser(UserDetails userDetails) { + RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + return; + } + attributes.setAttribute(SPRING_SECURITY_CONTEXT, userDetails, RequestAttributes.SCOPE_SESSION); + } +} diff --git a/src/main/java/nextstep/security/filter/AuthorizationFilter.java b/src/main/java/nextstep/security/filter/AuthorizationFilter.java new file mode 100644 index 0000000..a72616e --- /dev/null +++ b/src/main/java/nextstep/security/filter/AuthorizationFilter.java @@ -0,0 +1,23 @@ +package nextstep.security.filter; + +import nextstep.security.context.UserContextHolder; +import nextstep.security.userdetails.UserDetails; +import org.springframework.http.HttpStatus; + +import javax.servlet.*; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class AuthorizationFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + UserDetails userDetails = UserContextHolder.getUser(); + if (userDetails == null) { + ((HttpServletResponse) response).setStatus(HttpStatus.UNAUTHORIZED.value()); + return; + } + + chain.doFilter(request, response); + } +} 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..bff3d96 --- /dev/null +++ b/src/main/java/nextstep/security/filter/BasicAuthenticationFilter.java @@ -0,0 +1,79 @@ +package nextstep.security.filter; + +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; +import nextstep.security.context.UserContextHolder; +import nextstep.security.manager.AuthenticationManager; +import nextstep.security.userdetails.UserDetails; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class BasicAuthenticationFilter implements Filter { + + public static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; + private static final String BASIC = "Basic"; + private static final String BLANK = " "; + private static final String COLON = ":"; + + private final AuthenticationManager authenticationManager; + + public BasicAuthenticationFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + String[] credentials = parseBasicHeader((HttpServletRequest) request); + if (credentials == null) { + chain.doFilter(request, response); + return; + } + + Authentication authentication = this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(credentials[0], credentials[1])); + if (authentication != null) { + UserContextHolder.setUser((UserDetails) authentication.getPrincipal()); + } + chain.doFilter(request, response); + } + + @Nullable + private String[] parseBasicHeader(HttpServletRequest request) { + // authorization 헤더를 조회한다. + String authorizationValue = request.getHeader(HttpHeaders.AUTHORIZATION); + if (authorizationValue == null || authorizationValue.isBlank()) { + return null; + } + + // Basic authentication 방식인지 확인한다. + String[] strs = authorizationValue.split(BLANK); + if (strs.length != 2) { + return null; + } + + String authType = strs[0]; + String authValue = strs[1]; + + // Basic authentication 방식이 아닌 경우 null을 반환한다. + if (!BASIC.equalsIgnoreCase(authType)) { + return null; + } + + String decodedBasicValue = new String(Base64.getDecoder().decode(authValue), StandardCharsets.UTF_8); + if (decodedBasicValue.isBlank()) { + return null; + } + + strs = decodedBasicValue.split(COLON); + if (strs.length != 2) { + return null; + } + + return new String[]{strs[0], strs[1]}; + } +} diff --git a/src/main/java/nextstep/security/filter/FilterChainProxy.java b/src/main/java/nextstep/security/filter/FilterChainProxy.java new file mode 100644 index 0000000..282595c --- /dev/null +++ b/src/main/java/nextstep/security/filter/FilterChainProxy.java @@ -0,0 +1,89 @@ +package nextstep.security.filter; + +import nextstep.security.SecurityFilterChain; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public class FilterChainProxy extends GenericFilterBean { + + private final List filterChains; + private final FilterChainDecorator filterChainDecorator; + + public FilterChainProxy(List filterChains) { + this.filterChains = filterChains; + this.filterChainDecorator = new VirtualFilterChainDecorator(); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + doFilterInternal(request, response, chain); + } + + private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { + // 1. 요청에 맞는 체인 안에 필터 조회 + List filters = getFilters((HttpServletRequest) request); + + // 2. 필터 실행 + filterChainDecorator.decorate(chain, filters) + .doFilter(request, response); + } + + private List getFilters(HttpServletRequest request) { + return filterChains.stream() + .filter(v -> v.matches(request)) + .map(SecurityFilterChain::getFilters) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + public interface FilterChainDecorator { + default FilterChain decorate(FilterChain original) { + return this.decorate(original, List.of()); + } + + FilterChain decorate(FilterChain original, List filters); + } + + public static final class VirtualFilterChainDecorator implements FilterChainDecorator { + public VirtualFilterChainDecorator() { + } + + public FilterChain decorate(FilterChain original) { + return original; + } + + public FilterChain decorate(FilterChain original, List filters) { + return new VirtualFilterChain(original, filters); + } + } + + 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(); + } + + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + if (this.currentPosition == this.size) { + this.originalChain.doFilter(request, response); + } else { + // 현재 포지션 위치 찾는 거 였음.. + ++this.currentPosition; + Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1); + nextFilter.doFilter(request, response, this); + } + } + } +} diff --git a/src/main/java/nextstep/security/filter/FormLoginAuthFilter.java b/src/main/java/nextstep/security/filter/FormLoginAuthFilter.java new file mode 100644 index 0000000..39f79bc --- /dev/null +++ b/src/main/java/nextstep/security/filter/FormLoginAuthFilter.java @@ -0,0 +1,33 @@ +package nextstep.security.filter; + +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; +import nextstep.security.context.UserContextHolder; +import nextstep.security.manager.AuthenticationManager; +import nextstep.security.userdetails.UserDetails; + +import javax.servlet.*; +import java.io.IOException; +import java.util.Map; + +public class FormLoginAuthFilter implements Filter { + + private final AuthenticationManager authenticationManager; + + public FormLoginAuthFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + Map paramMap = request.getParameterMap(); + String email = paramMap.get("username")[0]; + String password = paramMap.get("password")[0]; + + Authentication authentication = this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(email, password)); + if (authentication != null) { + UserContextHolder.setUser((UserDetails) authentication.getPrincipal()); + } + chain.doFilter(request, response); + } +} diff --git a/src/main/java/nextstep/security/interceptor/AuthorizationInterceptor.java b/src/main/java/nextstep/security/interceptor/AuthorizationInterceptor.java new file mode 100644 index 0000000..dec0836 --- /dev/null +++ b/src/main/java/nextstep/security/interceptor/AuthorizationInterceptor.java @@ -0,0 +1,22 @@ +package nextstep.security.interceptor; + +import nextstep.security.context.UserContextHolder; +import nextstep.security.userdetails.UserDetails; +import org.springframework.http.HttpStatus; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class AuthorizationInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + UserDetails userDetails = UserContextHolder.getUser(); + if (userDetails == null) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + return false; + } + return true; + } +} diff --git a/src/main/java/nextstep/security/interceptor/BasicAuthenticationInterceptor.java b/src/main/java/nextstep/security/interceptor/BasicAuthenticationInterceptor.java new file mode 100644 index 0000000..e73ebd9 --- /dev/null +++ b/src/main/java/nextstep/security/interceptor/BasicAuthenticationInterceptor.java @@ -0,0 +1,76 @@ +package nextstep.security.interceptor; + +import nextstep.security.context.UserContextHolder; +import nextstep.security.service.UserDetailsService; +import nextstep.security.userdetails.UserDetails; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class BasicAuthenticationInterceptor implements HandlerInterceptor { + + public static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; + private static final String BASIC = "Basic"; + private static final String BLANK = " "; + private static final String COLON = ":"; + + private final UserDetailsService userDetailsService; + + public BasicAuthenticationInterceptor(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String[] authValue = parseBasicHeader(request); + if (authValue == null) { + return true; + } + + UserDetails userDetails = userDetailsService.loadUserByEmailAndPassword(authValue[0], authValue[1]); + if (userDetails != null) { + UserContextHolder.setUser(userDetails); + } + return true; + } + + @Nullable + private String[] parseBasicHeader(HttpServletRequest request) { + // authorization 헤더를 조회한다. + String authorizationValue = request.getHeader(HttpHeaders.AUTHORIZATION); + if (authorizationValue == null || authorizationValue.isBlank()) { + return null; + } + + // Basic authentication 방식인지 확인한다. + String[] strs = authorizationValue.split(BLANK); + if (strs.length != 2) { + return null; + } + + String authType = strs[0]; + String authValue = strs[1]; + + // Basic authentication 방식이 아닌 경우 null을 반환한다. + if (!BASIC.equalsIgnoreCase(authType)) { + return null; + } + + String decodedBasicValue = new String(Base64.getDecoder().decode(authValue), StandardCharsets.UTF_8); + if (decodedBasicValue.isBlank()) { + return null; + } + + strs = decodedBasicValue.split(COLON); + if (strs.length != 2) { + return null; + } + + return new String[]{strs[0], strs[1]}; + } +} diff --git a/src/main/java/nextstep/security/interceptor/FormLoginAuthInterceptor.java b/src/main/java/nextstep/security/interceptor/FormLoginAuthInterceptor.java new file mode 100644 index 0000000..c4b99b7 --- /dev/null +++ b/src/main/java/nextstep/security/interceptor/FormLoginAuthInterceptor.java @@ -0,0 +1,34 @@ +package nextstep.security.interceptor; + +import nextstep.app.ui.AuthenticationException; +import nextstep.security.context.UserContextHolder; +import nextstep.security.service.UserDetailsService; +import nextstep.security.userdetails.UserDetails; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +public class FormLoginAuthInterceptor implements HandlerInterceptor { + + private final UserDetailsService userDetailsService; + + public FormLoginAuthInterceptor(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + Map paramMap = request.getParameterMap(); + String email = paramMap.get("username")[0]; + String password = paramMap.get("password")[0]; + + UserDetails userDetails = userDetailsService.loadUserByEmailAndPassword(email, password); + if (userDetails == null) { + throw new AuthenticationException(); + } + UserContextHolder.setUser(userDetails); + return true; + } +} diff --git a/src/main/java/nextstep/security/manager/AuthenticationManager.java b/src/main/java/nextstep/security/manager/AuthenticationManager.java new file mode 100644 index 0000000..3c8ad96 --- /dev/null +++ b/src/main/java/nextstep/security/manager/AuthenticationManager.java @@ -0,0 +1,8 @@ +package nextstep.security.manager; + +import nextstep.security.authentication.Authentication; + +public interface AuthenticationManager { + + Authentication authenticate(Authentication authentication); +} diff --git a/src/main/java/nextstep/security/manager/ProviderManager.java b/src/main/java/nextstep/security/manager/ProviderManager.java new file mode 100644 index 0000000..a2c2500 --- /dev/null +++ b/src/main/java/nextstep/security/manager/ProviderManager.java @@ -0,0 +1,24 @@ +package nextstep.security.manager; + +import nextstep.security.authentication.Authentication; +import nextstep.security.provider.AuthenticationProvider; + +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) { + return providers.stream() + .filter(v -> v.supports(authentication.getClass())) + .map(v -> v.authenticate(authentication)) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/nextstep/security/provider/AuthenticationProvider.java b/src/main/java/nextstep/security/provider/AuthenticationProvider.java new file mode 100644 index 0000000..ed4094f --- /dev/null +++ b/src/main/java/nextstep/security/provider/AuthenticationProvider.java @@ -0,0 +1,10 @@ +package nextstep.security.provider; + +import nextstep.security.authentication.Authentication; + +public interface AuthenticationProvider { + + boolean supports(Class authentication); + + Authentication authenticate(Authentication authentication); +} diff --git a/src/main/java/nextstep/security/provider/UsernamePasswordProvider.java b/src/main/java/nextstep/security/provider/UsernamePasswordProvider.java new file mode 100644 index 0000000..9cc5a45 --- /dev/null +++ b/src/main/java/nextstep/security/provider/UsernamePasswordProvider.java @@ -0,0 +1,28 @@ +package nextstep.security.provider; + +import nextstep.security.authentication.Authentication; +import nextstep.security.authentication.UsernamePasswordAuthenticationToken; +import nextstep.security.service.UserDetailsService; +import nextstep.security.userdetails.UserDetails; + +public class UsernamePasswordProvider implements AuthenticationProvider { + + private final UserDetailsService userDetailsService; + + public UsernamePasswordProvider(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public boolean supports(Class authentication) { + return authentication.isAssignableFrom(UsernamePasswordAuthenticationToken.class); + } + + @Override + public Authentication authenticate(Authentication authentication) { + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) authentication; + + UserDetails userDetails = userDetailsService.loadUserByEmailAndPassword((String) usernamePasswordAuthenticationToken.getPrincipal(), (String) usernamePasswordAuthenticationToken.getCredentials()); + return new UsernamePasswordAuthenticationToken(userDetails, null); + } +} 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..34685e8 --- /dev/null +++ b/src/main/java/nextstep/security/service/UserDetailsService.java @@ -0,0 +1,15 @@ +package nextstep.security.service; + +import nextstep.security.userdetails.UserDetails; + +public interface UserDetailsService { + + /** + * 사용자 이름을 기반으로 사용자 정보를 가져온다. + * + * @param email 사용자 이메일 + * @param password 사용자 비밀번호 + * @return 사용자 정보 + */ + UserDetails loadUserByEmailAndPassword(String email, String password); +} 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..0287221 --- /dev/null +++ b/src/main/java/nextstep/security/userdetails/UserDetails.java @@ -0,0 +1,6 @@ +package nextstep.security.userdetails; + +public interface UserDetails { + String getEmail(); + String getPassword(); +}