초기에 우리는 웹 애플리케이션 내의 필터와 서블릿에 대해 공부했었다.
Java 웹 애플리케이션에서 어떤 역할을 하는지 알아보았었다.
필터는 Java 웹 애플리케이션의 구성 요소 중 하나이다.
웹 애플리케이션 내의 모든 요청이나 응답을 가로채고 차단하는 데 사용될 수 있다.
실제 비즈니스 로직 전에 실행되는 사전 로직을 수행할 수 있다.
Spring Security 프레임워크는 웹 애플리케이션 내의 보안을 강제한다.
내부 흐름 중 필터는 인증 및 권한 부여 흐름에서 중요한 역할을 한다.
Spring Security 내장 필터들은 인증 및 권한 부여 프로세스에 관여한다.
프로젝트의 특정 상황이나 요구 사항에 따라 인증 및 권한 부여 흐름 중 일부를 수정해야 할 때 필요하다.
이러한 상황에서는 자체적인 커스텀 필터를 작성하여 프로세스를 조정할 수 있다.
Spring Security FilterChain(필터 체인)은 인증을 시도할 때 실행되는 필터들의 연쇄이다.
이 필터 체인은 다양한 내장 필터들로 구성되어 있으며, 연쇄적으로 실행된다.
유저 인증 중에 Spring Security에서 활용된 몇 가지 내장 필터를 살펴보았었다.
UsernamePasswordAuthenticationFilter, BasicAuthenticationFilter 그리고 DefaultLoginPageGenerationFilter 등을 정리했었다.
필터에 관한 중요한 정보 중 하나는 인증을 시도하려고 할 때 실행되는 필터의 수가 상당히 많다는 것이다. 자체적인 역할과 책임을 갖는 10개 이상의 Spring Security 필터가 실행된다.
이러한 필터가 실행되는 방식은 연쇄적이다. 필터 체인 내에서 모든 필터가 필터 1이 실행된 후 필터 2, 필터 3으로 하나씩 진행된다.
그래서 이 체인은 체인 내에서 마지막 필터에 도달할 때까지 다음 필터를 계속해서 동작시킨다.
내장된 필터는 Spring Seucirty를 어떻게 구성하거나 인증을 시도하는지에 따라 어떻게 변경되는지에 따라 다양하다
Spring Security 프레임워크에서 유저를 인증할 때 사용하는 내장 필터를 살펴보자.
필터를 확인하기 위해 웹 애플리케이션 설정을 변경해야 한다.
@EnableWebSecurity 추가
Spring Boot 애플리케이션의 주 클래스에 @EnableWebSecurity
을 추가한다.
이 어노테이션에는 (debug = true)로 설정하여 디버그를 활성화한다.
application.properties 수정
application.properties 파일에서 FilterChainProxy 클래스
의 디버그 로깅을 활성화
하는 설정을 추가한다.
FilterChainProxy 클래스
는 Spring Security 프레임워크 내부에서 내장 필터를 연결하는 로직에 사용된다.
FilterChainProxy
안에는 VirualFilterChain
이라는 내부 FilterChain 구현
한 클래스가 있다. 이는 들어오는 요청을 해당하는 내부 필터 목록을 통해 전달하는 역할을 한다.
VirualFilterChain
의 doFilter()
을 살펴보면 Spring Security 필터 체인 내에서 사용가능한 모든 필터를 반복하는 로직이 있다.
currentPosition
이 어디인지를 추적할 수 있다.
if(this.currentPosition == this.size)
: 만약 currentPosition
이 필터의 총 size와 같다면 다음 필터(=nextFilter.doFilter()
)는 호출되지 않는다.
대신에 this.originalChain.doFilter(request,response)
를 호출한다.
if문 조건에 해당하지 않다면 이 체인 내에 가능한 다음 필터(=nextFilter.doFilter()
)를 불러온다.
이렇게하면 Spring Security 내의 활성화된 모든 내부 필터를 하나씩 반복하면서 각각의 필터 내부의 로직이 실행된다.
이러한 변경 사항은 실제 운영 환경에서 사용해서는 안 된다.
보안 위협을 초래할 수 있으며, 민감한 정보가 로그에 기록될 수 있다.
백엔드 서버를 시작한 후 디버깅 모드로 실행한다.
콘솔에 'Security debugging is enabled' 메시지가 표시되면 설정이 성공적으로 적용된 것이다.
Spring Security 필터 체인을 확인하기 위해 중단점을 추가한다.
디버그 콘솔에서 내장 필터들을 확인하고, 각 필터의 역할과 책임을 이해한다.
(1) 현재 필터의 총 size
는 17이며 이는 Spring Security가 약 17개의 필터를 실행한다는 뜻이다.
currentPosition
이 0이므로 if문에 들어가지 않을 것이다.
(2) 추가 필터 목록을 열면 Spring Security 프레임워크에서 실행되는 모든 필터이다.
(3) 각 필터는 doFilter() 메소드
를 호출해서 하나씩 반복된다.
엔드 유저에 인증 중에 실행된 내부 Spring Security 필터들을 볼 수 있다. 따라서 인증 중에 Spring Security의 많은 내부 필터가 실행되고 있음을 확인할 수 있다.
자체적인 커스텀 필터를 정의하고 실행하는 방법에 대해 알아보자.
필터를 작성하기 위해 jakarta.servlet 패키지
내의 Filter 인터페이스
를 확장한다.
Filter 인터페이스
를 구현하는 클래스에서 doFilter 메소드
를 구현한다.
doFilter 메소드
는 ServletRequest
, ServletResponse
및 FilterChain
을 매개변수로 받아 필터링 로직을 실행한다.
이미 알려진 필터의 위치 전, 후, 해당 위치에 추가 가능하다.
addFilterBefore
, addFilterAfter
및 addFilterAt
메소드를 사용하여 필터를 주입한다.
jakarta.servlet 패키지
에 위치한 Filter 인터페이스
를 살펴봅니다.
이 인터페이스는 Java Enterprise Edition에서 사용할 수 있으며, doFilter
외에 init
및 destroy
메소드를 포함한다.
init
및 destroy
메소드는 필수적이지 않으며, 필요에 따라 오버라이드하여 비즈니스 로직을 구현할 수 있다.
doFilter
메소드는 커스텀 필터의 주된 로직을 담당한다.
Spring Security FilterChain에 커스텀 필터를 추가해보자.
addFilterBefore
메소드를 사용하여 여러 커스텀 필터 중 CorsFilter
또는 CsrfFilter
를 먼저 실행하고 BasicAuthenticationFilter
를 그 뒤에 실행한다.
BasicAuthenticationFilter 직전
에 실행될 커스텀 필터를 만들어 구성하자.
BasicAuthenticationFilter
내에서 자격 증명을 추출하기 전에 선행되는 로직을 커스텀 필터에 구현한다.
package com.eazybytes.springsecsection2.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
public class RequestValidationBeforeFilter implements Filter {
public static final String AUTHENTICATION_SCHEME_BASIC = "Basic";
private Charset credentialsCharset = StandardCharsets.UTF_8;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//HttpServletRequest으로 형변환
HttpServletRequest req = (HttpServletRequest) request;
//HttpServletResponse으로 형변환
HttpServletResponse res = (HttpServletResponse) response;
//Angular로 부터 받은 Header에서 `AUTHORIZATION`을 얻는다
String header = req.getHeader(AUTHORIZATION);
//AUTHORIZATION header를 확보하면
if (header != null) {
//공백 제거 후
header = header.trim();
if (StringUtils.startsWithIgnoreCase(header, AUTHENTICATION_SCHEME_BASIC)) {
//header 내의 여섯 번째 부분에서 문자열 추출 (`basic `은 제거하고 추출)
byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
byte[] decoded;
try {
//base64Token를 디코딩
decoded = Base64.getDecoder().decode(base64Token);
String token = new String(decoded, credentialsCharset);
//':' 를 중심으로 문자열 분리
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
//':' 를 중심으로 email만 추출
String email = token.substring(0, delim);
//email이 "test"문자열이라면
if (email.toLowerCase().contains("test")) {
//400번
res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
} catch (IllegalArgumentException e) {
throw new BadCredentialsException("Failed to decode basic authentication token");
}
}
}
//filterChain의 도움으로 다음 필터로 넘어감
chain.doFilter(request, response);
}
}
RequestValidationFilter
를 만들어 BasicAuthenticationFilter
앞에 구현한다.
이 필터는 유저 이름에 'test'를 포함한 경우 유효하지 않은 요청으로 처리한다.
프로젝트 내의 filter 패키지
에 RequestValidationBeforeFilter
를 생성하여 jakarta.servlet 패키지의 Filter 인터페이스
를 구현
한다.
doFilter
메소드를 오버라이드하여 필터링 로직을 구현한다.
HttpServletRequest
와 HttpServletResponse
로 형변환하여 요청과 응답을 처리한다.
Authorization header
에서 유저 이름을 추출하여 'test' 값을 확인하고, 해당 경우에는 400 에러
를 반환한다.
Angular에서
app.request.interceptor.ts
를 살펴보면Basic
이라는 값 뒤에 ' ' 공백이 하나 있고,Authorization header
를 보낸다.
그리고 이메일과 비밀번호의base64
값을 콜론으로 구분해서 보내려고 시도한다.
그렇지 않은 경우에는 FilterChain
을 호출하여 다음 필터를 실행한다.
ProjectSecurityConfig 클래스
에서 addFilterBefore
메소드를 호출하여 커스텀 필터를 구성한다.
package com.eazybytes.springsecsection2.config;
import com.eazybytes.springsecsection2.filter.CsrfCookieFilter;
import com.eazybytes.springsecsection2.filter.RequestValidationBeforeFilter;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.preauth.RequestAttributeAuthenticationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import javax.sql.DataSource;
import java.util.Collections;
@Configuration
public class ProjectSecurityConfig {
//람다(Lambda) DSL 스타일 사용을 권장
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName("_csrf");
http.securityContext((context) -> context.requireExplicitSave(false))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
//CorsConfigurationSource 인터페이스를 구현하는 익명 클래스 생성하여 getCorsConfiguration() 메소드 재정의
.cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
//`etCorsConfiguration() 메소드에서 CorsConfiguration 객체를 생성하고 필요한 설정들을 추가
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration config = new CorsConfiguration();
//허용할 출처(도메인)를 설정
config.setAllowedOrigins(Collections.singletonList("http://localhost:4200"));
//허용할 HTTP 메소드를 설정
config.setAllowedMethods(Collections.singletonList("*"));
//인증 정보 허용 여부를 설정
config.setAllowCredentials(true);
//허용할 헤더를 설정
config.setAllowedHeaders(Collections.singletonList("*"));
//CORS 설정 캐시로 사용할 시간을 설정
config.setMaxAge(3600L);
return config;
}
})).csrf((csrf) -> csrf.csrfTokenRequestHandler(requestHandler).ignoringRequestMatchers("/register","/contact")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
.addFilterBefore(new RequestValidationBeforeFilter(),BasicAuthenticationFilter.class)
.authorizeHttpRequests((requests)->requests
.requestMatchers("/myAccount").hasRole("USER")
.requestMatchers("/myBalance").hasAnyRole("USER","ADMIN")
.requestMatchers("/myLoans").hasRole("USER")
.requestMatchers("/myCards").hasRole("USER")
.requestMatchers("/user").authenticated()
.requestMatchers("/notices","/contact","/register").permitAll())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
만들어준 커스텀 필터를 적용시키기 위해 Spring Security FilterChain 내에 addFilterBefore(new RequestValidationBeforeFilter(),BasicAuthenticationFilter.class)
을 추가하여 BasicAuthenticationFilter
가 실행하기 전에 RequestValidationBeforeFilter
가 실행되도록 설정해준다.
로그인 플로우를 테스트하여 커스텀 필터가 올바르게 동작하는지 확인한다.
커스텀 필터 RequestValidationFilter
가 BasicAuthenticationFilter
직전에 정상적으로 실행되는 것을 확인할 수 있다.
이메일에 'test' 값을 포함한 경우에는 400 에러
가 발생함을 확인한다.
커스텀 필터를 추가할 때에는 실행 시간이 오래 걸리는 로직을 사용하지 않도록 주의해야 한다.
필수적인 로직만을 포함하여 비즈니스 요구사항을 충족시키자
BasicAuthenticationFilter
다음에 Custom AuthenticationFilter
를 주입하여 인증 후 비즈니스 로직을 실행해보자.
인증이 마무리 되면 몇 가지 비즈니스 로직을 실행해야 하는 조건이 있다고 생각해보자.
이런 상황에서 또한 BasicAuthenticationFilter
를 선택할 수 있다.
만약 우리가 BasicAuthenticationFilter
바로 다음에 우리의 Custom AuthenticationFilter
을 실행한다고 하면 조건에 부합할 것이다.
Custom AuthenticationFilter
에 작성할 것은 커스텀 필터 내에 로거를 추가하는 것으로 어떠한 유저 인증이 성공적이고 그가 이러한 권한들을 가졌다를 보여주는 것이다.
인증이 성공하면 엔드 유저에게 로그, 감사 혹은 이메일을 보내는 상황이라고 가정해보자.
addFilterAfter()
메소드를 사용하여 Spring Security FilterChain 내에 커스텀 필터를 추가한다.
첫 번째 매개변수는 커스텀 필터의 객체이고, 두 번째 매개변수는 주입될 필터의 클래스 이름이다.
AuthoritiesLoggingAfterFilter 클래스
를 생성하여 Filter 인터페이스
를 구현한다.
doFilter()
메소드를 구현하여 인증된 유저의 세부 정보를 로깅하는 로직을 작성한다.
SecurityContextHolder를 사용하여 현재 인증된 유저의 정보를 가져온다.
유저가 인증되었을 경우 해당 유저의 이름과 권한을 로그한다.
package com.eazybytes.springsecsection2.filter;
import jakarta.servlet.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.io.IOException;
import java.util.logging.Logger;
public class AuthoritiesLoggingAfterFilter implements Filter {
private final Logger LOG =
Logger.getLogger(AuthoritiesLoggingAfterFilter.class.getName());
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//현재 인증된 유저의 세부정보를 Authentication(인증) 객체의 형태로 제공
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//authentication이 null이 아니라면 (=인증에 성공했다면)
if (null != authentication) {
//인증된 유저 이름과 유저의 권한을 로깅
LOG.info("User " + authentication.getName() + " is successfully authenticated and "
+ "has the authorities " + authentication.getAuthorities().toString());
}
//체인 내의 다음 필터를 호출
chain.doFilter(request, response);
}
}
AuthoritiesLoggingAfterFilter
만들어 BasicAuthenticationFilter
뒤에 구현한다.
이 필터의 목적은 로그인하려는 유저 인증이 성공적이라면 그의 이름 및 권한들을 보여주는 것이다.
프로젝트 내의 filter 패키지에 AuthoritiesLoggingAfterFilter
생성하여 jakarta.servlet 패키지의 Filter 인터페이스를 구현한다.
doFilter
메소드를 오버라이드하여 필터링 로직을 구현한다.
현재 인증된 유저의 세부 정보를 Authentication
(인증) 객체로 받아, null이 아니라면 인증된 유저이름과 권한을 로깅한다.
package com.eazybytes.springsecsection2.config;
import com.eazybytes.springsecsection2.filter.AuthoritiesLoggingAfterFilter;
import com.eazybytes.springsecsection2.filter.CsrfCookieFilter;
import com.eazybytes.springsecsection2.filter.RequestValidationBeforeFilter;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.preauth.RequestAttributeAuthenticationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import javax.sql.DataSource;
import java.util.Collections;
@Configuration
public class ProjectSecurityConfig {
//람다(Lambda) DSL 스타일 사용을 권장
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName("_csrf");
http.securityContext((context) -> context.requireExplicitSave(false))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
//CorsConfigurationSource 인터페이스를 구현하는 익명 클래스 생성하여 getCorsConfiguration() 메소드 재정의
.cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
//`etCorsConfiguration() 메소드에서 CorsConfiguration 객체를 생성하고 필요한 설정들을 추가
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration config = new CorsConfiguration();
//허용할 출처(도메인)를 설정
config.setAllowedOrigins(Collections.singletonList("http://localhost:4200"));
//허용할 HTTP 메소드를 설정
config.setAllowedMethods(Collections.singletonList("*"));
//인증 정보 허용 여부를 설정
config.setAllowCredentials(true);
//허용할 헤더를 설정
config.setAllowedHeaders(Collections.singletonList("*"));
//CORS 설정 캐시로 사용할 시간을 설정
config.setMaxAge(3600L);
return config;
}
})).csrf((csrf) -> csrf.csrfTokenRequestHandler(requestHandler).ignoringRequestMatchers("/register","/contact")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
.addFilterBefore(new RequestValidationBeforeFilter(),BasicAuthenticationFilter.class)
.addFilterAfter(new AuthoritiesLoggingAfterFilter(),BasicAuthenticationFilter.class)
.authorizeHttpRequests((requests)->requests
.requestMatchers("/myAccount").hasRole("USER")
.requestMatchers("/myBalance").hasAnyRole("USER","ADMIN")
.requestMatchers("/myLoans").hasRole("USER")
.requestMatchers("/myCards").hasRole("USER")
.requestMatchers("/user").authenticated()
.requestMatchers("/notices","/contact","/register").permitAll())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
ProjectSecurityConfig 클래스
에서 addFilterAfter()
메소드를 사용하여 커스텀 필터를 Spring Security FilterChain에 주입한다.
AuthoritiesLoggingAfterFilter
를 BasicAuthenticationFilter
다음에 주입하여 실행 순서를 정한다.
로그인 시 AuthoritiesLoggingAfterFilter
가 실행되며 인증된 유저의 세부 정보를 로그한다.
인증 객체 내의 누구의 인증이 성공적인지 엔드 유저의 세부정보를 볼 수 있다.
BasicAuthenticationFilter
다음에 커스텀 필터 AuthoritiesLoggingAfterFilter
가 올바르게 실행되었음을 확인한다.
Spring Security 내부 필터들 중에 하나의 Filter 위치와 정확히 같은 위치에서 커스텀 필터를 구성할 수 있다.
내부 필터는 체인 내부에서 실행될 예정이지만 이것은 매우 까다롭다.
Spring Security가 내부적으로 필터를 무작위로 실행하는 점에 대한 주의 해야 한다.
언제나 비즈니스 로직에 부작용이 없도록 로직을 작성해야 한다.
이 메서드의 목적을 데모로 보여주기 위해 LoggingFilter
를 만들어보자. 이 필터 내에서는 인증이 진행 중이라는 새로운 문장을 로그에 남길 것이다.
실제 애플리케이션에는 addFilterAt()
을 사용하는 시나리오가 많지 않을 것이다. 대부분은 addFilterBefore()
혹은 addFilterAfter()
를 사용한다.
addFilterAt()
를 사용되는 경우를 생각해보면 사용자에게 인증이 진행 중임을 알리기 위해 이메일을 보내거나 인증이 성공적이라고 알리기 위해 이메일을 보내거나 내부 응용프로그램에게 알림을 보내는 등의 요구사항이 있을 수 있다.
package com.eazybytes.springsecsection2.filter;
import jakarta.servlet.*;
import java.io.IOException;
import java.util.logging.Logger;
public class AuthoritiesLoggingAtFilter implements Filter {
//로거 객체 생성
private final Logger LOG =
Logger.getLogger(AuthoritiesLoggingAtFilter.class.getName());
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//동일한 LOG 객체를 사용하여 인증 검증이 진행 중이라는 문장을 정보로 기록
LOG.info("Authentication Validation is in progress");
//체인 내의 다음 필터를 호출
chain.doFilter(request, response);
}
}
인증 진행 중임을 알리는 로그를 남기는 AuthoritiesLoggingAtFilter
클래스를 생성한다.
Java의 Filter 인터페이스 구현하여 doFilter
메소드 재정의
로그를 남기기 위해 Java 라이브러리의 로거(logger)를 사용한다.
인증 진행 중임을 알리는 문장을 로그에 남기고 FilterChain
내의 다음 필터를 호출한다.
package com.eazybytes.springsecsection2.config;
import com.eazybytes.springsecsection2.filter.AuthoritiesLoggingAfterFilter;
import com.eazybytes.springsecsection2.filter.AuthoritiesLoggingAtFilter;
import com.eazybytes.springsecsection2.filter.CsrfCookieFilter;
import com.eazybytes.springsecsection2.filter.RequestValidationBeforeFilter;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.preauth.RequestAttributeAuthenticationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import javax.sql.DataSource;
import java.util.Collections;
@Configuration
public class ProjectSecurityConfig {
//람다(Lambda) DSL 스타일 사용을 권장
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName("_csrf");
http.securityContext((context) -> context.requireExplicitSave(false))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
//CorsConfigurationSource 인터페이스를 구현하는 익명 클래스 생성하여 getCorsConfiguration() 메소드 재정의
.cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
//`etCorsConfiguration() 메소드에서 CorsConfiguration 객체를 생성하고 필요한 설정들을 추가
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration config = new CorsConfiguration();
//허용할 출처(도메인)를 설정
config.setAllowedOrigins(Collections.singletonList("http://localhost:4200"));
//허용할 HTTP 메소드를 설정
config.setAllowedMethods(Collections.singletonList("*"));
//인증 정보 허용 여부를 설정
config.setAllowCredentials(true);
//허용할 헤더를 설정
config.setAllowedHeaders(Collections.singletonList("*"));
//CORS 설정 캐시로 사용할 시간을 설정
config.setMaxAge(3600L);
return config;
}
})).csrf((csrf) -> csrf.csrfTokenRequestHandler(requestHandler).ignoringRequestMatchers("/register","/contact")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
.addFilterBefore(new RequestValidationBeforeFilter(),BasicAuthenticationFilter.class)
.addFilterAfter(new AuthoritiesLoggingAfterFilter(),BasicAuthenticationFilter.class)
.addFilterAt(new AuthoritiesLoggingAtFilter(),BasicAuthenticationFilter.class)
.authorizeHttpRequests((requests)->requests
.requestMatchers("/myAccount").hasRole("USER")
.requestMatchers("/myBalance").hasAnyRole("USER","ADMIN")
.requestMatchers("/myLoans").hasRole("USER")
.requestMatchers("/myCards").hasRole("USER")
.requestMatchers("/user").authenticated()
.requestMatchers("/notices","/contact","/register").permitAll())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
ProjectSecurityConfig
클래스에서 addFilterAt
메소드 호출하여 커스텀 필터를 구성한다.
첫 번째 매개변수는 커스텀 필터의 객체이고, 두 번째 매개변수는 주입될 필터의 클래스 이름이다.
첫 번째 매개변수: AuthoritiesLoggingAtFilter
객체
두 번째 매개변수: 동일한 위치에 배치할 Spring Security 내부 필터 클래스 이름 (BasicAuthenticationFilter.class
등)
애플리케이션에서 로그인 후 AuthoritiesLoggingAtFilter
가 올바르게 실행되는지 확인한다.
인증 유효성 검사 진행 중
이라는 문장이 콘솔에 찍히는 것을 확인할 수 있다.
BasicAuthenticationFilter
와 같은 위치에 커스텀 필터가 성공적으로 구성됨을 확인한다.
.addFilterAt(new AuthoritiesLoggingAtFilter(),BasicAuthenticationFilter.class)
은 AuthoritiesLoggingAtFilter
와 BasicAuthenticationFilter
의 필터 실행 순서가 서로 무작위로 결정되므로 로직 작성 시 주의가 필요하다.
커스텀 필터를 생성할 때마다 사용할 수 있는 다른 옵션들은 어떤 것이 있는지 알아보자.
이전까지 필터라는 인터페이스를 가지고 구현하였다. 그리고 필터를 구현할 때는 비즈니스 로직을 doFilter()
라는 메소드를 오버라이딩하여 작성했다.
그러나 필터 인터페이스 외에도 Spring 사용 시에는 다른 옵션들도 있다.
추상 클래스로, 필터 인터페이스의 기본적인 구현을 제공한다
설정 매개변수, 초기 매개변수, 서블릿 컨텍스트 매개변수에 대한 세부 정보를 제공한다.
getEnvironment()
(환경 정보), getFilterConfig()
(필터 구성 정보), getServletContext()
(서블릿 컨텍스트 정보), initFilterBean()
(초기 매개변수) 등 다양한 메소드를 제공한다.
설정 매개변수에 접근하여 비즈니스 로직을 구현할 때 유용하다
커스텀 필터를 만들고 해당 필터를 Spring Security FilterChain에 구성하려고 할 때 기본적으로 Spring Security는 해당 필터가 각 요청에 대해 한 번만 실행되도록 보장하지 않는다.
여러 이유로 서블릿 컨테이너가 필터를 여러 번 호출할 수 있는 상황이 발생할 수 있다.
그러나 필터가 반드시 요청 당 한 번만 실행해야 하는 경우 이 추상 클래스OncePerRequestFilter
를 확장하여 커스텀 필터를 정의해야 한다.
= 각 요청마다 한 번만 실행됨을 보장하는 로직을 내장하고 있다.
필터를 건너 뛰고 한 번만 실행되도록 하는 모든 로직이 이 doFilter()
메서드에 정의되어 있다.
만약 OncePerRequestFilter가 이미 doFilter() 메소드를 오버라이드하여 구현했다면 나만의 비즈니스 로직은 어디에 작성해야 하는가?
바로 이 doFilterInternal()
메소드를 통해 비즈니스 로직을 정의한다.
shouldNotFilter()
메소드 등 유용한 메소드 제공하여 특정 요청에 필터를 적용할지 여부 결정이 가능하다.
예를 들어 shouldNotFilter()
는 예외적인 상황이 있을 때 이 필터를 실행하고 싶지 않다면 해당 세부 정보를 정의하고 false 값을 반환하는 대신에 조건에 따라 true를 반환할 수 있다.
GenericFilterBean
: 설정 매개변수에 접근하여 비즈니스 로직을 구현할 때 유용하다.
OncePerRequestFilter
: 각 요청에 대해 한 번만 실행되도록 보장하며, 특정 요청에 필터를 적용하는 경우 유용하다.
BasicAuthenticationFilter
와 같은 Spring Security 필터는 OncePerRequestFilter
를 사용하여 구현된다.
OncePerRequestFilter
를 활용하여 요청 당 한 번만 실행되는 필터를 정의하고자 할 때 사용한다.
가능하다면 OncePerRequestFilter
를 사용하여 커스텀 필터를 만드는 것을 추천한다.