From c12757bcc3a7aa107173dc08c8a8a7ebec534889 Mon Sep 17 00:00:00 2001 From: haero77 Date: Sat, 15 Mar 2025 19:56:40 +0900 Subject: [PATCH 01/14] =?UTF-8?q?(CSRF)=20CsrfFilter=20=EC=8B=A4=EC=8A=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 32 ++++++- docs/Questions.md | 4 + .../java/nextstep/app/SecurityConfig.java | 8 +- .../nextstep/app/ui/AccountController.java | 7 +- .../security/access/AccessDeniedHandler.java | 10 ++ .../security/web/csrf/CsrfFilter.java | 93 +++++++++++++++++++ .../nextstep/security/web/csrf/CsrfToken.java | 10 ++ .../web/csrf/CsrfTokenRepository.java | 13 +++ .../security/web/csrf/DefaultCsrfToken.java | 35 +++++++ .../csrf/HttpSessionCsrfTokenRepository.java | 60 ++++++++++++ src/main/resources/templates/account.html | 3 +- src/test/java/nextstep/app/CsrfTest.java | 4 +- 12 files changed, 267 insertions(+), 12 deletions(-) create mode 100644 docs/Questions.md create mode 100644 src/main/java/nextstep/security/access/AccessDeniedHandler.java create mode 100644 src/main/java/nextstep/security/web/csrf/CsrfFilter.java create mode 100644 src/main/java/nextstep/security/web/csrf/CsrfToken.java create mode 100644 src/main/java/nextstep/security/web/csrf/CsrfTokenRepository.java create mode 100644 src/main/java/nextstep/security/web/csrf/DefaultCsrfToken.java create mode 100644 src/main/java/nextstep/security/web/csrf/HttpSessionCsrfTokenRepository.java diff --git a/README.md b/README.md index 8b81680..b86151f 100644 --- a/README.md +++ b/README.md @@ -1 +1,31 @@ -# spring-security +> 미션 4: 취약점 대응 & 리팩토링 + +# 요구사항 + +## 실습 - 취약점 대응(CsrfFilter) + +> CsrfFilter를 이용한 CSRF 공격 대응 + +- [x] CsrfToken 구현 +- [x] CsrfTokenRepository 구현 - HttpSessionCsrfTokenRepository + - [x] CsrfToken 발급/저장/조회 +- [x] CsrfFilter 구현 + - [x] CsrfTokenRepository를 이용한 CsrfToken 검증 + +## 1단계 - SecurityFilterChain 리팩토링 + +> 주요 클래스 +> - HttpSecurity +> - HttpSecurityConfiguration +> - SecurityConfigurer +> - Customizer + +- [ ] HttpSecurity 구현 +- [ ] HttpSecurityConfiguration를 이용한 HttpSecurity 빈 등록 +- + +## 2단계 - 인증 관련 리팩토링 + +## 3단계 - 인가 관련 리팩토링 + +## 4단계 - Auto Configuration 적용 diff --git a/docs/Questions.md b/docs/Questions.md new file mode 100644 index 0000000..da14786 --- /dev/null +++ b/docs/Questions.md @@ -0,0 +1,4 @@ +> PR 코멘트로 질문 드리겠습니다..! 🙇‍♂️ + +- [ ] SSR에서는 HttpSessionCsrfTokenRepository, CSR에서는 CookieCsrfTokenRepository를 사용해야할 것 같은데, 그 이유를 얘기하고 근거가 적절한지 질문. + diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 03b238f..38914a9 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -21,17 +21,14 @@ import nextstep.security.config.SecurityFilterChain; import nextstep.security.context.SecurityContextHolderFilter; import nextstep.security.userdetails.UserDetailsService; +import nextstep.security.web.csrf.CsrfFilter; 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; - +import java.util.*; @Configuration @EnableAspectJAutoProxy @@ -82,6 +79,7 @@ public SecurityFilterChain securityFilterChain() { return new DefaultSecurityFilterChain( List.of( new SecurityContextHolderFilter(), + new CsrfFilter(Set.of(new MvcRequestMatcher(HttpMethod.POST, "/login"))), new UsernamePasswordAuthenticationFilter(authenticationManager()), new BasicAuthenticationFilter(authenticationManager()), new OAuth2AuthorizationRequestRedirectFilter(clientRegistrationRepository()), diff --git a/src/main/java/nextstep/app/ui/AccountController.java b/src/main/java/nextstep/app/ui/AccountController.java index 3e1d455..a1fb16e 100644 --- a/src/main/java/nextstep/app/ui/AccountController.java +++ b/src/main/java/nextstep/app/ui/AccountController.java @@ -1,6 +1,7 @@ package nextstep.app.ui; import jakarta.servlet.http.HttpServletRequest; +import nextstep.security.web.csrf.CsrfToken; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -17,8 +18,8 @@ public String getAccountPage(HttpServletRequest request, Model model) { model.addAttribute("username", "username"); model.addAttribute("email", "username@example.com"); -// CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); -// model.addAttribute("csrfToken", csrfToken); + CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + model.addAttribute("csrfToken", csrfToken); return "account"; } @@ -30,4 +31,4 @@ public String updateAccount(@RequestParam("username") String username, @RequestP return "redirect:/account"; } -} \ No newline at end of file +} diff --git a/src/main/java/nextstep/security/access/AccessDeniedHandler.java b/src/main/java/nextstep/security/access/AccessDeniedHandler.java new file mode 100644 index 0000000..395a082 --- /dev/null +++ b/src/main/java/nextstep/security/access/AccessDeniedHandler.java @@ -0,0 +1,10 @@ +package nextstep.security.access; + +import jakarta.servlet.http.HttpServletResponse; + +public class AccessDeniedHandler { + + public void handle(HttpServletResponse response) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } +} diff --git a/src/main/java/nextstep/security/web/csrf/CsrfFilter.java b/src/main/java/nextstep/security/web/csrf/CsrfFilter.java new file mode 100644 index 0000000..f6044e1 --- /dev/null +++ b/src/main/java/nextstep/security/web/csrf/CsrfFilter.java @@ -0,0 +1,93 @@ +package nextstep.security.web.csrf; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import nextstep.security.access.AccessDeniedHandler; +import nextstep.security.access.MvcRequestMatcher; +import nextstep.security.access.RequestMatcher; +import org.springframework.lang.Nullable; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Objects; +import java.util.Set; + +public final class CsrfFilter extends OncePerRequestFilter { + + private static final RequestMatcher DEFAULT_CSRF_MATCHER = new DefaultRequiresCsrfMatcher(); + + private final CsrfTokenRepository tokenRepository = new HttpSessionCsrfTokenRepository(); + private final Set ignoringRequestMatchers; + private RequestMatcher requiredCsrfRequestMatcher = DEFAULT_CSRF_MATCHER; + private final AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandler(); + + public CsrfFilter(Set ignoringRequestMatchers) { + this.ignoringRequestMatchers = ignoringRequestMatchers; + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + return this.ignoringRequestMatchers.stream() + .anyMatch(matcher -> matcher.matches(request)); + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + CsrfToken token = setUpToken(request, response); + + if (!this.requiredCsrfRequestMatcher.matches(request)) { + filterChain.doFilter(request, response); + return; + } + + // Need to check CSRF token + String actualToken = getActualToken(request, token); + if (!Objects.equals(token.getToken(), actualToken)) { + this.accessDeniedHandler.handle(response); + return; + } + + // CSRF token is valid + filterChain.doFilter(request, response); + } + + @Nullable + private String getActualToken(HttpServletRequest request, CsrfToken token) { + String actualToken = request.getHeader(token.getHeaderName()); + + if (actualToken == null) { + actualToken = request.getParameter(token.getParameterName()); + } + + return actualToken; + } + + private CsrfToken setUpToken(HttpServletRequest request, HttpServletResponse response) { + CsrfToken csrfToken = this.tokenRepository.loadToken(request); + if (csrfToken == null) { + csrfToken = this.tokenRepository.generateToken(); + this.tokenRepository.saveToken(csrfToken, request, response); + } + request.setAttribute(CsrfToken.class.getName(), csrfToken); + request.setAttribute(csrfToken.getParameterName(), csrfToken); + + return csrfToken; + } + + private static final class DefaultRequiresCsrfMatcher implements RequestMatcher { + + private static final Set ALLOWED_METHODS = Set.of("GET", "HEAD", "TRACE", "OPTIONS"); + + @Override + public boolean matches(HttpServletRequest request) { + // 허용한 메서드에 포함되지 않는 메서드라면 CSRF 토큰 검증이 필요하다. + return !ALLOWED_METHODS.contains(request.getMethod()); + } + } +} diff --git a/src/main/java/nextstep/security/web/csrf/CsrfToken.java b/src/main/java/nextstep/security/web/csrf/CsrfToken.java new file mode 100644 index 0000000..cf428bb --- /dev/null +++ b/src/main/java/nextstep/security/web/csrf/CsrfToken.java @@ -0,0 +1,10 @@ +package nextstep.security.web.csrf; + +public interface CsrfToken { + + String getHeaderName(); + + String getParameterName(); + + String getToken(); +} diff --git a/src/main/java/nextstep/security/web/csrf/CsrfTokenRepository.java b/src/main/java/nextstep/security/web/csrf/CsrfTokenRepository.java new file mode 100644 index 0000000..7c9af92 --- /dev/null +++ b/src/main/java/nextstep/security/web/csrf/CsrfTokenRepository.java @@ -0,0 +1,13 @@ +package nextstep.security.web.csrf; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public interface CsrfTokenRepository { + + CsrfToken generateToken(); + + void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response); + + CsrfToken loadToken(HttpServletRequest request); +} diff --git a/src/main/java/nextstep/security/web/csrf/DefaultCsrfToken.java b/src/main/java/nextstep/security/web/csrf/DefaultCsrfToken.java new file mode 100644 index 0000000..5105395 --- /dev/null +++ b/src/main/java/nextstep/security/web/csrf/DefaultCsrfToken.java @@ -0,0 +1,35 @@ +package nextstep.security.web.csrf; + +import org.springframework.util.Assert; + +public class DefaultCsrfToken implements CsrfToken { + + private final String headerName; + private final String parameterName; + private final String token; + + public DefaultCsrfToken(String headerName, String parameterName, String token) { + Assert.hasText(headerName, "headerName cannot be empty"); + Assert.hasText(parameterName, "parameterName cannot be empty"); + Assert.hasText(token, "token cannot be empty"); + + this.headerName = headerName; + this.parameterName = parameterName; + this.token = token; + } + + @Override + public String getHeaderName() { + return this.headerName; + } + + @Override + public String getParameterName() { + return this.parameterName; + } + + @Override + public String getToken() { + return this.token; + } +} diff --git a/src/main/java/nextstep/security/web/csrf/HttpSessionCsrfTokenRepository.java b/src/main/java/nextstep/security/web/csrf/HttpSessionCsrfTokenRepository.java new file mode 100644 index 0000000..2c6401d --- /dev/null +++ b/src/main/java/nextstep/security/web/csrf/HttpSessionCsrfTokenRepository.java @@ -0,0 +1,60 @@ +package nextstep.security.web.csrf; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.springframework.lang.Nullable; + +import java.util.UUID; + +/** + * Server-Side Rendering 시에는 HttpSessionCsrfTokenRepository 사용. + * -> 쿠키에 CSRF 토큰을 저장하지 않고 세션에만 저장하므로 헤더에 CSRF 토큰을 실을 수는 없음. (테스트에서는 헤더에 CSRF 토큰을 실음) + * -> 반면에 SSR에서는 html hidden 필드에 CSRF 토큰을 '_csrf' 파라미터에 실어서 전송할 수 있음. + * Client-Side Rendering 시에는 CookieCsrfTokenRepository 사용. + * -> 쿠키에 CSRF 토큰 값을 저장해야 하므로 프론트에서 쿠키값을 읽어서 헤더에 CSRF 토큰을 실을 수 있음. + */ +public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository { + + private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN"; + private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf"; + private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName() + .concat(".CSRF_TOKEN"); + + private final String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME; + + @Override + public CsrfToken generateToken() { + return new DefaultCsrfToken(DEFAULT_CSRF_HEADER_NAME, DEFAULT_CSRF_PARAMETER_NAME, createNewToken()); + } + + @Override + public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { + if (token != null) { + HttpSession session = request.getSession(); + session.setAttribute(this.sessionAttributeName, token); + return; + } + + // when token is null, remove CSRF Token from the session + HttpSession session = request.getSession(false); + if (session != null) { + session.removeAttribute(this.sessionAttributeName); + } + } + + @Override + @Nullable + public CsrfToken loadToken(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session == null) { + return null; + } + + return (CsrfToken) session.getAttribute(this.sessionAttributeName); + } + + private String createNewToken() { + return UUID.randomUUID().toString(); + } +} diff --git a/src/main/resources/templates/account.html b/src/main/resources/templates/account.html index 1c99a09..f3e1058 100644 --- a/src/main/resources/templates/account.html +++ b/src/main/resources/templates/account.html @@ -19,7 +19,8 @@

