diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 03b238f..866428d 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -1,61 +1,40 @@ package nextstep.app; import nextstep.oauth2.OAuth2ClientProperties; -import nextstep.oauth2.authentication.OAuth2LoginAuthenticationProvider; -import nextstep.oauth2.registration.ClientRegistration; -import nextstep.oauth2.registration.ClientRegistrationRepository; -import nextstep.oauth2.userinfo.OAuth2UserService; -import nextstep.oauth2.web.OAuth2AuthorizationRequestRedirectFilter; -import nextstep.oauth2.web.OAuth2AuthorizedClientRepository; -import nextstep.oauth2.web.OAuth2LoginAuthenticationFilter; -import nextstep.security.access.AnyRequestMatcher; -import nextstep.security.access.MvcRequestMatcher; -import nextstep.security.access.RequestMatcherEntry; import nextstep.security.access.hierarchicalroles.RoleHierarchy; import nextstep.security.access.hierarchicalroles.RoleHierarchyImpl; -import nextstep.security.authentication.*; -import nextstep.security.authorization.*; -import nextstep.security.config.DefaultSecurityFilterChain; -import nextstep.security.config.DelegatingFilterProxy; -import nextstep.security.config.FilterChainProxy; +import nextstep.security.authorization.SecuredMethodInterceptor; import nextstep.security.config.SecurityFilterChain; -import nextstep.security.context.SecurityContextHolderFilter; -import nextstep.security.userdetails.UserDetailsService; +import nextstep.security.config.annotation.EnableWebSecurity; +import nextstep.security.config.annotation.HttpSecurity; +import nextstep.security.config.annotation.configurer.Customizer; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; -import org.springframework.http.HttpMethod; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; @Configuration @EnableAspectJAutoProxy @EnableConfigurationProperties(OAuth2ClientProperties.class) +@EnableWebSecurity public class SecurityConfig { - private final UserDetailsService userDetailsService; - private final OAuth2UserService oAuth2UserService; - private final OAuth2ClientProperties oAuth2ClientProperties; - - public SecurityConfig(UserDetailsService userDetailsService, OAuth2UserService oAuth2UserService, OAuth2ClientProperties oAuth2ClientProperties) { - this.userDetailsService = userDetailsService; - this.oAuth2UserService = oAuth2UserService; - this.oAuth2ClientProperties = oAuth2ClientProperties; - } - - @Bean - public DelegatingFilterProxy delegatingFilterProxy() { - return new DelegatingFilterProxy(filterChainProxy(List.of(securityFilterChain()))); - } - @Bean - public FilterChainProxy filterChainProxy(List securityFilterChains) { - return new FilterChainProxy(securityFilterChains); + public SecurityFilterChain securityFilterChain(HttpSecurity http) { + return http + .csrf(c -> c.ignoringRequestMatchers("/login", "/logout")) + .authorizeHttpRequests( + authorizeHttp -> { + authorizeHttp.requestMatchers("/members").hasRole("ADMIN"); + authorizeHttp.requestMatchers("/members/me").hasRole("USER"); + authorizeHttp.anyRequest().permitAll(); + } + ) + .httpBasic(Customizer.withDefaults()) + .formLogin(Customizer.withDefaults()) + .oauth2Login(Customizer.withDefaults()) + .build(); } @Bean @@ -69,53 +48,4 @@ public RoleHierarchy roleHierarchy() { .role("ADMIN").implies("USER") .build(); } - - @Bean - public AuthenticationManager authenticationManager() { - return new ProviderManager(List.of( - new DaoAuthenticationProvider(userDetailsService), - new OAuth2LoginAuthenticationProvider(oAuth2UserService))); - } - - @Bean - public SecurityFilterChain securityFilterChain() { - return new DefaultSecurityFilterChain( - List.of( - new SecurityContextHolderFilter(), - new UsernamePasswordAuthenticationFilter(authenticationManager()), - new BasicAuthenticationFilter(authenticationManager()), - new OAuth2AuthorizationRequestRedirectFilter(clientRegistrationRepository()), - new OAuth2LoginAuthenticationFilter(clientRegistrationRepository(), new OAuth2AuthorizedClientRepository(), authenticationManager()), - new AuthorizationFilter(requestAuthorizationManager()) - ) - ); - } - - @Bean - public RequestMatcherDelegatingAuthorizationManager requestAuthorizationManager() { - List> mappings = new ArrayList<>(); - mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members"), new AuthorityAuthorizationManager(roleHierarchy(), "ADMIN"))); - mappings.add(new RequestMatcherEntry<>(new MvcRequestMatcher(HttpMethod.GET, "/members/me"), new AuthorityAuthorizationManager(roleHierarchy(), "USER"))); - mappings.add(new RequestMatcherEntry<>(AnyRequestMatcher.INSTANCE, new PermitAllAuthorizationManager())); - return new RequestMatcherDelegatingAuthorizationManager(mappings); - } - - @Bean - public ClientRegistrationRepository clientRegistrationRepository() { - Map registrations = getClientRegistrations(oAuth2ClientProperties); - return new ClientRegistrationRepository(registrations); - } - - private static Map getClientRegistrations(OAuth2ClientProperties properties) { - Map clientRegistrations = new HashMap<>(); - properties.getRegistration().forEach((key, value) -> clientRegistrations.put(key, - getClientRegistration(key, value, properties.getProvider().get(key)))); - return clientRegistrations; - } - - private static ClientRegistration getClientRegistration(String registrationId, - OAuth2ClientProperties.Registration registration, OAuth2ClientProperties.Provider provider) { - return new ClientRegistration(registrationId, registration.getClientId(), registration.getClientSecret(), registration.getRedirectUri(), registration.getScope(), provider.getAuthorizationUri(), provider.getTokenUri(), provider.getUserInfoUri(), provider.getUserNameAttributeName()); - } } - diff --git a/src/main/java/nextstep/app/domain/MemberRepository.java b/src/main/java/nextstep/app/domain/MemberRepository.java index d2aacaf..6f25773 100644 --- a/src/main/java/nextstep/app/domain/MemberRepository.java +++ b/src/main/java/nextstep/app/domain/MemberRepository.java @@ -9,4 +9,6 @@ public interface MemberRepository { List findAll(); Member save(Member member); + + void deleteAll(); } diff --git a/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java b/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java index bd6d10f..3e7d605 100644 --- a/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java +++ b/src/main/java/nextstep/app/infrastructure/InmemoryMemberRepository.java @@ -29,4 +29,9 @@ public Member save(Member member) { members.put(member.getEmail(), member); return member; } + + @Override + public void deleteAll() { + members.clear(); + } } diff --git a/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java b/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java index a11ebdd..b395c25 100644 --- a/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java +++ b/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java @@ -14,7 +14,6 @@ import nextstep.oauth2.registration.ClientRegistrationRepository; import nextstep.security.authentication.AbstractAuthenticationProcessingFilter; import nextstep.security.authentication.Authentication; -import nextstep.security.authentication.AuthenticationManager; import org.springframework.core.convert.converter.Converter; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; @@ -31,8 +30,8 @@ public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProce private final Converter authenticationResultConverter = this::createAuthenticationResult; - public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository, AuthenticationManager authenticationManager) { - super(DEFAULT_LOGIN_REQUEST_BASE_URI, authenticationManager); + public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) { + super(DEFAULT_LOGIN_REQUEST_BASE_URI); this.clientRegistrationRepository = clientRegistrationRepository; this.authorizedClientRepository = authorizedClientRepository; } diff --git a/src/main/java/nextstep/security/authentication/AbstractAuthenticationProcessingFilter.java b/src/main/java/nextstep/security/authentication/AbstractAuthenticationProcessingFilter.java index 6d2be07..e925495 100644 --- a/src/main/java/nextstep/security/authentication/AbstractAuthenticationProcessingFilter.java +++ b/src/main/java/nextstep/security/authentication/AbstractAuthenticationProcessingFilter.java @@ -25,11 +25,11 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt private static final AuthenticationSuccessHandler successHandler = (request, response, authentication) -> response.sendRedirect("/"); private static final AuthenticationFailureHandler failureHandler = (request, response, exception) -> response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); - protected AbstractAuthenticationProcessingFilter(String filterProcessesUrl, AuthenticationManager authenticationManager) { - this(request -> { + protected AbstractAuthenticationProcessingFilter(String filterProcessesUrl) { + this.requiresAuthenticationRequestMatcher = request -> { String uri = request.getRequestURI(); return uri.startsWith(filterProcessesUrl); - }, authenticationManager); + }; } protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher, AuthenticationManager authenticationManager) { @@ -44,7 +44,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { - if (!requiresAuthentication(request, response)) { + if (!requiresAuthentication(request)) { chain.doFilter(request, response); return; } @@ -66,16 +66,16 @@ private void successfulAuthentication(HttpServletRequest request, HttpServletRes SecurityContextHolder.setContext(context); this.securityContextRepository.saveContext(context, request, response); - this.successHandler.onAuthenticationSuccess(request, response, authResult); + successHandler.onAuthenticationSuccess(request, response, authResult); } private void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); - this.failureHandler.onAuthenticationFailure(request, response, failed); + failureHandler.onAuthenticationFailure(request, response, failed); } - protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { + protected boolean requiresAuthentication(HttpServletRequest request) { return this.requiresAuthenticationRequestMatcher.matches(request); } @@ -85,4 +85,8 @@ public abstract Authentication attemptAuthentication(HttpServletRequest request, protected AuthenticationManager getAuthenticationManager() { return authenticationManager; } + + public void setAuthenticationManager(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } } diff --git a/src/main/java/nextstep/security/authentication/AuthenticationManagerBuilder.java b/src/main/java/nextstep/security/authentication/AuthenticationManagerBuilder.java new file mode 100644 index 0000000..b9fa2d7 --- /dev/null +++ b/src/main/java/nextstep/security/authentication/AuthenticationManagerBuilder.java @@ -0,0 +1,34 @@ +package nextstep.security.authentication; + +import nextstep.security.userdetails.UserDetailsService; +import org.springframework.context.ApplicationContext; + +import java.util.ArrayList; +import java.util.List; + +public class AuthenticationManagerBuilder { + + private final List authenticationProviders = new ArrayList<>(); + + public AuthenticationManagerBuilder(ApplicationContext context) { + List userDetailsServices = new ArrayList<>(context.getBeansOfType(UserDetailsService.class).values()); + if (userDetailsServices.isEmpty()) { + return; + } + if (userDetailsServices.size() > 1) { + return; + } + + UserDetailsService userDetailsService = userDetailsServices.get(0); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService); + authenticationProvider(provider); + } + + public void authenticationProvider(AuthenticationProvider authenticationProvider) { + this.authenticationProviders.add(authenticationProvider); + } + + public AuthenticationManager build() { + return new ProviderManager(this.authenticationProviders); + } +} diff --git a/src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java b/src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java index 16ef801..1e4de2e 100644 --- a/src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java +++ b/src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java @@ -5,6 +5,7 @@ import nextstep.security.access.RequestMatcherEntry; import nextstep.security.authentication.Authentication; +import java.util.ArrayList; import java.util.List; public class RequestMatcherDelegatingAuthorizationManager implements AuthorizationManager { @@ -27,4 +28,22 @@ public AuthorizationDecision check(Authentication authentication, HttpServletReq return new AuthorizationDecision(true); } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + + private final List> mappings = new ArrayList<>(); + + public Builder add(RequestMatcher matcher, AuthorizationManager manager) { + this.mappings.add(new RequestMatcherEntry<>(matcher, manager)); + return this; + } + + public RequestMatcherDelegatingAuthorizationManager build() { + return new RequestMatcherDelegatingAuthorizationManager(this.mappings); + } + } } diff --git a/src/main/java/nextstep/security/autoconfig/OAuth2ClientAutoConfiguration.java b/src/main/java/nextstep/security/autoconfig/OAuth2ClientAutoConfiguration.java new file mode 100644 index 0000000..15bc9cd --- /dev/null +++ b/src/main/java/nextstep/security/autoconfig/OAuth2ClientAutoConfiguration.java @@ -0,0 +1,11 @@ +package nextstep.security.autoconfig; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.context.annotation.Import; + +@AutoConfiguration(before = SecurityAutoConfiguration.class) +@Import({OAuth2ClientRegistrationRepositoryConfiguration.class}) +public class OAuth2ClientAutoConfiguration { + +} diff --git a/src/main/java/nextstep/security/autoconfig/OAuth2ClientRegistrationRepositoryConfiguration.java b/src/main/java/nextstep/security/autoconfig/OAuth2ClientRegistrationRepositoryConfiguration.java new file mode 100644 index 0000000..e187856 --- /dev/null +++ b/src/main/java/nextstep/security/autoconfig/OAuth2ClientRegistrationRepositoryConfiguration.java @@ -0,0 +1,36 @@ +package nextstep.security.autoconfig; + +import nextstep.oauth2.OAuth2ClientProperties; +import nextstep.oauth2.registration.ClientRegistration; +import nextstep.oauth2.registration.ClientRegistrationRepository; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(OAuth2ClientProperties.class) +public class OAuth2ClientRegistrationRepositoryConfiguration { + + @Bean + @ConditionalOnMissingBean(ClientRegistrationRepository.class) + ClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) { + Map registrations = getClientRegistrations(properties); + return new ClientRegistrationRepository(registrations); + } + + private static Map getClientRegistrations(OAuth2ClientProperties properties) { + Map clientRegistrations = new HashMap<>(); + properties.getRegistration().forEach((key, value) -> clientRegistrations.put(key, + getClientRegistration(key, value, properties.getProvider().get(key)))); + return clientRegistrations; + } + + private static ClientRegistration getClientRegistration(String registrationId, + OAuth2ClientProperties.Registration registration, OAuth2ClientProperties.Provider provider) { + return new ClientRegistration(registrationId, registration.getClientId(), registration.getClientSecret(), registration.getRedirectUri(), registration.getScope(), provider.getAuthorizationUri(), provider.getTokenUri(), provider.getUserInfoUri(), provider.getUserNameAttributeName()); + } +} diff --git a/src/main/java/nextstep/security/autoconfig/SecurityFilterAutoConfiguration.java b/src/main/java/nextstep/security/autoconfig/SecurityFilterAutoConfiguration.java new file mode 100644 index 0000000..9af7faa --- /dev/null +++ b/src/main/java/nextstep/security/autoconfig/SecurityFilterAutoConfiguration.java @@ -0,0 +1,14 @@ +package nextstep.security.autoconfig; + +import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean; +import org.springframework.context.annotation.Bean; + +public class SecurityFilterAutoConfiguration { + + private static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain"; + + @Bean + public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration() { + return new DelegatingFilterProxyRegistrationBean(DEFAULT_FILTER_NAME); + } +} diff --git a/src/main/java/nextstep/security/config/annotation/EnableWebSecurity.java b/src/main/java/nextstep/security/config/annotation/EnableWebSecurity.java new file mode 100644 index 0000000..8e320c0 --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/EnableWebSecurity.java @@ -0,0 +1,12 @@ +package nextstep.security.config.annotation; + +import org.springframework.context.annotation.Import; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Import({HttpSecurityConfiguration.class, WebSecurityConfiguration.class}) +public @interface EnableWebSecurity { +} diff --git a/src/main/java/nextstep/security/config/annotation/HttpSecurity.java b/src/main/java/nextstep/security/config/annotation/HttpSecurity.java new file mode 100644 index 0000000..94c85d3 --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/HttpSecurity.java @@ -0,0 +1,163 @@ +package nextstep.security.config.annotation; + +import jakarta.servlet.*; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.AuthenticationManagerBuilder; +import nextstep.security.authentication.AuthenticationProvider; +import nextstep.security.config.DefaultSecurityFilterChain; +import nextstep.security.config.annotation.configurer.*; +import org.springframework.context.ApplicationContext; +import org.springframework.core.OrderComparator; +import org.springframework.core.Ordered; + +import java.io.IOException; +import java.util.*; + +public class HttpSecurity { + private final LinkedHashMap, SecurityConfigurer> configurers = new LinkedHashMap<>(); + + private List filters = new ArrayList<>(); + + private FilterOrderRegistration filterOrders = new FilterOrderRegistration(); + + private final Map, Object> sharedObjects = new HashMap<>(); + + public HttpSecurity(AuthenticationManagerBuilder authenticationManagerBuilder, Map, Object> sharedObjects) { + setSharedObject(AuthenticationManagerBuilder.class, authenticationManagerBuilder); + for (Map.Entry, Object> entry : sharedObjects.entrySet()) { + setSharedObject((Class) entry.getKey(), entry.getValue()); + } + } + + private ApplicationContext getContext() { + return getSharedObject(ApplicationContext.class); + } + + public C getSharedObject(Class sharedType) { + return (C) this.sharedObjects.get(sharedType); + } + + public void setSharedObject(Class sharedType, C object) { + this.sharedObjects.put(sharedType, object); + } + + private void init() { + for (SecurityConfigurer configurer : this.configurers.values()) { + configurer.init(this); + } + } + + private void beforeConfigure() { + AuthenticationManager manager = getAuthenticationRegistry().build(); + setSharedObject(AuthenticationManager.class, manager); + } + + private void configure() { + for (SecurityConfigurer configurer : this.configurers.values()) { + configurer.configure(this); + } + } + + public DefaultSecurityFilterChain build() { + init(); + beforeConfigure(); + configure(); + return performBuild(); + } + + private DefaultSecurityFilterChain performBuild() { + this.filters.sort(OrderComparator.INSTANCE); + + List sortedFilters = new ArrayList<>(this.filters.size()); + for (OrderedFilter filter : this.filters) { + sortedFilters.add(filter.filter); + } + return new DefaultSecurityFilterChain(sortedFilters); + } + + public void addFilter(Filter filter) { + Integer order = this.filterOrders.getOrder(filter.getClass()); + if (order == null) { + throw new IllegalArgumentException(); + } + filters.add(new OrderedFilter(filter, order)); + } + + public void authenticationProvider(AuthenticationProvider authenticationProvider) { + getAuthenticationRegistry().authenticationProvider(authenticationProvider); + } + + private AuthenticationManagerBuilder getAuthenticationRegistry() { + return getSharedObject(AuthenticationManagerBuilder.class); + } + + public HttpSecurity csrf(Customizer csrfCustomizer) { + csrfCustomizer.customize(getOrApply(new CsrfConfigurer())); + return HttpSecurity.this; + } + + public HttpSecurity httpBasic(Customizer httpBasicCustomizer) { + httpBasicCustomizer.customize(getOrApply(new HttpBasicConfigurer())); + return HttpSecurity.this; + } + + public HttpSecurity formLogin(Customizer formLoginCustomizer) { + formLoginCustomizer.customize(getOrApply(new UsernamePasswordAuthenticationConfigurer())); + return HttpSecurity.this; + } + + public HttpSecurity oauth2Login(Customizer oauth2LoginCustomizer) { + oauth2LoginCustomizer.customize(getOrApply(new OAuth2LoginConfigurer())); + return HttpSecurity.this; + } + + public HttpSecurity authorizeHttpRequests( + Customizer authorizeHttpRequestsCustomizer) { + ApplicationContext context = getContext(); + authorizeHttpRequestsCustomizer.customize(getOrApply(new AuthorizeHttpRequestsConfigurer(context)).getRegistry()); + return HttpSecurity.this; + } + + public HttpSecurity securityContext(Customizer securityContextCustomizer) { + securityContextCustomizer.customize(getOrApply(new SecurityContextConfigurer())); + return HttpSecurity.this; + } + + private C getOrApply(C configurer) { + Class clazz = configurer.getClass(); + C existingConfig = (C) this.configurers.get(clazz); + if (existingConfig != null) { + return existingConfig; + } + this.configurers.put(clazz, configurer); + return configurer; + } + + private static final class OrderedFilter implements Ordered, Filter { + + private final Filter filter; + + private final int order; + + private OrderedFilter(Filter filter, int order) { + this.filter = filter; + this.order = order; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + this.filter.doFilter(servletRequest, servletResponse, filterChain); + } + + @Override + public int getOrder() { + return this.order; + } + + @Override + public String toString() { + return "OrderedFilter{" + "filter=" + this.filter + ", order=" + this.order + '}'; + } + } +} diff --git a/src/main/java/nextstep/security/config/annotation/HttpSecurityConfiguration.java b/src/main/java/nextstep/security/config/annotation/HttpSecurityConfiguration.java new file mode 100644 index 0000000..4431cda --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/HttpSecurityConfiguration.java @@ -0,0 +1,37 @@ +package nextstep.security.config.annotation; + +import nextstep.security.authentication.AuthenticationManagerBuilder; +import nextstep.security.config.annotation.configurer.Customizer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class HttpSecurityConfiguration { + + private static final String BEAN_NAME_PREFIX = "nextstep.security.config.annotation.HttpSecurityConfiguration."; + + private static final String HTTP_SECURITY_BEAN_NAME = BEAN_NAME_PREFIX + "httpSecurity"; + + @Autowired + private ApplicationContext context; + + @Bean(HTTP_SECURITY_BEAN_NAME) + @Scope("prototype") + HttpSecurity httpSecurity() { + AuthenticationManagerBuilder authenticationBuilder = new AuthenticationManagerBuilder(context); + return new HttpSecurity(authenticationBuilder, createSharedObjects()) + .securityContext(Customizer.withDefaults()); + } + + private Map, Object> createSharedObjects() { + Map, Object> sharedObjects = new HashMap<>(); + sharedObjects.put(ApplicationContext.class, this.context); + return sharedObjects; + } +} diff --git a/src/main/java/nextstep/security/config/annotation/WebSecurityConfiguration.java b/src/main/java/nextstep/security/config/annotation/WebSecurityConfiguration.java new file mode 100644 index 0000000..1a3af8c --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/WebSecurityConfiguration.java @@ -0,0 +1,45 @@ +package nextstep.security.config.annotation; + + +import jakarta.servlet.Filter; +import nextstep.security.config.DefaultSecurityFilterChain; +import nextstep.security.config.FilterChainProxy; +import nextstep.security.config.SecurityFilterChain; +import nextstep.security.config.annotation.configurer.Customizer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Collections; +import java.util.List; + +@Configuration(proxyBeanMethods = false) +public class WebSecurityConfiguration { + + public static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain"; + + private List securityFilterChains = Collections.emptyList(); + + @Autowired(required = false) + private HttpSecurity httpSecurity; + + @Autowired(required = false) + void setFilterChains(List securityFilterChains) { + this.securityFilterChains = securityFilterChains; + } + + @Bean(name = DEFAULT_FILTER_NAME) + public Filter springSecurityFilterChain() { + boolean hasFilterChain = !this.securityFilterChains.isEmpty(); + + if (!hasFilterChain) { + this.httpSecurity.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()); + this.httpSecurity.formLogin(Customizer.withDefaults()); + this.httpSecurity.httpBasic(Customizer.withDefaults()); + DefaultSecurityFilterChain filterChain = this.httpSecurity.build(); + securityFilterChains.add(filterChain); + } + + return new FilterChainProxy(securityFilterChains); + } +} diff --git a/src/main/java/nextstep/security/config/annotation/configurer/AuthorizationManagers.java b/src/main/java/nextstep/security/config/annotation/configurer/AuthorizationManagers.java new file mode 100644 index 0000000..41c0945 --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/configurer/AuthorizationManagers.java @@ -0,0 +1,23 @@ +package nextstep.security.config.annotation.configurer; + +import nextstep.security.authorization.AuthorizationDecision; +import nextstep.security.authorization.AuthorizationManager; + +public class AuthorizationManagers { + public static AuthorizationManager not(AuthorizationManager manager) { + return (authentication, object) -> { + AuthorizationDecision decision = manager.check(authentication, object); + if (decision == null) { + return null; + } + return new NotAuthorizationDecision(decision); + }; + } + + private static final class NotAuthorizationDecision extends AuthorizationDecision { + + private NotAuthorizationDecision(AuthorizationDecision decision) { + super(!decision.isGranted()); + } + } +} diff --git a/src/main/java/nextstep/security/config/annotation/configurer/AuthorizeHttpRequestsConfigurer.java b/src/main/java/nextstep/security/config/annotation/configurer/AuthorizeHttpRequestsConfigurer.java new file mode 100644 index 0000000..e42d291 --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/configurer/AuthorizeHttpRequestsConfigurer.java @@ -0,0 +1,135 @@ +package nextstep.security.config.annotation.configurer; + +import jakarta.servlet.http.HttpServletRequest; +import nextstep.security.access.AnyRequestMatcher; +import nextstep.security.access.MvcRequestMatcher; +import nextstep.security.access.RequestMatcher; +import nextstep.security.access.hierarchicalroles.NullRoleHierarchy; +import nextstep.security.access.hierarchicalroles.RoleHierarchy; +import nextstep.security.authorization.*; +import nextstep.security.config.annotation.HttpSecurity; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpMethod; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + + +public class AuthorizeHttpRequestsConfigurer implements SecurityConfigurer { + + private final AuthorizationManagerRequestMatcherRegistry registry; + + private final RoleHierarchy roleHierarchy; + + public AuthorizeHttpRequestsConfigurer(ApplicationContext context) { + this.registry = new AuthorizationManagerRequestMatcherRegistry(); + + this.roleHierarchy = (context.getBeanNamesForType(RoleHierarchy.class).length > 0) + ? context.getBean(RoleHierarchy.class) : new NullRoleHierarchy(); + } + + @Override + public void init(HttpSecurity http) { + } + + @Override + public void configure(HttpSecurity http) { + AuthorizationManager authorizationManager = this.registry.createAuthorizationManager(); + AuthorizationFilter authorizationFilter = new AuthorizationFilter(authorizationManager); + http.addFilter(authorizationFilter); + } + + private AuthorizationManagerRequestMatcherRegistry addMapping(List matchers, AuthorizationManager manager) { + for (RequestMatcher matcher : matchers) { + this.registry.addMapping(matcher, manager); + } + return this.registry; + } + + public AuthorizationManagerRequestMatcherRegistry getRegistry() { + return this.registry; + } + + public class AuthorizationManagerRequestMatcherRegistry { + + private final RequestMatcherDelegatingAuthorizationManager.Builder managerBuilder = RequestMatcherDelegatingAuthorizationManager + .builder(); + + private void addMapping(RequestMatcher matcher, AuthorizationManager manager) { + this.managerBuilder.add(matcher, manager); + } + + private AuthorizationManager createAuthorizationManager() { + return this.managerBuilder.build(); + } + + public AuthorizedUrl requestMatchers(String... patterns) { + return requestMatchers(null, patterns); + } + + public AuthorizedUrl anyRequest() { + return requestMatchers(AnyRequestMatcher.INSTANCE); + } + + public AuthorizedUrl requestMatchers(HttpMethod method, String... patterns) { + List matchers = new ArrayList<>(); + for (String pattern : patterns) { + MvcRequestMatcher mvc = new MvcRequestMatcher(method, pattern); + matchers.add(mvc); + } + return chainRequestMatchers(matchers); + } + + public AuthorizedUrl requestMatchers(RequestMatcher... requestMatchers) { + return chainRequestMatchers(Arrays.asList(requestMatchers)); + } + + protected AuthorizedUrl chainRequestMatchers(List requestMatchers) { + return new AuthorizedUrl(requestMatchers); + } + } + + public class AuthorizedUrl { + + private final List matchers; + + private boolean not; + + AuthorizedUrl(List matchers) { + this.matchers = matchers; + } + + protected List getMatchers() { + return this.matchers; + } + + public AuthorizedUrl not() { + this.not = true; + return this; + } + + public AuthorizationManagerRequestMatcherRegistry permitAll() { + return access((a, o) -> new AuthorizationDecision(true)); + } + + public AuthorizationManagerRequestMatcherRegistry denyAll() { + return access((a, o) -> new AuthorizationDecision(false)); + } + + public AuthorizationManagerRequestMatcherRegistry hasRole(String role) { + return access(new AuthorityAuthorizationManager(roleHierarchy, role)); + } + + public AuthorizationManagerRequestMatcherRegistry authenticated() { + return access(new AuthenticatedAuthorizationManager()); + } + + public AuthorizationManagerRequestMatcherRegistry access( + AuthorizationManager manager) { + return (this.not) + ? AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, AuthorizationManagers.not(manager)) + : AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager); + } + } +} diff --git a/src/main/java/nextstep/security/config/annotation/configurer/CsrfConfigurer.java b/src/main/java/nextstep/security/config/annotation/configurer/CsrfConfigurer.java new file mode 100644 index 0000000..daabaf8 --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/configurer/CsrfConfigurer.java @@ -0,0 +1,33 @@ +package nextstep.security.config.annotation.configurer; + +import nextstep.security.access.MvcRequestMatcher; +import nextstep.security.access.RequestMatcher; +import nextstep.security.config.annotation.HttpSecurity; +import nextstep.security.csrf.CsrfFilter; + +import java.util.HashSet; +import java.util.Set; + +public class CsrfConfigurer implements SecurityConfigurer { + + private final Set ignoredCsrfProtectionMatchers = new HashSet<>(); + + @Override + public void init(HttpSecurity http) { + // 다른 요소가 필요없음 + } + + @Override + public void configure(HttpSecurity http) { + CsrfFilter csrfFilter = new CsrfFilter(ignoredCsrfProtectionMatchers); + http.addFilter(csrfFilter); + } + + public CsrfConfigurer ignoringRequestMatchers(String... patterns) { + for (String p : patterns) { + MvcRequestMatcher mvc = new MvcRequestMatcher(null, p); + ignoredCsrfProtectionMatchers.add(mvc); + } + return this; + } +} diff --git a/src/main/java/nextstep/security/config/annotation/configurer/Customizer.java b/src/main/java/nextstep/security/config/annotation/configurer/Customizer.java new file mode 100644 index 0000000..ed90bbd --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/configurer/Customizer.java @@ -0,0 +1,13 @@ +package nextstep.security.config.annotation.configurer; + +@FunctionalInterface +public interface Customizer { + + void customize(T t); + + static Customizer withDefaults() { + return t -> { + }; + } + +} diff --git a/src/main/java/nextstep/security/config/annotation/configurer/FilterOrderRegistration.java b/src/main/java/nextstep/security/config/annotation/configurer/FilterOrderRegistration.java new file mode 100644 index 0000000..777acf8 --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/configurer/FilterOrderRegistration.java @@ -0,0 +1,104 @@ +package nextstep.security.config.annotation.configurer; + +import jakarta.servlet.Filter; +import nextstep.security.authentication.BasicAuthenticationFilter; +import nextstep.security.authentication.UsernamePasswordAuthenticationFilter; +import nextstep.security.authorization.AuthorizationFilter; +import nextstep.security.context.SecurityContextHolderFilter; +import nextstep.security.csrf.CsrfFilter; +import org.apache.catalina.filters.CorsFilter; + +import java.util.HashMap; +import java.util.Map; + +public class FilterOrderRegistration { + private static final int INITIAL_ORDER = 100; + + private static final int ORDER_STEP = 100; + + private final Map filterToOrder = new HashMap<>(); + + public FilterOrderRegistration() { + Step order = new Step(INITIAL_ORDER, ORDER_STEP); +// put(DisableEncodeUrlFilter.class, order.next()); +// put(ForceEagerSessionCreationFilter.class, order.next()); +// put(ChannelProcessingFilter.class, order.next()); + order.next(); // gh-8105 +// put(WebAsyncManagerIntegrationFilter.class, order.next()); + put(SecurityContextHolderFilter.class, order.next()); +// put(HeaderWriterFilter.class, order.next()); + put(CorsFilter.class, order.next()); + put(CsrfFilter.class, order.next()); +// put(LogoutFilter.class, order.next()); + this.filterToOrder.put( + "nextstep.oauth2.web.OAuth2AuthorizationRequestRedirectFilter", + order.next()); +// this.filterToOrder.put( +// "org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter", +// order.next()); +// put(X509AuthenticationFilter.class, order.next()); +// put(AbstractPreAuthenticatedProcessingFilter.class, order.next()); +// this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next()); + this.filterToOrder.put("nextstep.oauth2.web.OAuth2LoginAuthenticationFilter", + order.next()); +// this.filterToOrder.put( +// "org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter", +// order.next()); + put(UsernamePasswordAuthenticationFilter.class, order.next()); + order.next(); // gh-8105 +// put(DefaultLoginPageGeneratingFilter.class, order.next()); +// put(DefaultLogoutPageGeneratingFilter.class, order.next()); +// put(ConcurrentSessionFilter.class, order.next()); +// put(DigestAuthenticationFilter.class, order.next()); +// this.filterToOrder.put( +// "org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter", +// order.next()); + put(BasicAuthenticationFilter.class, order.next()); +// put(RequestCacheAwareFilter.class, order.next()); +// put(SecurityContextHolderAwareRequestFilter.class, order.next()); +// put(JaasApiIntegrationFilter.class, order.next()); +// put(RememberMeAuthenticationFilter.class, order.next()); +// put(AnonymousAuthenticationFilter.class, order.next()); +// this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter", +// order.next()); +// put(SessionManagementFilter.class, order.next()); +// put(ExceptionTranslationFilter.class, order.next()); +// put(FilterSecurityInterceptor.class, order.next()); + put(AuthorizationFilter.class, order.next()); +// put(SwitchUserFilter.class, order.next()); + } + + void put(Class filter, int position) { + this.filterToOrder.putIfAbsent(filter.getName(), position); + } + + public Integer getOrder(Class clazz) { + while (clazz != null) { + Integer result = this.filterToOrder.get(clazz.getName()); + if (result != null) { + return result; + } + clazz = clazz.getSuperclass(); + } + return null; + } + + private static class Step { + + private int value; + + private final int stepSize; + + Step(int initialValue, int stepSize) { + this.value = initialValue; + this.stepSize = stepSize; + } + + int next() { + int value = this.value; + this.value += this.stepSize; + return value; + } + + } +} diff --git a/src/main/java/nextstep/security/config/annotation/configurer/HttpBasicConfigurer.java b/src/main/java/nextstep/security/config/annotation/configurer/HttpBasicConfigurer.java new file mode 100644 index 0000000..a28782e --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/configurer/HttpBasicConfigurer.java @@ -0,0 +1,18 @@ +package nextstep.security.config.annotation.configurer; + +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.BasicAuthenticationFilter; +import nextstep.security.config.annotation.HttpSecurity; + +public class HttpBasicConfigurer implements SecurityConfigurer { + @Override + public void init(HttpSecurity http) { + } + + @Override + public void configure(HttpSecurity http) { + var authenticationManager = http.getSharedObject(AuthenticationManager.class); + var basicAuthenticationFilter = new BasicAuthenticationFilter(authenticationManager); + http.addFilter(basicAuthenticationFilter); + } +} diff --git a/src/main/java/nextstep/security/config/annotation/configurer/OAuth2ClientConfigurerUtils.java b/src/main/java/nextstep/security/config/annotation/configurer/OAuth2ClientConfigurerUtils.java new file mode 100644 index 0000000..6d46bc6 --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/configurer/OAuth2ClientConfigurerUtils.java @@ -0,0 +1,59 @@ +package nextstep.security.config.annotation.configurer; + +import nextstep.oauth2.registration.ClientRegistrationRepository; +import nextstep.oauth2.web.OAuth2AuthorizedClientRepository; +import nextstep.security.config.annotation.HttpSecurity; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.util.StringUtils; + +import java.util.Map; + +public class OAuth2ClientConfigurerUtils { + + protected OAuth2ClientConfigurerUtils() { + throw new UnsupportedOperationException(); + } + + public static ClientRegistrationRepository getClientRegistrationRepository(HttpSecurity builder) { + ClientRegistrationRepository clientRegistrationRepository = builder.getSharedObject(ClientRegistrationRepository.class); + if (clientRegistrationRepository == null) { + clientRegistrationRepository = getClientRegistrationRepositoryBean(builder); + builder.setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository); + } + return clientRegistrationRepository; + } + + private static ClientRegistrationRepository getClientRegistrationRepositoryBean(HttpSecurity builder) { + return builder.getSharedObject(ApplicationContext.class).getBean(ClientRegistrationRepository.class); + } + + public static OAuth2AuthorizedClientRepository getAuthorizedClientRepository(HttpSecurity builder) { + OAuth2AuthorizedClientRepository authorizedClientRepository = builder + .getSharedObject(OAuth2AuthorizedClientRepository.class); + if (authorizedClientRepository == null) { + authorizedClientRepository = getAuthorizedClientRepositoryBean(builder); + if (authorizedClientRepository == null) { + authorizedClientRepository = new OAuth2AuthorizedClientRepository(); + } + builder.setSharedObject(OAuth2AuthorizedClientRepository.class, authorizedClientRepository); + } + return authorizedClientRepository; + } + + private static OAuth2AuthorizedClientRepository getAuthorizedClientRepositoryBean(HttpSecurity builder) { + Map authorizedClientRepositoryMap = BeanFactoryUtils + .beansOfTypeIncludingAncestors(builder.getSharedObject(ApplicationContext.class), + OAuth2AuthorizedClientRepository.class); + if (authorizedClientRepositoryMap.size() > 1) { + throw new NoUniqueBeanDefinitionException(OAuth2AuthorizedClientRepository.class, + authorizedClientRepositoryMap.size(), + "Expected single matching bean of type '" + OAuth2AuthorizedClientRepository.class.getName() + + "' but found " + authorizedClientRepositoryMap.size() + ": " + + StringUtils.collectionToCommaDelimitedString(authorizedClientRepositoryMap.keySet())); + } + return (!authorizedClientRepositoryMap.isEmpty() ? authorizedClientRepositoryMap.values().iterator().next() + : null); + } +} diff --git a/src/main/java/nextstep/security/config/annotation/configurer/OAuth2LoginConfigurer.java b/src/main/java/nextstep/security/config/annotation/configurer/OAuth2LoginConfigurer.java new file mode 100644 index 0000000..917b4d7 --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/configurer/OAuth2LoginConfigurer.java @@ -0,0 +1,44 @@ +package nextstep.security.config.annotation.configurer; + +import nextstep.oauth2.authentication.OAuth2LoginAuthenticationProvider; +import nextstep.oauth2.userinfo.DefaultOAuth2UserService; +import nextstep.oauth2.userinfo.OAuth2UserService; +import nextstep.oauth2.web.OAuth2AuthorizationRequestRedirectFilter; +import nextstep.oauth2.web.OAuth2LoginAuthenticationFilter; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.config.annotation.HttpSecurity; +import org.springframework.context.ApplicationContext; + +public class OAuth2LoginConfigurer implements SecurityConfigurer { + + private OAuth2LoginAuthenticationFilter authFilter; + + @Override + public void init(HttpSecurity http) { + OAuth2LoginAuthenticationFilter authenticationFilter = new OAuth2LoginAuthenticationFilter( + OAuth2ClientConfigurerUtils.getClientRegistrationRepository(http), + OAuth2ClientConfigurerUtils.getAuthorizedClientRepository(http)); + this.authFilter = authenticationFilter; + + OAuth2UserService oauth2UserService = getOAuth2UserService(http); + OAuth2LoginAuthenticationProvider oauth2LoginAuthenticationProvider = new OAuth2LoginAuthenticationProvider(oauth2UserService); + http.authenticationProvider(oauth2LoginAuthenticationProvider); + } + + @Override + public void configure(HttpSecurity http) { + OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = + new OAuth2AuthorizationRequestRedirectFilter(OAuth2ClientConfigurerUtils.getClientRegistrationRepository(http)); + http.addFilter(authorizationRequestFilter); + + OAuth2LoginAuthenticationFilter authenticationFilter = this.authFilter; + authenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); + http.addFilter(authenticationFilter); + } + + private OAuth2UserService getOAuth2UserService(HttpSecurity http) { + ApplicationContext context = http.getSharedObject(ApplicationContext.class); + OAuth2UserService bean = context.getBean(OAuth2UserService.class); + return (bean != null) ? bean : new DefaultOAuth2UserService(); + } +} diff --git a/src/main/java/nextstep/security/config/annotation/configurer/SecurityConfigurer.java b/src/main/java/nextstep/security/config/annotation/configurer/SecurityConfigurer.java new file mode 100644 index 0000000..f1ad198 --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/configurer/SecurityConfigurer.java @@ -0,0 +1,9 @@ +package nextstep.security.config.annotation.configurer; + +import nextstep.security.config.annotation.HttpSecurity; + +public interface SecurityConfigurer { + void init(HttpSecurity http); + + void configure(HttpSecurity http); +} diff --git a/src/main/java/nextstep/security/config/annotation/configurer/SecurityContextConfigurer.java b/src/main/java/nextstep/security/config/annotation/configurer/SecurityContextConfigurer.java new file mode 100644 index 0000000..e4bcd15 --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/configurer/SecurityContextConfigurer.java @@ -0,0 +1,17 @@ +package nextstep.security.config.annotation.configurer; + +import nextstep.security.config.annotation.HttpSecurity; +import nextstep.security.context.SecurityContextHolderFilter; + +public class SecurityContextConfigurer implements SecurityConfigurer { + @Override + public void init(HttpSecurity http) { + + } + + @Override + public void configure(HttpSecurity http) { + SecurityContextHolderFilter securityContextHolderFilter = new SecurityContextHolderFilter(); + http.addFilter(securityContextHolderFilter); + } +} diff --git a/src/main/java/nextstep/security/config/annotation/configurer/UsernamePasswordAuthenticationConfigurer.java b/src/main/java/nextstep/security/config/annotation/configurer/UsernamePasswordAuthenticationConfigurer.java new file mode 100644 index 0000000..ffc7ada --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/configurer/UsernamePasswordAuthenticationConfigurer.java @@ -0,0 +1,21 @@ +package nextstep.security.config.annotation.configurer; + +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.UsernamePasswordAuthenticationFilter; +import nextstep.security.config.annotation.HttpSecurity; + +public class UsernamePasswordAuthenticationConfigurer implements SecurityConfigurer { + + @Override + public void init(HttpSecurity http) { + // 다른 요소가 필요없음 + } + + @Override + public void configure(HttpSecurity http) { + var authenticationManager = http.getSharedObject(AuthenticationManager.class); + var usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter(authenticationManager); + http.addFilter(usernamePasswordAuthenticationFilter); + } + +} diff --git a/src/main/java/nextstep/security/csrf/AccessDeniedHandler.java b/src/main/java/nextstep/security/csrf/AccessDeniedHandler.java new file mode 100644 index 0000000..63307f8 --- /dev/null +++ b/src/main/java/nextstep/security/csrf/AccessDeniedHandler.java @@ -0,0 +1,19 @@ +package nextstep.security.csrf; + +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.authorization.AccessDeniedException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.http.HttpStatus; + +import java.io.IOException; + +public class AccessDeniedHandler { + + private static final Log log = LogFactory.getLog(AccessDeniedHandler.class); + + public void onAccessFailure(HttpServletResponse response, AccessDeniedException exception) throws IOException { + log.error(exception); + response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase()); + } +} diff --git a/src/main/java/nextstep/security/csrf/CsrfFilter.java b/src/main/java/nextstep/security/csrf/CsrfFilter.java new file mode 100644 index 0000000..c86d7cb --- /dev/null +++ b/src/main/java/nextstep/security/csrf/CsrfFilter.java @@ -0,0 +1,74 @@ +package nextstep.security.csrf; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.access.RequestMatcher; +import nextstep.security.authorization.AccessDeniedException; +import org.springframework.http.HttpMethod; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Set; + +public class CsrfFilter extends OncePerRequestFilter { + + private final Set ignoringRequestMatchers; + private final CsrfTokenRepository csrfTokenRepository = new CsrfTokenRepository(); + private final AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandler(); + + public CsrfFilter(Set ignoringRequestMatchers) { + this.ignoringRequestMatchers = ignoringRequestMatchers; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + if (isIgnoreRequest(request, response, filterChain)) return; + + CsrfToken actualToken = csrfTokenRepository.loadToken(request); + if (actualToken == null) { + actualToken = csrfTokenRepository.generateToken(request); + csrfTokenRepository.saveToken(actualToken, request, response); + } + + request.setAttribute(actualToken.parameterName(), actualToken); + request.setAttribute(actualToken.headerName(), actualToken); + + if (!HttpMethod.POST.name().equalsIgnoreCase(request.getMethod())) { + filterChain.doFilter(request, response); + return; + } + + try { + validateToken(request, actualToken); + } catch (AccessDeniedException e) { + accessDeniedHandler.onAccessFailure(response, e); + } + + filterChain.doFilter(request, response); + } + + private boolean isIgnoreRequest(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws IOException, ServletException { + for (RequestMatcher ignoringRequestMatcher : ignoringRequestMatchers) { + if (ignoringRequestMatcher.matches(request)) { + filterChain.doFilter(request, response); + return true; + } + } + return false; + } + + private void validateToken(HttpServletRequest request, CsrfToken actualToken) { + String headerToken = request.getHeader(actualToken.headerName()); + String paramToken = request.getParameter(actualToken.parameterName()); + + boolean validToken = (headerToken != null && headerToken.equals(actualToken.token())) || + (paramToken != null && paramToken.equals(actualToken.token())); + + if (!validToken) { + throw new AccessDeniedException("CSRF token validation failed"); + } + } +} diff --git a/src/main/java/nextstep/security/csrf/CsrfToken.java b/src/main/java/nextstep/security/csrf/CsrfToken.java new file mode 100644 index 0000000..aecb9f4 --- /dev/null +++ b/src/main/java/nextstep/security/csrf/CsrfToken.java @@ -0,0 +1,13 @@ +package nextstep.security.csrf; + +import java.io.Serial; +import java.io.Serializable; + +public record CsrfToken( + String headerName, + String parameterName, + String token +) implements Serializable { + @Serial + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/nextstep/security/csrf/CsrfTokenRepository.java b/src/main/java/nextstep/security/csrf/CsrfTokenRepository.java new file mode 100644 index 0000000..186f78a --- /dev/null +++ b/src/main/java/nextstep/security/csrf/CsrfTokenRepository.java @@ -0,0 +1,41 @@ +package nextstep.security.csrf; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import java.util.UUID; + +public class CsrfTokenRepository { + + private static final String CSRF_CUSTOM_TOKEN_HEADER = "X-CSRF-TOKEN"; + private static final String CSRF_CUSTOM_TOKEN_PARAMETER = "csrfToken"; + + public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { + HttpSession session = request.getSession(true); + session.setAttribute(CSRF_CUSTOM_TOKEN_HEADER, token); + + response.setHeader(token.headerName(), token.token()); + } + + public CsrfToken loadToken(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session == null) { + return null; + } + + Object attribute = session.getAttribute(CSRF_CUSTOM_TOKEN_HEADER); + if (attribute instanceof CsrfToken csrfToken) { + return csrfToken; + } + + return null; + } + + public CsrfToken generateToken(HttpServletRequest request) { + String sessionId = request.getSession(true).getId(); + String tokenValue = UUID.randomUUID().toString() + sessionId.hashCode(); + return new CsrfToken(CSRF_CUSTOM_TOKEN_HEADER, CSRF_CUSTOM_TOKEN_PARAMETER, tokenValue); + } + +} diff --git a/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..7b5b1f6 --- /dev/null +++ b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +nextstep.security.autoconfig.SecurityFilterAutoConfiguration +nextstep.security.autoconfig.OAuth2ClientAutoConfiguration diff --git a/src/main/resources/templates/account.html b/src/main/resources/templates/account.html index 1c99a09..3879a1b 100644 --- a/src/main/resources/templates/account.html +++ b/src/main/resources/templates/account.html @@ -19,7 +19,7 @@

Account Information

Update Account Information

- + diff --git a/src/test/java/nextstep/app/BasicAuthTest.java b/src/test/java/nextstep/app/BasicAuthTest.java index 2dea9bb..a3facc2 100644 --- a/src/test/java/nextstep/app/BasicAuthTest.java +++ b/src/test/java/nextstep/app/BasicAuthTest.java @@ -33,6 +33,7 @@ class BasicAuthTest { @BeforeEach void setUp() { + memberRepository.deleteAll(); memberRepository.save(TEST_ADMIN_MEMBER); memberRepository.save(TEST_USER_MEMBER); } diff --git a/src/test/java/nextstep/app/SecuredTest.java b/src/test/java/nextstep/app/SecuredTest.java index 9e672e5..137387d 100644 --- a/src/test/java/nextstep/app/SecuredTest.java +++ b/src/test/java/nextstep/app/SecuredTest.java @@ -34,6 +34,7 @@ class SecuredTest { @BeforeEach void setUp() { + memberRepository.deleteAll(); memberRepository.save(TEST_ADMIN_MEMBER); memberRepository.save(TEST_USER_MEMBER); }