2023.05.07 - [공부 기록] - [SPRING] 스프링이 Event를 다루는 방법 - TransactionalEventListener
개발자 단톡방에서 어떤 한 분이 질문을 올리셨다.
질문 내용은 "/" 경로로 접근하는 경우와 "/login/**" 경로로 로그인 관련 요청을 하는 경우 만 permitAll로 허용해주고 나머지는 시큐리티 필터를 타도록 하고 싶은데 "/" 를 허용해도 403 Forbidden 응답이 내려온다는 것이다.
이렇게 해봐라 저렇게 해봐라 많이들 이야기 해주셨지만 해결되지 않았고, "개인 프로젝트면 깃헙 공유해주세요"라고 부탁드려서 프로젝트를 클론하고 해결해드린 내용을 정리하려고 한다.
우선 프로젝트는 index.html을 static 디렉토리 하위에 위치해두고 카카오 로그인 리다이렉트와 메인 페이지로 활용하고 있었다.
그리고 말씀하신것 처럼 "/"와 "/login/**"을 permitAll 해두었고 나머지는 .anyRequest().authenticated()설정을 해두었다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.cors().and()
.authorizeHttpRequests()
.requestMatchers("/", "/login/**").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.build();
}
처음 시도했던 방법은 index.html이 호출하는 js와 css, image 들에 대한 허용이다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.cors().and()
.authorizeHttpRequests()
.requestMatchers("/", "/login/**").permitAll()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.build();
}
public StaticResourceRequestMatcher atCommonLocations() {
return at(EnumSet.allOf(StaticResourceLocation.class));
}
atCommonLocations는 정적 자원을 일반적으로 위치시키는 경로들에 대해 반환을 해준다. 아래 경로와 일치하지 않다면 디렉터리 이름을 일치시켜주거나 별도로 추가해주면 된다.
public enum StaticResourceLocation {
CSS("/css/**"),
JAVA_SCRIPT("/js/**"),
IMAGES("/images/**"),
WEB_JARS("/webjars/**"),
FAVICON("/favicon.*", "/*/icon-*");
private final String[] patterns;
StaticResourceLocation(String... patterns) {
this.patterns = patterns;
}
public Stream<String> getPatterns() {
return Arrays.stream(this.patterns);
}
}
하지만 그래도 해결되지 않았고 log level을 debug로 올리고 천천히 찾아보기로 했다.
Securing GET /
Mapped to ParameterizableViewController [view="forward:index.html"]
Set SecurityContextHolder to anonymous SecurityContext
Mapped to ParameterizableViewController [view="forward:index.html"]
Secured GET /
Set encoding to UTF-8
GET "/", parameters={}
Mapped to ParameterizableViewController [view="forward:index.html"]
Selected 'text/html' given [text/html, application/xhtml+xml, image/avif, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8, application/signed-exchange;v=b3;q=0.7]
View name 'forward:', model {}
Forwarding to [index.html]
Securing GET /index.html
Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
Pre-authenticated entry point called. Rejecting access
Disabling the response for further output
The Response is vehiculed using a wrapper: org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterResponse
"/"로 들어온 요청은 Anonymous 권한으로 통과하고 index.html로 포워딩까지 되는걸 볼 수 있다.
여기서 index.html로 포워딩 되는 이유는 WelcomePageHandlerMapping 클래스가 index.html을 찾아서 매핑해주기 때문이다.
final class WelcomePageHandlerMapping extends AbstractUrlHandlerMapping {
private static final Log logger = LogFactory.getLog(WelcomePageHandlerMapping.class);
private static final List<MediaType> MEDIA_TYPES_ALL = Collections.singletonList(MediaType.ALL);
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
if (welcomePage != null && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage);
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
private boolean welcomeTemplateExists(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext) {
return templateAvailabilityProviders.getProvider("index", applicationContext) != null;
}
private void setRootViewName(String viewName) {
ParameterizableViewController controller = new ParameterizableViewController();
controller.setViewName(viewName);
setRootHandler(controller);
setOrder(2);
}
@Override
public Object getHandlerInternal(HttpServletRequest request) throws Exception {
for (MediaType mediaType : getAcceptedMediaTypes(request)) {
if (mediaType.includes(MediaType.TEXT_HTML)) {
return super.getHandlerInternal(request);
}
}
return null;
}
private List<MediaType> getAcceptedMediaTypes(HttpServletRequest request) {
String acceptHeader = request.getHeader(HttpHeaders.ACCEPT);
if (StringUtils.hasText(acceptHeader)) {
return MediaType.parseMediaTypes(acceptHeader);
}
return MEDIA_TYPES_ALL;
}
}
정리하자면 "/" 경로로 들어온 요청은 필터에서 permitAll로 권한 인증을 통과했지만 "index.html"로 서블릿 포워딩 되면서 시큐리티 필터를 다시 거치게 되는데 권한 없음으로 403이 내려오는 것이다.
이미 권한 인증을 받았는데 왜 403이 나오는건지 궁금할 수도 있다. 보통 요청 한번에 인증 한번 아닌가?
그 이유는 직접 구현한 토큰 인증 필터와 같은 경우 OncePerRequestFilter를 확장해서 요청 당 한번만 거치도록 설정하지만 기본적으로 필터는 서블릿에 대한 매 요청마다 거치는 것이 기본 전략이기 때문이다.
index.html에 대한 권한 인증이 필요함을 알게되어 처음엔 "/static/index.html"을 허용 경로로 추가했지만 되지 않았다.
그래서 로그를 더 살펴보니 클래스 패스에 매핑된 경로를 찾고 있었다.
Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
그렇다 "/" 로 접근하면 "/" 에 매핑 된 "/static/index.html"로 포워드하는데 "/static/**"은 클래스패스 "/" 에 매핑되어 있기 때문에 "/index.html"이 실제 리소스 접근 경로인것이다.
질문자에게 설명드렸을 때 이 부분을 제일 헷갈려 하셨는데 "/"로 접근했을때 요청은 아래 순서로 처리 된다.
아래와 같이 정적 리소스와 웰컴 페이지에 대한 권한을 허용한는 것으로 해결 되었다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.httpBasic().disable()
.formLogin().disable()
.csrf().disable()
.cors().and()
.authorizeHttpRequests()
.requestMatchers("/", "/login/**").permitAll()
.requestMatchers("/index.html").permitAll()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.build();
}
웰컴 페이지가 안나온다는 정말 단순한 요청이었다.
사실 서버 사이드 렌더링을 할 일이 별로 없어서 웰컴페이지 처리에 대해 생각해 볼 일이 없었는데 필터와 서블릿, 그리고 클래스 패스까지 다시 한번 돌아 볼 수 있는 재밌는 시간이었다.
P.S.
이전 게시글에서 정적 자원 처리를 ingnore를 사용하였는데 이 설정을 사용하면 아래와 같은 로그를 통해 ingnore 대신 authorizeHttpRequests을 통해 permitAll을 사용하기를 권장한다.
You are asking Spring Security to ignore org.springframework.boot.autoconfigure.security.servlet.StaticResourceRequest$StaticResourceRequestMatcher@3ece79fe. This is not recommended -
- please use permitAll via HttpSecurity#authorizeHttpRequests instead.
https://ktae23.tistory.com/251
'공부 기록' 카테고리의 다른 글
[NEXTSTEP] ATDD 과정 1주차 피드백, 2주차 시작 (0) | 2023.07.09 |
---|---|
[NEXTSTEP] ATDD 과정 1주차 시작 (0) | 2023.07.01 |
[SPRING] 스프링이 Event를 다루는 방법 - TransactionalEventListener (0) | 2023.05.07 |
[SPRING] 스프링이 Event를 다루는 방법 - EventListener (0) | 2023.05.07 |
[SPRING] 스프링이 Event를 다루는 방법 - ApplicationEventPublisher (0) | 2023.05.07 |
댓글