Account Information

Update Account Information

- + + diff --git a/src/test/java/nextstep/app/CsrfTest.java b/src/test/java/nextstep/app/CsrfTest.java index 7b745ac..ade8264 100644 --- a/src/test/java/nextstep/app/CsrfTest.java +++ b/src/test/java/nextstep/app/CsrfTest.java @@ -48,7 +48,7 @@ public void ignoringRequestMatchers() throws Exception { @Test public void includeToken() throws Exception { - MvcResult mvcResult = mockMvc.perform(get("/account")) + MvcResult mvcResult = mockMvc.perform(get("/account")) // GET 요청이므로 CSRF 검증 X .andExpect(status().isOk()) .andReturn(); @@ -91,4 +91,4 @@ private String extractCsrfTokenFromPage(MvcResult mvcResult) throws UnsupportedE throw new IllegalStateException("CSRF token not found in response"); } } -} \ No newline at end of file +} From 94f95dfbfe9460ae76d63e172a0a6dcf2adcb1f2 Mon Sep 17 00:00:00 2001 From: haero77 Date: Sat, 15 Mar 2025 18:07:28 +0900 Subject: [PATCH 02/14] =?UTF-8?q?(step1)=20HttpSecurityConfiguration?= =?UTF-8?q?=EC=9D=84=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20HttpSecurity=20?= =?UTF-8?q?=EB=B9=88=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/app/SecurityApplication.java | 1 - .../java/nextstep/app/SecurityConfig.java | 17 +++-- .../annotation/web/SecurityConfigurer.java | 10 +++ .../annotation/web/builders/HttpSecurity.java | 62 +++++++++++++++++++ .../web/configuration/EnableWebSecurity.java | 15 +++++ .../HttpSecurityConfiguration.java | 14 +++++ 6 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 src/main/java/nextstep/security/config/annotation/web/SecurityConfigurer.java create mode 100644 src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java create mode 100644 src/main/java/nextstep/security/config/annotation/web/configuration/EnableWebSecurity.java create mode 100644 src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java diff --git a/src/main/java/nextstep/app/SecurityApplication.java b/src/main/java/nextstep/app/SecurityApplication.java index 64578ce..6a46aaf 100644 --- a/src/main/java/nextstep/app/SecurityApplication.java +++ b/src/main/java/nextstep/app/SecurityApplication.java @@ -9,5 +9,4 @@ public class SecurityApplication { public static void main(String[] args) { SpringApplication.run(SecurityApplication.class, args); } - } diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 38914a9..7b7631c 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -15,10 +15,10 @@ 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.config.SecurityFilterChain; +import nextstep.security.config.*; +import nextstep.security.config.annotation.web.builders.HttpSecurity; +import nextstep.security.config.annotation.web.configuration.EnableWebSecurity; +import nextstep.security.config.annotation.web.configuration.HttpSecurityConfiguration; import nextstep.security.context.SecurityContextHolderFilter; import nextstep.security.userdetails.UserDetailsService; import nextstep.security.web.csrf.CsrfFilter; @@ -26,6 +26,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Import; import org.springframework.http.HttpMethod; import java.util.*; @@ -33,6 +34,7 @@ @Configuration @EnableAspectJAutoProxy @EnableConfigurationProperties(OAuth2ClientProperties.class) +@EnableWebSecurity public class SecurityConfig { private final UserDetailsService userDetailsService; @@ -74,6 +76,13 @@ public AuthenticationManager authenticationManager() { new OAuth2LoginAuthenticationProvider(oAuth2UserService))); } + @Bean + public SecurityFilterChain securityFilterChain2(HttpSecurity http) { + return http + .csrf() + .build(); + } + @Bean public SecurityFilterChain securityFilterChain() { return new DefaultSecurityFilterChain( diff --git a/src/main/java/nextstep/security/config/annotation/web/SecurityConfigurer.java b/src/main/java/nextstep/security/config/annotation/web/SecurityConfigurer.java new file mode 100644 index 0000000..4abe021 --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/web/SecurityConfigurer.java @@ -0,0 +1,10 @@ +package nextstep.security.config.annotation.web; + +import nextstep.security.config.annotation.web.builders.HttpSecurity; + +public interface SecurityConfigurer { + + void init(HttpSecurity http); + + void configure(HttpSecurity http); +} diff --git a/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java b/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java new file mode 100644 index 0000000..e82b326 --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java @@ -0,0 +1,62 @@ +package nextstep.security.config.annotation.web.builders; + +import jakarta.servlet.Filter; +import nextstep.security.config.DefaultSecurityFilterChain; +import nextstep.security.config.SecurityFilterChain; +import nextstep.security.config.annotation.web.SecurityConfigurer; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +public class HttpSecurity { + + private final LinkedHashMap, SecurityConfigurer> configurers = new LinkedHashMap<>(); + private final List filters = new ArrayList<>(); + + public SecurityFilterChain build() { + init(); + configure(); + return new DefaultSecurityFilterChain(filters); + } + + private void init() { + for (SecurityConfigurer configurer : this.configurers.values()) { + configurer.init(this); + } + } + + private void configure() { + for (SecurityConfigurer configurer : this.configurers.values()) { + configurer.configure(this); + } + } + + public HttpSecurity csrf() { + return this; + } + + public HttpSecurity httpBasic() { + return this; + } + + public HttpSecurity formLogin() { + return this; + } + + public HttpSecurity authorizeHttpRequests() { + return this; + } + + private SecurityConfigurer getOrApply(SecurityConfigurer configurer) { + Class clazz = configurer.getClass(); + + SecurityConfigurer existingConfig = this.configurers.get(clazz); + if (existingConfig != null) { + return existingConfig; + } + + this.configurers.put(clazz, configurer); + return configurer; + } +} diff --git a/src/main/java/nextstep/security/config/annotation/web/configuration/EnableWebSecurity.java b/src/main/java/nextstep/security/config/annotation/web/configuration/EnableWebSecurity.java new file mode 100644 index 0000000..ad64be2 --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/web/configuration/EnableWebSecurity.java @@ -0,0 +1,15 @@ +package nextstep.security.config.annotation.web.configuration; + +import org.springframework.context.annotation.Import; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Import({HttpSecurityConfiguration.class}) +public @interface EnableWebSecurity { + +} diff --git a/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java new file mode 100644 index 0000000..fcc6f41 --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -0,0 +1,14 @@ +package nextstep.security.config.annotation.web.configuration; + +import nextstep.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class HttpSecurityConfiguration { + + @Bean + HttpSecurity httpSecurity() { + return new HttpSecurity(); + } +} From 4a4d0a70c5603806b00bc5b423d04184c69f1649 Mon Sep 17 00:00:00 2001 From: haero77 Date: Sun, 16 Mar 2025 00:34:37 +0900 Subject: [PATCH 03/14] =?UTF-8?q?(step1)=20httpSecurity=20-=20csrf=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/SecurityConfig.java | 54 +++++++++---------- .../nextstep/security/config/Customizer.java | 12 +++++ .../annotation/web/builders/HttpSecurity.java | 13 +++-- .../web/configurers/CsrfConfigurer.java | 51 ++++++++++++++++++ .../security/web/csrf/CsrfFilter.java | 16 ++++-- .../security/web/util/AndRequestMatcher.java | 31 +++++++++++ .../web/util/NegatedRequestMatcher.java | 18 +++++++ .../security/web/util/OrRequestMatcher.java | 27 ++++++++++ 8 files changed, 189 insertions(+), 33 deletions(-) create mode 100644 src/main/java/nextstep/security/config/Customizer.java create mode 100644 src/main/java/nextstep/security/config/annotation/web/configurers/CsrfConfigurer.java create mode 100644 src/main/java/nextstep/security/web/util/AndRequestMatcher.java create mode 100644 src/main/java/nextstep/security/web/util/NegatedRequestMatcher.java create mode 100644 src/main/java/nextstep/security/web/util/OrRequestMatcher.java diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 7b7631c..3653224 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -5,31 +5,31 @@ 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.authentication.AuthenticationManager; +import nextstep.security.authentication.DaoAuthenticationProvider; +import nextstep.security.authentication.ProviderManager; import nextstep.security.authorization.*; -import nextstep.security.config.*; +import nextstep.security.config.DelegatingFilterProxy; +import nextstep.security.config.FilterChainProxy; +import nextstep.security.config.SecurityFilterChain; import nextstep.security.config.annotation.web.builders.HttpSecurity; import nextstep.security.config.annotation.web.configuration.EnableWebSecurity; -import nextstep.security.config.annotation.web.configuration.HttpSecurityConfiguration; -import nextstep.security.context.SecurityContextHolderFilter; import nextstep.security.userdetails.UserDetailsService; -import nextstep.security.web.csrf.CsrfFilter; 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.context.annotation.Import; import org.springframework.http.HttpMethod; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @Configuration @EnableAspectJAutoProxy @@ -48,8 +48,8 @@ public SecurityConfig(UserDetailsService userDetailsService, OAuth2UserService o } @Bean - public DelegatingFilterProxy delegatingFilterProxy() { - return new DelegatingFilterProxy(filterChainProxy(List.of(securityFilterChain()))); + public DelegatingFilterProxy delegatingFilterProxy(HttpSecurity http) { + return new DelegatingFilterProxy(filterChainProxy(List.of(securityFilterChain2(http)))); } @Bean @@ -79,24 +79,24 @@ public AuthenticationManager authenticationManager() { @Bean public SecurityFilterChain securityFilterChain2(HttpSecurity http) { return http - .csrf() + .csrf(c -> c.ignoringRequestMatchers("/login")) .build(); } - @Bean - public SecurityFilterChain securityFilterChain() { - return new DefaultSecurityFilterChain( - List.of( - new SecurityContextHolderFilter(), - new CsrfFilter(Set.of(new MvcRequestMatcher(HttpMethod.POST, "/login"))), - new UsernamePasswordAuthenticationFilter(authenticationManager()), - new BasicAuthenticationFilter(authenticationManager()), - new OAuth2AuthorizationRequestRedirectFilter(clientRegistrationRepository()), - new OAuth2LoginAuthenticationFilter(clientRegistrationRepository(), new OAuth2AuthorizedClientRepository(), authenticationManager()), - new AuthorizationFilter(requestAuthorizationManager()) - ) - ); - } +// @Bean +// public SecurityFilterChain securityFilterChain() { +// return new DefaultSecurityFilterChain( +// List.of( +// new SecurityContextHolderFilter(), +// new CsrfFilter(Set.of(new MvcRequestMatcher(HttpMethod.POST, "/login"))), +// new UsernamePasswordAuthenticationFilter(authenticationManager()), +// new BasicAuthenticationFilter(authenticationManager()), +// new OAuth2AuthorizationRequestRedirectFilter(clientRegistrationRepository()), +// new OAuth2LoginAuthenticationFilter(clientRegistrationRepository(), new OAuth2AuthorizedClientRepository(), authenticationManager()), +// new AuthorizationFilter(requestAuthorizationManager()) +// ) +// ); +// } @Bean public RequestMatcherDelegatingAuthorizationManager requestAuthorizationManager() { diff --git a/src/main/java/nextstep/security/config/Customizer.java b/src/main/java/nextstep/security/config/Customizer.java new file mode 100644 index 0000000..f3c8891 --- /dev/null +++ b/src/main/java/nextstep/security/config/Customizer.java @@ -0,0 +1,12 @@ +package nextstep.security.config; + +@FunctionalInterface +public interface Customizer { + + void customize(T t); + + static Customizer withDefaults() { + return (t) -> { + }; + } +} diff --git a/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java b/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java index e82b326..32675c1 100644 --- a/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java +++ b/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java @@ -1,9 +1,11 @@ package nextstep.security.config.annotation.web.builders; import jakarta.servlet.Filter; +import nextstep.security.config.Customizer; import nextstep.security.config.DefaultSecurityFilterChain; import nextstep.security.config.SecurityFilterChain; import nextstep.security.config.annotation.web.SecurityConfigurer; +import nextstep.security.config.annotation.web.configurers.CsrfConfigurer; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -32,7 +34,8 @@ private void configure() { } } - public HttpSecurity csrf() { + public HttpSecurity csrf(Customizer csrfCustomizer) { + csrfCustomizer.customize(getOrApply(new CsrfConfigurer())); return this; } @@ -48,12 +51,16 @@ public HttpSecurity authorizeHttpRequests() { return this; } - private SecurityConfigurer getOrApply(SecurityConfigurer configurer) { + public void addFilter(Filter filter) { + this.filters.add(filter); + } + + private C getOrApply(C configurer) { Class clazz = configurer.getClass(); SecurityConfigurer existingConfig = this.configurers.get(clazz); if (existingConfig != null) { - return existingConfig; + return (C) existingConfig; } this.configurers.put(clazz, configurer); diff --git a/src/main/java/nextstep/security/config/annotation/web/configurers/CsrfConfigurer.java b/src/main/java/nextstep/security/config/annotation/web/configurers/CsrfConfigurer.java new file mode 100644 index 0000000..3ec6abc --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/web/configurers/CsrfConfigurer.java @@ -0,0 +1,51 @@ +package nextstep.security.config.annotation.web.configurers; + +import nextstep.security.access.MvcRequestMatcher; +import nextstep.security.access.RequestMatcher; +import nextstep.security.config.annotation.web.SecurityConfigurer; +import nextstep.security.config.annotation.web.builders.HttpSecurity; +import nextstep.security.web.csrf.CsrfFilter; +import nextstep.security.web.util.AndRequestMatcher; +import nextstep.security.web.util.NegatedRequestMatcher; +import nextstep.security.web.util.OrRequestMatcher; + +import java.util.ArrayList; +import java.util.List; + +public class CsrfConfigurer implements SecurityConfigurer { + + private RequestMatcher csrfRequiredRequestMathcer = CsrfFilter.DEFAULT_CSRF_MATCHER; + private List ignoredCsrfProtectionMatchers = new ArrayList<>(); + + @Override + public void init(HttpSecurity http) { + } + + @Override + public void configure(HttpSecurity http) { + CsrfFilter filter = new CsrfFilter(); + + RequestMatcher csrfProtectionRequired = getCsrfProtectionRequiredRequestMatcher(); + filter.setRequireCsrfProtectionMatcher(csrfProtectionRequired); + + http.addFilter(filter); + } + + private RequestMatcher getCsrfProtectionRequiredRequestMatcher() { + if (this.ignoredCsrfProtectionMatchers.isEmpty()) { + return this.csrfRequiredRequestMathcer; + } + + return new AndRequestMatcher( + this.csrfRequiredRequestMathcer, + new NegatedRequestMatcher(new OrRequestMatcher(this.ignoredCsrfProtectionMatchers)) + ); + } + + public void ignoringRequestMatchers(String... patterns) { + for (String pattern : patterns) { + MvcRequestMatcher mvc = new MvcRequestMatcher(null, pattern); + ignoredCsrfProtectionMatchers.add(mvc); + } + } +} diff --git a/src/main/java/nextstep/security/web/csrf/CsrfFilter.java b/src/main/java/nextstep/security/web/csrf/CsrfFilter.java index f6044e1..ba808e7 100644 --- a/src/main/java/nextstep/security/web/csrf/CsrfFilter.java +++ b/src/main/java/nextstep/security/web/csrf/CsrfFilter.java @@ -8,6 +8,7 @@ import nextstep.security.access.MvcRequestMatcher; import nextstep.security.access.RequestMatcher; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @@ -16,13 +17,17 @@ public final class CsrfFilter extends OncePerRequestFilter { - private static final RequestMatcher DEFAULT_CSRF_MATCHER = new DefaultRequiresCsrfMatcher(); + public static final RequestMatcher DEFAULT_CSRF_MATCHER = new DefaultRequiresCsrfMatcher(); private final CsrfTokenRepository tokenRepository = new HttpSessionCsrfTokenRepository(); private final Set ignoringRequestMatchers; - private RequestMatcher requiredCsrfRequestMatcher = DEFAULT_CSRF_MATCHER; + private RequestMatcher requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER; private final AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandler(); + public CsrfFilter() { + this(Set.of()); + } + public CsrfFilter(Set ignoringRequestMatchers) { this.ignoringRequestMatchers = ignoringRequestMatchers; } @@ -41,7 +46,7 @@ protected void doFilterInternal( ) throws ServletException, IOException { CsrfToken token = setUpToken(request, response); - if (!this.requiredCsrfRequestMatcher.matches(request)) { + if (!this.requireCsrfProtectionMatcher.matches(request)) { filterChain.doFilter(request, response); return; } @@ -80,6 +85,11 @@ private CsrfToken setUpToken(HttpServletRequest request, HttpServletResponse res return csrfToken; } + public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) { + Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null"); + this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher; + } + private static final class DefaultRequiresCsrfMatcher implements RequestMatcher { private static final Set ALLOWED_METHODS = Set.of("GET", "HEAD", "TRACE", "OPTIONS"); diff --git a/src/main/java/nextstep/security/web/util/AndRequestMatcher.java b/src/main/java/nextstep/security/web/util/AndRequestMatcher.java new file mode 100644 index 0000000..dff49cc --- /dev/null +++ b/src/main/java/nextstep/security/web/util/AndRequestMatcher.java @@ -0,0 +1,31 @@ +package nextstep.security.web.util; + +import jakarta.servlet.http.HttpServletRequest; +import nextstep.security.access.RequestMatcher; +import org.springframework.util.Assert; + +import java.util.List; + +public class AndRequestMatcher implements RequestMatcher { + + private final List requestMatchers; + + public AndRequestMatcher(RequestMatcher... requestMatchers) { + this(List.of(requestMatchers)); + } + + public AndRequestMatcher(List requestMatchers) { + Assert.notEmpty(requestMatchers, "requestMatchers cannot be empty"); + this.requestMatchers = requestMatchers; + } + + @Override + public boolean matches(HttpServletRequest request) { + for (RequestMatcher requestMatcher : this.requestMatchers) { + if (!requestMatcher.matches(request)) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/nextstep/security/web/util/NegatedRequestMatcher.java b/src/main/java/nextstep/security/web/util/NegatedRequestMatcher.java new file mode 100644 index 0000000..647eb42 --- /dev/null +++ b/src/main/java/nextstep/security/web/util/NegatedRequestMatcher.java @@ -0,0 +1,18 @@ +package nextstep.security.web.util; + +import jakarta.servlet.http.HttpServletRequest; +import nextstep.security.access.RequestMatcher; + +public class NegatedRequestMatcher implements RequestMatcher { + + private final RequestMatcher matcher; + + public NegatedRequestMatcher(RequestMatcher matcher) { + this.matcher = matcher; + } + + @Override + public boolean matches(HttpServletRequest request) { + return !matcher.matches(request); + } +} diff --git a/src/main/java/nextstep/security/web/util/OrRequestMatcher.java b/src/main/java/nextstep/security/web/util/OrRequestMatcher.java new file mode 100644 index 0000000..5d28513 --- /dev/null +++ b/src/main/java/nextstep/security/web/util/OrRequestMatcher.java @@ -0,0 +1,27 @@ +package nextstep.security.web.util; + +import jakarta.servlet.http.HttpServletRequest; +import nextstep.security.access.RequestMatcher; +import org.springframework.util.Assert; + +import java.util.List; + +public class OrRequestMatcher implements RequestMatcher { + + private final List matchers; + + public OrRequestMatcher(List matchers) { + Assert.notEmpty(matchers, "matchers cannot be empty"); + this.matchers = matchers; + } + + @Override + public boolean matches(HttpServletRequest request) { + for (RequestMatcher matcher : matchers) { + if (matcher.matches(request)) { + return true; + } + } + return false; + } +} From 53b5a98d448e4ceb47d8c12e6865a1d9cae1ec48 Mon Sep 17 00:00:00 2001 From: haero77 Date: Sun, 16 Mar 2025 00:57:40 +0900 Subject: [PATCH 04/14] (step2) set up requirements --- README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b86151f..b1d56ae 100644 --- a/README.md +++ b/README.md @@ -15,17 +15,57 @@ ## 1단계 - SecurityFilterChain 리팩토링 > 주요 클래스 -> - HttpSecurity -> - HttpSecurityConfiguration -> - SecurityConfigurer -> - Customizer +> - `HttpSecurity` +> - `HttpSecurityConfiguration` +> - `SecurityConfigurer` +> - `Customizer` +> - `@EnableWebSecurity` + +- [x] HttpSecurity 구현 +- [x] @EnableWebSecurity, HttpSecurityConfiguration를 이용한 HttpSecurity 빈 등록 +- [x] csrf 필터를 configurer를 이용하여 설정 -- [ ] HttpSecurity 구현 -- [ ] HttpSecurityConfiguration를 이용한 HttpSecurity 빈 등록 -- ## 2단계 - 인증 관련 리팩토링 +- [ ] `.formLogin()` 메서드를 사용하여 폼 로그인 기능을 설정하고, U`sernamePasswordAuthenticationFilter`를 자동으로 추가한다. +- [ ] `.httpBasic()` 메서드를 사용해 HTTP Basic 인증을 설정하고, `BasicAuthenticationFilter`를 자동으로 추가한다. +- [ ] `securityContext()`는 유저가 직접 설정할 수 없도록 하고, HttpSecurity가 빈으로 등록될 때 자동으로 설정한다. + ## 3단계 - 인가 관련 리팩토링 -## 4단계 - Auto Configuration 적용 +## 4단계 - Auto Configuration 적용 + +--- + +# 플로우차트를 활용한 깊은 이해 + +## CSRF 공격 대응 + +### CSRF 토큰 적용 전 + +```mermaid +sequenceDiagram + participant User + participant HackerSite + participant Bank + User ->> HackerSite: 악성 링크 클릭 + HackerSite ->> Bank: POST 이체 요청 (자동 실행) + Bank -->> HackerSite: 성공 (세션 쿠키만으로 인증) +``` + +### CSRF 토큰 적용 후 + +```mermaid +sequenceDiagram + participant User + participant HackerSite + participant Bank + User ->> Bank: 로그인 + Bank -->> User: 세션 쿠키 + CSRF 토큰(HTML 숨김 필드) + User ->> HackerSite: 악성 링크 클릭 + HackerSite ->> Bank: POST 이체 요청 (세션 쿠키만 포함) + Bank ->> Bank: 1. 세션에서 CSRF 토큰 조회 + Bank ->> Bank: 2. 요청의 CSRF 토큰 비교 + Bank -->> HackerSite: 403 Forbidden (토큰 없음) +``` From f9a0f3c49549b4597236ba4d89e3a86c000b1f34 Mon Sep 17 00:00:00 2001 From: haero77 Date: Sun, 16 Mar 2025 00:53:12 +0900 Subject: [PATCH 05/14] =?UTF-8?q?(step2)=20httpSecurity=20-=20FormLoginCon?= =?UTF-8?q?figurer=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20UsernamePasswo?= =?UTF-8?q?rdAuthenticationFilter=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/SecurityConfig.java | 2 ++ .../annotation/web/builders/HttpSecurity.java | 22 +++++++++++++++---- .../HttpSecurityConfiguration.java | 5 +++-- .../web/configurers/FormLoginConfigurer.java | 20 +++++++++++++++++ 4 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 src/main/java/nextstep/security/config/annotation/web/configurers/FormLoginConfigurer.java diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 3653224..661bd17 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -14,6 +14,7 @@ import nextstep.security.authentication.DaoAuthenticationProvider; import nextstep.security.authentication.ProviderManager; import nextstep.security.authorization.*; +import nextstep.security.config.Customizer; import nextstep.security.config.DelegatingFilterProxy; import nextstep.security.config.FilterChainProxy; import nextstep.security.config.SecurityFilterChain; @@ -80,6 +81,7 @@ public AuthenticationManager authenticationManager() { public SecurityFilterChain securityFilterChain2(HttpSecurity http) { return http .csrf(c -> c.ignoringRequestMatchers("/login")) + .formLogin(Customizer.withDefaults()) .build(); } diff --git a/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java b/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java index 32675c1..6a9b5b4 100644 --- a/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java +++ b/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java @@ -1,20 +1,33 @@ package nextstep.security.config.annotation.web.builders; import jakarta.servlet.Filter; +import nextstep.security.authentication.AuthenticationManager; import nextstep.security.config.Customizer; import nextstep.security.config.DefaultSecurityFilterChain; import nextstep.security.config.SecurityFilterChain; import nextstep.security.config.annotation.web.SecurityConfigurer; import nextstep.security.config.annotation.web.configurers.CsrfConfigurer; +import nextstep.security.config.annotation.web.configurers.FormLoginConfigurer; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; +import java.util.*; public class HttpSecurity { private final LinkedHashMap, SecurityConfigurer> configurers = new LinkedHashMap<>(); private final List filters = new ArrayList<>(); + private final Map, Object> sharedObjects = new HashMap<>(); + + public HttpSecurity(AuthenticationManager authenticationManager) { + setSharedObject(AuthenticationManager.class, authenticationManager); + } + + public C getSharedObject(Class sharedType) { + return (C) this.sharedObjects.get(sharedType); + } + + private void setSharedObject(Class sharedType, C object) { + this.sharedObjects.put(sharedType, object); + } public SecurityFilterChain build() { init(); @@ -43,7 +56,8 @@ public HttpSecurity httpBasic() { return this; } - public HttpSecurity formLogin() { + public HttpSecurity formLogin(Customizer formLoginCustomizer) { + formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer())); return this; } diff --git a/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index fcc6f41..1fc8ca6 100644 --- a/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -1,5 +1,6 @@ package nextstep.security.config.annotation.web.configuration; +import nextstep.security.authentication.AuthenticationManager; import nextstep.security.config.annotation.web.builders.HttpSecurity; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -8,7 +9,7 @@ public class HttpSecurityConfiguration { @Bean - HttpSecurity httpSecurity() { - return new HttpSecurity(); + HttpSecurity httpSecurity(AuthenticationManager authenticationManager) { + return new HttpSecurity(authenticationManager); } } diff --git a/src/main/java/nextstep/security/config/annotation/web/configurers/FormLoginConfigurer.java b/src/main/java/nextstep/security/config/annotation/web/configurers/FormLoginConfigurer.java new file mode 100644 index 0000000..cea1dcb --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/web/configurers/FormLoginConfigurer.java @@ -0,0 +1,20 @@ +package nextstep.security.config.annotation.web.configurers; + +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.UsernamePasswordAuthenticationFilter; +import nextstep.security.config.annotation.web.SecurityConfigurer; +import nextstep.security.config.annotation.web.builders.HttpSecurity; + +public class FormLoginConfigurer implements SecurityConfigurer { + + @Override + public void init(HttpSecurity http) { + } + + @Override + public void configure(HttpSecurity http) { + AuthenticationManager manager = http.getSharedObject(AuthenticationManager.class); + UsernamePasswordAuthenticationFilter filter = new UsernamePasswordAuthenticationFilter(manager); + http.addFilter(filter); + } +} From 96849c69c8d94975378f199c83d3af872c7b9d1c Mon Sep 17 00:00:00 2001 From: haero77 Date: Sun, 16 Mar 2025 00:57:23 +0900 Subject: [PATCH 06/14] =?UTF-8?q?(step2)=20httpSecurity=20-=20HttpBasicCon?= =?UTF-8?q?figurer=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20BasicAuthentic?= =?UTF-8?q?ationFilter=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/SecurityConfig.java | 1 + .../annotation/web/builders/HttpSecurity.java | 8 +++++--- .../web/configurers/HttpBasicConfigurer.java | 20 +++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 src/main/java/nextstep/security/config/annotation/web/configurers/HttpBasicConfigurer.java diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 661bd17..372a4cd 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -82,6 +82,7 @@ public SecurityFilterChain securityFilterChain2(HttpSecurity http) { return http .csrf(c -> c.ignoringRequestMatchers("/login")) .formLogin(Customizer.withDefaults()) + .httpBasic(Customizer.withDefaults()) .build(); } diff --git a/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java b/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java index 6a9b5b4..52a8c83 100644 --- a/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java +++ b/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java @@ -8,6 +8,7 @@ import nextstep.security.config.annotation.web.SecurityConfigurer; import nextstep.security.config.annotation.web.configurers.CsrfConfigurer; import nextstep.security.config.annotation.web.configurers.FormLoginConfigurer; +import nextstep.security.config.annotation.web.configurers.HttpBasicConfigurer; import java.util.*; @@ -52,12 +53,13 @@ public HttpSecurity csrf(Customizer csrfCustomizer) { return this; } - public HttpSecurity httpBasic() { + public HttpSecurity formLogin(Customizer formLoginCustomizer) { + formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer())); return this; } - public HttpSecurity formLogin(Customizer formLoginCustomizer) { - formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer())); + public HttpSecurity httpBasic(Customizer httpBasicCustomizer) { + httpBasicCustomizer.customize(getOrApply(new HttpBasicConfigurer())); return this; } diff --git a/src/main/java/nextstep/security/config/annotation/web/configurers/HttpBasicConfigurer.java b/src/main/java/nextstep/security/config/annotation/web/configurers/HttpBasicConfigurer.java new file mode 100644 index 0000000..de140ca --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/web/configurers/HttpBasicConfigurer.java @@ -0,0 +1,20 @@ +package nextstep.security.config.annotation.web.configurers; + +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authentication.BasicAuthenticationFilter; +import nextstep.security.config.annotation.web.SecurityConfigurer; +import nextstep.security.config.annotation.web.builders.HttpSecurity; + +public class HttpBasicConfigurer implements SecurityConfigurer { + + @Override + public void init(HttpSecurity http) { + } + + @Override + public void configure(HttpSecurity http) { + AuthenticationManager manager = http.getSharedObject(AuthenticationManager.class); + BasicAuthenticationFilter filter = new BasicAuthenticationFilter(manager); + http.addFilter(filter); + } +} From 58387a8807f956bbe85cfafa09fb1317c336b03d Mon Sep 17 00:00:00 2001 From: haero77 Date: Sun, 16 Mar 2025 12:17:05 +0900 Subject: [PATCH 07/14] =?UTF-8?q?(step2)=20httpSecurity=20-=20SecurityCont?= =?UTF-8?q?extConfigurer=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20Security?= =?UTF-8?q?ContextHolderFilter=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../annotation/web/builders/HttpSecurity.java | 6 ++++++ .../HttpSecurityConfiguration.java | 8 +++++++- .../configurers/SecurityContextConfigurer.java | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/main/java/nextstep/security/config/annotation/web/configurers/SecurityContextConfigurer.java diff --git a/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java b/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java index 52a8c83..e1ef067 100644 --- a/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java +++ b/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java @@ -9,6 +9,7 @@ import nextstep.security.config.annotation.web.configurers.CsrfConfigurer; import nextstep.security.config.annotation.web.configurers.FormLoginConfigurer; import nextstep.security.config.annotation.web.configurers.HttpBasicConfigurer; +import nextstep.security.config.annotation.web.configurers.SecurityContextConfigurer; import java.util.*; @@ -67,6 +68,11 @@ public HttpSecurity authorizeHttpRequests() { return this; } + public HttpSecurity securityContext(Customizer securityContextCustomizer) { + securityContextCustomizer.customize(getOrApply(new SecurityContextConfigurer())); + return this; + } + public void addFilter(Filter filter) { this.filters.add(filter); } diff --git a/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index 1fc8ca6..d077d13 100644 --- a/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -1,6 +1,7 @@ package nextstep.security.config.annotation.web.configuration; import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.config.Customizer; import nextstep.security.config.annotation.web.builders.HttpSecurity; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -10,6 +11,11 @@ public class HttpSecurityConfiguration { @Bean HttpSecurity httpSecurity(AuthenticationManager authenticationManager) { - return new HttpSecurity(authenticationManager); + HttpSecurity http = new HttpSecurity(authenticationManager); + + http + .securityContext(Customizer.withDefaults()); + + return http; } } diff --git a/src/main/java/nextstep/security/config/annotation/web/configurers/SecurityContextConfigurer.java b/src/main/java/nextstep/security/config/annotation/web/configurers/SecurityContextConfigurer.java new file mode 100644 index 0000000..ed8b17c --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/web/configurers/SecurityContextConfigurer.java @@ -0,0 +1,18 @@ +package nextstep.security.config.annotation.web.configurers; + +import nextstep.security.config.annotation.web.SecurityConfigurer; +import nextstep.security.config.annotation.web.builders.HttpSecurity; +import nextstep.security.context.SecurityContextHolderFilter; + +public class SecurityContextConfigurer implements SecurityConfigurer { + + @Override + public void init(HttpSecurity http) { + } + + @Override + public void configure(HttpSecurity http) { + SecurityContextHolderFilter filter = new SecurityContextHolderFilter(); + http.addFilter(filter); + } +} From 9329446fedc6f124a1b49d204dc9aa084b646ead Mon Sep 17 00:00:00 2001 From: haero77 Date: Sun, 16 Mar 2025 13:11:53 +0900 Subject: [PATCH 08/14] =?UTF-8?q?(step2)=20httpSecurity=20-=20OAuth2LoginC?= =?UTF-8?q?onfigurer=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20OAuth2=20?= =?UTF-8?q?=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8,=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=ED=95=84=ED=84=B0=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++-- .../java/nextstep/app/SecurityConfig.java | 1 + ...th2AuthorizationRequestRedirectFilter.java | 2 + .../web/OAuth2LoginAuthenticationFilter.java | 5 +++ .../annotation/web/builders/HttpSecurity.java | 13 ++++--- .../HttpSecurityConfiguration.java | 14 ++++++- .../configurers/OAuth2LoginConfigurer.java | 37 +++++++++++++++++++ 7 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 src/main/java/nextstep/security/config/annotation/web/configurers/OAuth2LoginConfigurer.java diff --git a/README.md b/README.md index b1d56ae..955f297 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,11 @@ ## 2단계 - 인증 관련 리팩토링 -- [ ] `.formLogin()` 메서드를 사용하여 폼 로그인 기능을 설정하고, U`sernamePasswordAuthenticationFilter`를 자동으로 추가한다. -- [ ] `.httpBasic()` 메서드를 사용해 HTTP Basic 인증을 설정하고, `BasicAuthenticationFilter`를 자동으로 추가한다. -- [ ] `securityContext()`는 유저가 직접 설정할 수 없도록 하고, HttpSecurity가 빈으로 등록될 때 자동으로 설정한다. +- [x] `.formLogin()` 메서드를 사용하여 폼 로그인 기능을 설정하고, U`sernamePasswordAuthenticationFilter`를 자동으로 추가한다. +- [x] `.httpBasic()` 메서드를 사용해 HTTP Basic 인증을 설정하고, `BasicAuthenticationFilter`를 자동으로 추가한다. +- [x] `.securityContext()` 메서드를 사용하여 `SecurityContextHolderFilter` 자동으로 추가 +- [x] oauth2 리팩토링 + - [x] OAuth2AuthorizationRequestRedirectFilter 등록, OAuth2LoginAuthenticationFilter 등록 ## 3단계 - 인가 관련 리팩토링 diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index 372a4cd..c2ee67a 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -83,6 +83,7 @@ public SecurityFilterChain securityFilterChain2(HttpSecurity http) { .csrf(c -> c.ignoringRequestMatchers("/login")) .formLogin(Customizer.withDefaults()) .httpBasic(Customizer.withDefaults()) + .oauth2Login(Customizer.withDefaults()) .build(); } diff --git a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java index d11c64e..972d13c 100644 --- a/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java +++ b/src/main/java/nextstep/oauth2/web/OAuth2AuthorizationRequestRedirectFilter.java @@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletResponse; import nextstep.oauth2.endpoint.OAuth2AuthorizationRequest; import nextstep.oauth2.registration.ClientRegistrationRepository; +import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @@ -19,6 +20,7 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt private final AuthorizationRequestRepository authorizationRequestRepository = new AuthorizationRequestRepository(); public OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository) { + Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); authorizationRequestResolver = new OAuth2AuthorizationRequestResolver(clientRegistrationRepository, DEFAULT_AUTHORIZATION_REQUEST_BASE_URI); } diff --git a/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java b/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java index a11ebdd..2d91bf7 100644 --- a/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java +++ b/src/main/java/nextstep/oauth2/web/OAuth2LoginAuthenticationFilter.java @@ -33,6 +33,11 @@ public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProce public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository, AuthenticationManager authenticationManager) { super(DEFAULT_LOGIN_REQUEST_BASE_URI, authenticationManager); + + Assert.notNull(authenticationManager, "authenticationManager cannot be null"); + Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); + Assert.notNull(authorizedClientRepository, "authorizationRequestRepository cannot be null"); + this.clientRegistrationRepository = clientRegistrationRepository; this.authorizedClientRepository = authorizedClientRepository; } diff --git a/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java b/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java index e1ef067..2df1943 100644 --- a/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java +++ b/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java @@ -6,10 +6,7 @@ import nextstep.security.config.DefaultSecurityFilterChain; import nextstep.security.config.SecurityFilterChain; import nextstep.security.config.annotation.web.SecurityConfigurer; -import nextstep.security.config.annotation.web.configurers.CsrfConfigurer; -import nextstep.security.config.annotation.web.configurers.FormLoginConfigurer; -import nextstep.security.config.annotation.web.configurers.HttpBasicConfigurer; -import nextstep.security.config.annotation.web.configurers.SecurityContextConfigurer; +import nextstep.security.config.annotation.web.configurers.*; import java.util.*; @@ -19,8 +16,9 @@ public class HttpSecurity { private final List filters = new ArrayList<>(); private final Map, Object> sharedObjects = new HashMap<>(); - public HttpSecurity(AuthenticationManager authenticationManager) { + public HttpSecurity(AuthenticationManager authenticationManager, Map, Object> sharedObjects) { setSharedObject(AuthenticationManager.class, authenticationManager); + this.sharedObjects.putAll(sharedObjects); } public C getSharedObject(Class sharedType) { @@ -64,6 +62,11 @@ public HttpSecurity httpBasic(Customizer httpBasicCustomize return this; } + public HttpSecurity oauth2Login(Customizer oauth2LoginCustomizer) { + oauth2LoginCustomizer.customize(getOrApply(new OAuth2LoginConfigurer())); + return this; + } + public HttpSecurity authorizeHttpRequests() { return this; } diff --git a/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index d077d13..92203c1 100644 --- a/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -3,19 +3,29 @@ import nextstep.security.authentication.AuthenticationManager; import nextstep.security.config.Customizer; import nextstep.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.util.Map; + @Configuration public class HttpSecurityConfiguration { @Bean - HttpSecurity httpSecurity(AuthenticationManager authenticationManager) { - HttpSecurity http = new HttpSecurity(authenticationManager); + HttpSecurity httpSecurity( + ApplicationContext applicationContext, + AuthenticationManager authenticationManager + ) { + HttpSecurity http = new HttpSecurity(authenticationManager, createSharedObjects(applicationContext)); http .securityContext(Customizer.withDefaults()); return http; } + + private Map, Object> createSharedObjects(ApplicationContext applicationContext) { + return Map.of(ApplicationContext.class, applicationContext); + } } diff --git a/src/main/java/nextstep/security/config/annotation/web/configurers/OAuth2LoginConfigurer.java b/src/main/java/nextstep/security/config/annotation/web/configurers/OAuth2LoginConfigurer.java new file mode 100644 index 0000000..88f1eac --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/web/configurers/OAuth2LoginConfigurer.java @@ -0,0 +1,37 @@ +package nextstep.security.config.annotation.web.configurers; + +import nextstep.oauth2.registration.ClientRegistrationRepository; +import nextstep.oauth2.web.OAuth2AuthorizationRequestRedirectFilter; +import nextstep.oauth2.web.OAuth2AuthorizedClientRepository; +import nextstep.oauth2.web.OAuth2LoginAuthenticationFilter; +import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.config.annotation.web.SecurityConfigurer; +import nextstep.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.context.ApplicationContext; + +public class OAuth2LoginConfigurer implements SecurityConfigurer { + + private AuthenticationManager authenticationManager; + private ClientRegistrationRepository clientRegistrationRepository; + private OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository; + + @Override + public void init(HttpSecurity http) { + this.authenticationManager = http.getSharedObject(AuthenticationManager.class); + this.clientRegistrationRepository = http.getSharedObject(ApplicationContext.class).getBean(ClientRegistrationRepository.class); + this.oAuth2AuthorizedClientRepository = new OAuth2AuthorizedClientRepository(); + } + + @Override + public void configure(HttpSecurity http) { + OAuth2AuthorizationRequestRedirectFilter redirectFilter = new OAuth2AuthorizationRequestRedirectFilter(clientRegistrationRepository); + http.addFilter(redirectFilter); + + OAuth2LoginAuthenticationFilter authenticationFilter = new OAuth2LoginAuthenticationFilter( + clientRegistrationRepository, + oAuth2AuthorizedClientRepository, + authenticationManager + ); + http.addFilter(authenticationFilter); + } +} From 555da018f2004510395c99c21f105347924d1657 Mon Sep 17 00:00:00 2001 From: haero77 Date: Tue, 18 Mar 2025 22:18:29 +0900 Subject: [PATCH 09/14] =?UTF-8?q?(step3)=20httpSecurity=20-=20AuthorizeHtt?= =?UTF-8?q?pRequestsConfigurer=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20?= =?UTF-8?q?=EC=9D=B8=EA=B0=80=20=EA=B4=80=EB=A6=AC=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/app/SecurityConfig.java | 44 +++-------- .../AuthenticatedAuthorizationManager.java | 8 +- .../authorization/AuthorizationFilter.java | 18 ++++- ...MatcherDelegatingAuthorizationManager.java | 1 - .../annotation/web/builders/HttpSecurity.java | 38 +++++++++- .../AuthorizeHttpRequestsConfigurer.java | 76 +++++++++++++++++++ 6 files changed, 142 insertions(+), 43 deletions(-) create mode 100644 src/main/java/nextstep/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index c2ee67a..ee029b3 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -5,15 +5,12 @@ import nextstep.oauth2.registration.ClientRegistration; import nextstep.oauth2.registration.ClientRegistrationRepository; import nextstep.oauth2.userinfo.OAuth2UserService; -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.AuthenticationManager; import nextstep.security.authentication.DaoAuthenticationProvider; import nextstep.security.authentication.ProviderManager; -import nextstep.security.authorization.*; +import nextstep.security.authorization.SecuredMethodInterceptor; import nextstep.security.config.Customizer; import nextstep.security.config.DelegatingFilterProxy; import nextstep.security.config.FilterChainProxy; @@ -25,9 +22,7 @@ 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; @@ -63,13 +58,6 @@ public SecuredMethodInterceptor securedMethodInterceptor() { return new SecuredMethodInterceptor(); } - @Bean - public RoleHierarchy roleHierarchy() { - return RoleHierarchyImpl.with() - .role("ADMIN").implies("USER") - .build(); - } - @Bean public AuthenticationManager authenticationManager() { return new ProviderManager(List.of( @@ -80,6 +68,11 @@ public AuthenticationManager authenticationManager() { @Bean public SecurityFilterChain securityFilterChain2(HttpSecurity http) { return http + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/members").hasRole("ADMIN") + .requestMatchers("/members/me").authenticated() + .anyRequest().permitAll() + ) .csrf(c -> c.ignoringRequestMatchers("/login")) .formLogin(Customizer.withDefaults()) .httpBasic(Customizer.withDefaults()) @@ -87,28 +80,11 @@ public SecurityFilterChain securityFilterChain2(HttpSecurity http) { .build(); } -// @Bean -// public SecurityFilterChain securityFilterChain() { -// return new DefaultSecurityFilterChain( -// List.of( -// new SecurityContextHolderFilter(), -// new CsrfFilter(Set.of(new MvcRequestMatcher(HttpMethod.POST, "/login"))), -// 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); + public RoleHierarchy roleHierarchy() { + return RoleHierarchyImpl.with() + .role("ADMIN").implies("USER") + .build(); } @Bean diff --git a/src/main/java/nextstep/security/authorization/AuthenticatedAuthorizationManager.java b/src/main/java/nextstep/security/authorization/AuthenticatedAuthorizationManager.java index 12c2920..6b1350d 100644 --- a/src/main/java/nextstep/security/authorization/AuthenticatedAuthorizationManager.java +++ b/src/main/java/nextstep/security/authorization/AuthenticatedAuthorizationManager.java @@ -3,9 +3,13 @@ import nextstep.security.authentication.Authentication; public class AuthenticatedAuthorizationManager implements AuthorizationManager { + @Override public AuthorizationDecision check(Authentication authentication, T object) { - boolean granted = authentication.isAuthenticated(); - return new AuthorizationDecision(granted); + return new AuthorizationDecision(isGranted(authentication)); + } + + private boolean isGranted(Authentication authentication) { + return authentication != null && authentication.isAuthenticated(); } } diff --git a/src/main/java/nextstep/security/authorization/AuthorizationFilter.java b/src/main/java/nextstep/security/authorization/AuthorizationFilter.java index 1341005..cc32502 100644 --- a/src/main/java/nextstep/security/authorization/AuthorizationFilter.java +++ b/src/main/java/nextstep/security/authorization/AuthorizationFilter.java @@ -29,15 +29,27 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo try { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); AuthorizationDecision decision = this.authorizationManager.check(authentication, request); - if (decision != null && !decision.isGranted()) { - throw new AccessDeniedException("Access Denied"); + + if (authorizationSucceed(decision)) { + chain.doFilter(servletRequest, servletResponse); + return; + } + + // 인가에 실패했는데 인증에 실패해서 인가에 실패한 경우 + if (authentication == null || !authentication.isAuthenticated()) { + throw new AuthenticationException(); } - chain.doFilter(request, response); + // 인가에 실패한 경우 + throw new AccessDeniedException(); } catch (AccessDeniedException e) { response.sendError(HttpServletResponse.SC_FORBIDDEN); } catch (AuthenticationException e) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED); } } + + private boolean authorizationSucceed(AuthorizationDecision decision) { + return decision != null && decision.isGranted(); + } } diff --git a/src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java b/src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java index 16ef801..0ae2f87 100644 --- a/src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java +++ b/src/main/java/nextstep/security/authorization/RequestMatcherDelegatingAuthorizationManager.java @@ -20,7 +20,6 @@ public AuthorizationDecision check(Authentication authentication, HttpServletReq RequestMatcher matcher = mapping.getRequestMatcher(); if (matcher.matches(request)) { AuthorizationManager manager = mapping.getEntry(); - return manager.check(authentication, request); } } diff --git a/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java b/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java index 2df1943..3229317 100644 --- a/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java +++ b/src/main/java/nextstep/security/config/annotation/web/builders/HttpSecurity.java @@ -1,12 +1,17 @@ package nextstep.security.config.annotation.web.builders; import jakarta.servlet.Filter; +import nextstep.security.access.hierarchicalroles.RoleHierarchy; import nextstep.security.authentication.AuthenticationManager; +import nextstep.security.authorization.AuthorizationFilter; import nextstep.security.config.Customizer; import nextstep.security.config.DefaultSecurityFilterChain; import nextstep.security.config.SecurityFilterChain; import nextstep.security.config.annotation.web.SecurityConfigurer; import nextstep.security.config.annotation.web.configurers.*; +import nextstep.security.context.SecurityContextHolderFilter; +import org.springframework.context.ApplicationContext; +import org.springframework.util.Assert; import java.util.*; @@ -25,14 +30,14 @@ public C getSharedObject(Class sharedType) { return (C) this.sharedObjects.get(sharedType); } - private void setSharedObject(Class sharedType, C object) { + public void setSharedObject(Class sharedType, C object) { this.sharedObjects.put(sharedType, object); } public SecurityFilterChain build() { init(); configure(); - return new DefaultSecurityFilterChain(filters); + return new DefaultSecurityFilterChain(orderFilters()); } private void init() { @@ -67,7 +72,10 @@ public HttpSecurity oauth2Login(Customizer oauth2LoginCus return this; } - public HttpSecurity authorizeHttpRequests() { + public HttpSecurity authorizeHttpRequests(Customizer requestsCustomizer) { + RoleHierarchy roleHierarchy = getSharedObject(ApplicationContext.class).getBean(RoleHierarchy.class); + Assert.notNull(roleHierarchy, "roleHierarchy must not be null"); + requestsCustomizer.customize(getOrApply(new AuthorizeHttpRequestsConfigurer(roleHierarchy))); return this; } @@ -91,4 +99,28 @@ private C getOrApply(C configurer) { this.configurers.put(clazz, configurer); return configurer; } + + private List orderFilters() { + List orderedFilters = new ArrayList<>(this.filters); + + // SecurityContext는 인증 전부터 관리되므로, SecurityContextHolder 필터를 인증 필터보다 앞에 위치. + orderedFilters.stream() + .filter(filter -> filter instanceof SecurityContextHolderFilter) + .findFirst() + .ifPresent(filter -> { + orderedFilters.remove(filter); + orderedFilters.add(0, filter); + }); + + // 인증 후에 인가를 실행해야하므로, 인가 필터를 가장 마지막에 위치 + orderedFilters.stream() + .filter(filter -> filter instanceof AuthorizationFilter) + .findFirst() + .ifPresent(filter -> { + orderedFilters.remove(filter); + orderedFilters.add(filter); + }); + + return orderedFilters; + } } diff --git a/src/main/java/nextstep/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java b/src/main/java/nextstep/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java new file mode 100644 index 0000000..7df5fef --- /dev/null +++ b/src/main/java/nextstep/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java @@ -0,0 +1,76 @@ +package nextstep.security.config.annotation.web.configurers; + +import nextstep.security.access.AnyRequestMatcher; +import nextstep.security.access.MvcRequestMatcher; +import nextstep.security.access.RequestMatcher; +import nextstep.security.access.RequestMatcherEntry; +import nextstep.security.access.hierarchicalroles.RoleHierarchy; +import nextstep.security.authorization.*; +import nextstep.security.config.annotation.web.SecurityConfigurer; +import nextstep.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class AuthorizeHttpRequestsConfigurer implements SecurityConfigurer { + + List> mappings = new ArrayList<>(); + private List currentMatchers; + private final RoleHierarchy roleHierarchy; + + public AuthorizeHttpRequestsConfigurer(RoleHierarchy roleHierarchy) { + Assert.notNull(roleHierarchy, "roleHierarchy cannot be null"); + this.roleHierarchy = roleHierarchy; + } + + @Override + public void init(HttpSecurity http) { + // AuthorizeHttpRequestsConfigurer.hasRole() 보다 HttpSecurity.build()가 먼저 수행되므로, + // init()에서 roleHierarchy 초기화를 진행 시 .hasRole() 시점에 roleHierarchy가 null이 되는 문제 발생 -> 생성자에서 roleHierarchy 주입. + } + + @Override + public void configure(HttpSecurity http) { + RequestMatcherDelegatingAuthorizationManager manager = new RequestMatcherDelegatingAuthorizationManager(mappings); + AuthorizationFilter filter = new AuthorizationFilter(manager); + http.addFilter(filter); + } + + public AuthorizeHttpRequestsConfigurer requestMatchers(String... patterns) { + this.currentMatchers = Arrays.stream(patterns) + .map(pattern -> new MvcRequestMatcher(null, pattern)) + .toList(); + + return this; + } + + public AuthorizeHttpRequestsConfigurer anyRequest() { + this.currentMatchers = List.of(AnyRequestMatcher.INSTANCE); + return this; + } + + public AuthorizeHttpRequestsConfigurer hasRole(String role) { + addMappings(new AuthorityAuthorizationManager<>(this.roleHierarchy, role)); + return this; + } + + public AuthorizeHttpRequestsConfigurer permitAll() { + addMappings(new PermitAllAuthorizationManager<>()); + return this; + } + + public AuthorizeHttpRequestsConfigurer authenticated() { + addMappings(new AuthenticatedAuthorizationManager<>()); + return this; + } + + private void addMappings(AuthorizationManager authorizationManager) { + for (RequestMatcher matcher : this.currentMatchers) { + this.mappings.add( + new RequestMatcherEntry<>(matcher, authorizationManager) + ); + } + } +} From 87a58c64214f3b3da40afcdfa1556e25984f18f7 Mon Sep 17 00:00:00 2001 From: haero77 Date: Tue, 18 Mar 2025 22:17:53 +0900 Subject: [PATCH 10/14] docs: update requirements, questions --- README.md | 33 ++++++++++++++++++++++++++------- docs/Questions.md | 13 +++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 955f297..7c4510d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## 실습 - 취약점 대응(CsrfFilter) -> CsrfFilter를 이용한 CSRF 공격 대응 +> CsrfFilter를 이용한 CSRF 공격 대응 - [x] CsrfToken 구현 - [x] CsrfTokenRepository 구현 - HttpSessionCsrfTokenRepository @@ -25,22 +25,41 @@ - [x] @EnableWebSecurity, HttpSecurityConfiguration를 이용한 HttpSecurity 빈 등록 - [x] csrf 필터를 configurer를 이용하여 설정 - ## 2단계 - 인증 관련 리팩토링 - [x] `.formLogin()` 메서드를 사용하여 폼 로그인 기능을 설정하고, U`sernamePasswordAuthenticationFilter`를 자동으로 추가한다. - [x] `.httpBasic()` 메서드를 사용해 HTTP Basic 인증을 설정하고, `BasicAuthenticationFilter`를 자동으로 추가한다. -- [x] `.securityContext()` 메서드를 사용하여 `SecurityContextHolderFilter` 자동으로 추가 -- [x] oauth2 리팩토링 +- [x] `.securityContext()` 메서드를 사용하여 `SecurityContextHolderFilter` 자동으로 추가 +- [x] oauth2 리팩토링 - [x] OAuth2AuthorizationRequestRedirectFilter 등록, OAuth2LoginAuthenticationFilter 등록 ## 3단계 - 인가 관련 리팩토링 -## 4단계 - Auto Configuration 적용 +> 예시 코드 + +```java + +@Bean +public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/public").permitAll() // /public 경로는 모두 허용 + .anyRequest().authenticated()) // 그 외의 경로는 인증 필요 + .formLogin(Customizer.withDefaults()) // 폼 로그인 + .httpBasic(Customizer.withDefaults()); // HTTP Basic 인증 ---- + return http.build(); +} +``` + +- [x] `authorizeHttpRequests()` 메서드 구현 + - [x] `AuthorizeHttpRequestsConfigurer`를 이용한 설정 + - [x] 특정 경로에 대해 인증 없이 접근 가능하도록 설정하고, 나머지 요청에 대해서는 인증이 필요하도록 설정한다. + - [x] 특정 경로에 대해서 권한에 따라 접근 가능하게 할지/말지를 설정한다 + +## 4단계 - Auto Configuration 적용 -# 플로우차트를 활용한 깊은 이해 +# 플로우차트를 활용한 이해 ## CSRF 공격 대응 diff --git a/docs/Questions.md b/docs/Questions.md index da14786..809daea 100644 --- a/docs/Questions.md +++ b/docs/Questions.md @@ -2,3 +2,16 @@ - [ ] SSR에서는 HttpSessionCsrfTokenRepository, CSR에서는 CookieCsrfTokenRepository를 사용해야할 것 같은데, 그 이유를 얘기하고 근거가 적절한지 질문. +### 인증 필터간 순서 + +- 현재 구현한 HttpSecurity의 경우, 어떤 인증 메서드(`csrf()`, `oauth2Login()` 등)를 호출하냐에따라 SecurityFilterChain의 필터 순서가 결정된다. +- 이 방식은 `authorizeHttpRequests()` 가 다른 인증 메서드보다 먼저 호출될 경우, `AuthorizationFilter`가 다른 인증 필터보다 먼저 수행하기 때문에 인증 전에 인가가 수행되버리는 문제가 있다. + - `authorizeHttpRequests()` 가 먼저 호출되어도 필터 체인의 가장 마지막에 순서를 위치시키도록 하는 로직이 필요함. (우선 강제로 순서를 맞춰주는 방식으로 구현) + +### authorizationFlter에서는 403을 리턴하는데 인증되지 않은 경우는 401을 리턴해야한다. + +- 문제를 해결하기 위해 exceptionTranslationFilter를 지금 구현하는 것은 오버 엔지니어링. + +### RoleHierarchy 가 빈으로 등록되었는데, 찾지 못하는 문제? + +- httpSecurity.build 보다 hasRole이 먼저 호출되고, 그래서 this.roleHierarchy 가 null이 되는 문제 발생 From 6ffb3e25f4b6c64cbc14387fc8b3f8b9d56bfe94 Mon Sep 17 00:00:00 2001 From: haero77 Date: Wed, 19 Mar 2025 21:20:44 +0900 Subject: [PATCH 11/14] =?UTF-8?q?(step4)=20Auto=20Configuration=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 +++++++++ img.png | Bin 0 -> 26775 bytes img_1.png | Bin 0 -> 91637 bytes .../java/nextstep/app/SecurityConfig.java | 2 + ...EnableSpringBootSecurityConfiguration.java | 15 ++++++++ .../SpringBootWebSecurityConfiguration.java | 35 ++++++++++++++++++ 6 files changed, 69 insertions(+) create mode 100644 img.png create mode 100644 img_1.png create mode 100644 src/main/java/nextstep/autoconfigure/EnableSpringBootSecurityConfiguration.java create mode 100644 src/main/java/nextstep/autoconfigure/SpringBootWebSecurityConfiguration.java diff --git a/README.md b/README.md index 7c4510d..5ac05e6 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,23 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti ## 4단계 - Auto Configuration 적용 +> Auto Configuration을 통한 기본 SecurityFilterChain 설정 +> +> 스프링 부트의 자동 설정을 통해 기본 SecurityFilterChain이 설정되도록 하고, 필요시 사용자가 새로운 SecurityFilterChain을 추가할 수 있도록 한다. 사용자가 새로운 SecurityFilterChain을 정의할 경우, 기본 보안 설정이 비활성화된다. +> 주요 클래스 +> - @ConditionalOnDefaultWebSecurity +> - @DefaultWebSecurityCondition +> - SpringBootWebSecurityConfiguration +> - EnableWebSecurity +> + +![img.png](img.png) +![img_1.png](img_1.png) + +- [x] AutoConfiguration을 이용하여 + - [x] 사용자가 SecurityFilterChain을 정의한 경우 기본 SecurityFilterChain이 설정된다. + - [x] 사용자가 SecurityFilterChain을 정의하지 않은 경우 기본 SecurityFilterChain이 설정되지 않는다. + # 플로우차트를 활용한 이해 ## CSRF 공격 대응 diff --git a/img.png b/img.png new file mode 100644 index 0000000000000000000000000000000000000000..bfd9293613298871a662d515e188cd0ebad4d119 GIT binary patch literal 26775 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 +^1)I)|?Vd|6fq$cH?b01!?Gz4{le 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:vgnX_hY)ripa`2 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 BiTeNm literal 0 HcmV?d00001 diff --git a/src/main/java/nextstep/app/SecurityConfig.java b/src/main/java/nextstep/app/SecurityConfig.java index ee029b3..5b81715 100644 --- a/src/main/java/nextstep/app/SecurityConfig.java +++ b/src/main/java/nextstep/app/SecurityConfig.java @@ -1,5 +1,6 @@ package nextstep.app; +import nextstep.autoconfigure.EnableSpringBootSecurityConfiguration; import nextstep.oauth2.OAuth2ClientProperties; import nextstep.oauth2.authentication.OAuth2LoginAuthenticationProvider; import nextstep.oauth2.registration.ClientRegistration; @@ -31,6 +32,7 @@ @EnableAspectJAutoProxy @EnableConfigurationProperties(OAuth2ClientProperties.class) @EnableWebSecurity +@EnableSpringBootSecurityConfiguration public class SecurityConfig { private final UserDetailsService userDetailsService; diff --git a/src/main/java/nextstep/autoconfigure/EnableSpringBootSecurityConfiguration.java b/src/main/java/nextstep/autoconfigure/EnableSpringBootSecurityConfiguration.java new file mode 100644 index 0000000..cfe0189 --- /dev/null +++ b/src/main/java/nextstep/autoconfigure/EnableSpringBootSecurityConfiguration.java @@ -0,0 +1,15 @@ +package nextstep.autoconfigure; + +import org.springframework.context.annotation.Import; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Import(SpringBootWebSecurityConfiguration.class) +public @interface EnableSpringBootSecurityConfiguration { + +} diff --git a/src/main/java/nextstep/autoconfigure/SpringBootWebSecurityConfiguration.java b/src/main/java/nextstep/autoconfigure/SpringBootWebSecurityConfiguration.java new file mode 100644 index 0000000..7722e41 --- /dev/null +++ b/src/main/java/nextstep/autoconfigure/SpringBootWebSecurityConfiguration.java @@ -0,0 +1,35 @@ +package nextstep.autoconfigure; + +import nextstep.security.config.Customizer; +import nextstep.security.config.SecurityFilterChain; +import nextstep.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +// 클래스에 선언된 빈 생성 메서드 간 빈 의존성이 없으므로 굳이 프록시 객체를 생성하지 않기 위해 proxyBeanMethods = false 옵션 사용 +@Configuration(proxyBeanMethods = false) +public class SpringBootWebSecurityConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(SecurityFilterChain.class) + static class SecurityFilterChainConfiguration { + + @Bean + @Order(SecurityProperties.BASIC_AUTH_ORDER) + SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) { + return http + .authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()) + .formLogin(Customizer.withDefaults()) + .httpBasic(Customizer.withDefaults()) + .build(); + } + } + + @Configuration(proxyBeanMethods = false) + static class WebSecurityEnablerConfiguration { + + } +} From 61da879ef88fd1e85d52ff3e9b1d93a8aaf303f8 Mon Sep 17 00:00:00 2001 From: haero77 Date: Wed, 26 Mar 2025 21:27:03 +0900 Subject: [PATCH 12/14] =?UTF-8?q?(feedback):=20Configurer=EC=9D=98=20init,?= =?UTF-8?q?=20configure=20=EA=B5=AC=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - init: 초기 설정, 준비 작업. 필터 설정에 필요한 의존성 주입 - configure: init 단계에서 준비된 객체를 이용하여 필터를 설정하고, 필터 추가 --- .../annotation/web/configurers/CsrfConfigurer.java | 10 ++++++---- .../web/configurers/FormLoginConfigurer.java | 7 ++++--- .../web/configurers/HttpBasicConfigurer.java | 6 ++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/java/nextstep/security/config/annotation/web/configurers/CsrfConfigurer.java b/src/main/java/nextstep/security/config/annotation/web/configurers/CsrfConfigurer.java index 3ec6abc..9eaa26c 100644 --- a/src/main/java/nextstep/security/config/annotation/web/configurers/CsrfConfigurer.java +++ b/src/main/java/nextstep/security/config/annotation/web/configurers/CsrfConfigurer.java @@ -14,11 +14,13 @@ public class CsrfConfigurer implements SecurityConfigurer { - private RequestMatcher csrfRequiredRequestMathcer = CsrfFilter.DEFAULT_CSRF_MATCHER; - private List ignoredCsrfProtectionMatchers = new ArrayList<>(); + private RequestMatcher csrfRequiredRequestMatcher; + private List ignoredCsrfProtectionMatchers; @Override public void init(HttpSecurity http) { + this.csrfRequiredRequestMatcher = CsrfFilter.DEFAULT_CSRF_MATCHER; + this.ignoredCsrfProtectionMatchers = new ArrayList<>(); } @Override @@ -33,11 +35,11 @@ public void configure(HttpSecurity http) { private RequestMatcher getCsrfProtectionRequiredRequestMatcher() { if (this.ignoredCsrfProtectionMatchers.isEmpty()) { - return this.csrfRequiredRequestMathcer; + return this.csrfRequiredRequestMatcher; } return new AndRequestMatcher( - this.csrfRequiredRequestMathcer, + this.csrfRequiredRequestMatcher, new NegatedRequestMatcher(new OrRequestMatcher(this.ignoredCsrfProtectionMatchers)) ); } diff --git a/src/main/java/nextstep/security/config/annotation/web/configurers/FormLoginConfigurer.java b/src/main/java/nextstep/security/config/annotation/web/configurers/FormLoginConfigurer.java index cea1dcb..dd48df6 100644 --- a/src/main/java/nextstep/security/config/annotation/web/configurers/FormLoginConfigurer.java +++ b/src/main/java/nextstep/security/config/annotation/web/configurers/FormLoginConfigurer.java @@ -7,14 +7,15 @@ public class FormLoginConfigurer implements SecurityConfigurer { + private AuthenticationManager authenticationManager; + @Override public void init(HttpSecurity http) { + this.authenticationManager = http.getSharedObject(AuthenticationManager.class); } @Override public void configure(HttpSecurity http) { - AuthenticationManager manager = http.getSharedObject(AuthenticationManager.class); - UsernamePasswordAuthenticationFilter filter = new UsernamePasswordAuthenticationFilter(manager); - http.addFilter(filter); + http.addFilter(new UsernamePasswordAuthenticationFilter(this.authenticationManager)); } } diff --git a/src/main/java/nextstep/security/config/annotation/web/configurers/HttpBasicConfigurer.java b/src/main/java/nextstep/security/config/annotation/web/configurers/HttpBasicConfigurer.java index de140ca..c881ab0 100644 --- a/src/main/java/nextstep/security/config/annotation/web/configurers/HttpBasicConfigurer.java +++ b/src/main/java/nextstep/security/config/annotation/web/configurers/HttpBasicConfigurer.java @@ -7,14 +7,16 @@ public class HttpBasicConfigurer implements SecurityConfigurer { + private AuthenticationManager authenticationManager; + @Override public void init(HttpSecurity http) { + this.authenticationManager = http.getSharedObject(AuthenticationManager.class); } @Override public void configure(HttpSecurity http) { - AuthenticationManager manager = http.getSharedObject(AuthenticationManager.class); - BasicAuthenticationFilter filter = new BasicAuthenticationFilter(manager); + BasicAuthenticationFilter filter = new BasicAuthenticationFilter(this.authenticationManager); http.addFilter(filter); } } From 1dfcc3fd18a5d33af6e773aac7d05dec8b12c658 Mon Sep 17 00:00:00 2001 From: haero77 Date: Wed, 26 Mar 2025 21:49:48 +0900 Subject: [PATCH 13/14] =?UTF-8?q?(feedback):=20HttpSecurity=EB=A5=BC=20?= =?UTF-8?q?=EB=B9=88=EC=9C=BC=EB=A1=9C=20=EB=93=B1=EB=A1=9D=ED=95=A0=20?= =?UTF-8?q?=EB=95=8C=20=EC=83=9D=EA=B8=B0=EB=8A=94=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=EC=A0=90=EC=9D=84=20scope=3Dprototype=EC=9D=84=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HttpSecurity가 싱글톤 인스턴스로 유지될 경우 앱 개발자가 SecurityFilterChain을 새로 만드는 순간 이전에 설정한 HttpSecurity 설정이 유지되는 문제가 있음. - 빈 스코프를 프로토타입으로 변경하여, 빈을 주입할 때마다 httpSecurity를 호출하게 만들어 ApplicationContext 주입은 유지. - 최종적으로 여러 SecurityFilterChain 빈에서 각각 독립적인 HttpSecurity 설정을 할 수 있게된다. --- .../web/configuration/HttpSecurityConfiguration.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index 92203c1..bd4e34d 100644 --- a/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/src/main/java/nextstep/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -3,9 +3,11 @@ import nextstep.security.authentication.AuthenticationManager; import nextstep.security.config.Customizer; import nextstep.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; 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.Map; @@ -13,6 +15,7 @@ public class HttpSecurityConfiguration { @Bean + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) HttpSecurity httpSecurity( ApplicationContext applicationContext, AuthenticationManager authenticationManager From 1f906aa16eefe4f988810e38bc6ab91d9f715b97 Mon Sep 17 00:00:00 2001 From: haero77 Date: Sat, 29 Mar 2025 15:29:54 +0900 Subject: [PATCH 14/14] =?UTF-8?q?(feedback)=20=ED=94=BC=EB=93=9C=EB=B0=B1?= =?UTF-8?q?=20=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 5ac05e6..867079f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,19 @@ > 미션 4: 취약점 대응 & 리팩토링 + +* [요구사항](#요구사항) + * [실습 - 취약점 대응(CsrfFilter)](#실습---취약점-대응csrffilter) + * [1단계 - SecurityFilterChain 리팩토링](#1단계---securityfilterchain-리팩토링) + * [2단계 - 인증 관련 리팩토링](#2단계---인증-관련-리팩토링) + * [3단계 - 인가 관련 리팩토링](#3단계---인가-관련-리팩토링) + * [4단계 - Auto Configuration 적용](#4단계---auto-configuration-적용) + * [미션을 진행하며 생긴 고민에 대한 흔적](#미션을-진행하며-생긴-고민에-대한-흔적) +* [플로우차트를 활용한 이해](#플로우차트를-활용한-이해) + * [CSRF 공격 대응](#csrf-공격-대응) + * [CSRF 토큰 적용 전](#csrf-토큰-적용-전) + * [CSRF 토큰 적용 후](#csrf-토큰-적용-후) + + # 요구사항 ## 실습 - 취약점 대응(CsrfFilter) @@ -76,6 +90,19 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti - [x] 사용자가 SecurityFilterChain을 정의한 경우 기본 SecurityFilterChain이 설정된다. - [x] 사용자가 SecurityFilterChain을 정의하지 않은 경우 기본 SecurityFilterChain이 설정되지 않는다. +### 미션을 진행하며 생긴 고민에 대한 흔적 & 피드백 내용 + +- [x] [CSRF 구현 방식에 대한 고민 (세션, 쿠키 둘 중 어디에 토큰을 저장할지?)](https://github.com/next-step/spring-security-refactoring/pull/9#discussion_r2001099639) +- [x] [HttpSecurity에서 필터의 순서를 어떻게 제어할지에 대한 고민](https://github.com/next-step/spring-security-refactoring/pull/9#discussion_r2001125969) + - 👉시큐리티에서는 httpSecurity.addFilter에서 OrderedFilter 생성. 또는 httpSecurity.addFilterAtOffsetOf에서 FilterOrderRegistration을 참조하여 필터 순서를 조정. +- [x] [`AuthorizeHttpRequestsConfigurer::hasRole` 호출 시 roleHierarchy에 대한 의존성을 어떻게 관리할지에 대한 고민](https://github.com/next-step/spring-security-refactoring/pull/9#discussion_r2001147784) +- [x] [AuthorizationFilter에서 인증이 안 되었을 때 401을 던지는 것이 자연스러운가에 대한 고민](https://github.com/next-step/spring-security-refactoring/pull/9#discussion_r2001166948) +- [x] [예외 상황에 따라서 어떻게 예외를 핸들링할지에 대한 고민](https://github.com/next-step/spring-security-refactoring/pull/9#discussion_r2005589776) +- [x] [HttpSecurity를 빈 등록 시 유연성이 떨어지는 문제 고민](https://github.com/next-step/spring-security-refactoring/pull/9#discussion_r2005603658) (구현 완) +- [x] [SecurityConfigurer의 `init`, `configure` 구분을 어떻게 하고 있는지?](https://github.com/next-step/spring-security-refactoring/pull/9#discussion_r2005616627) (구현 완) + + + # 플로우차트를 활용한 이해 ## CSRF 공격 대응