CSRF 만료 세션 처리 및 정적 리소스 캐싱 제어 추가
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled

- CSRF 만료 시 기존 세션 만료 안내로 리다이렉트 처리
- 정적 리소스 캐싱 토글 기능 추가 (개발 환경 즉시 반영 지원)
- 회원가입 페이지 및 세션 설정 스타일 수정
This commit is contained in:
Rinjae
2026-06-26 15:05:19 +09:00
parent f79e528fec
commit 82948a13ce
7 changed files with 76 additions and 7 deletions
@@ -12,7 +12,10 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CsrfException;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@@ -107,6 +110,11 @@ public class PortalConfigSecurity {
.ignoringRequestMatchers(new AntPathRequestMatcher("/api/session/check-duplicate"))
.ignoringRequestMatchers(new AntPathRequestMatcher("/internal/migration/**"))
)
// 로그인 페이지에 오래 머물러 세션(=CSRF 토큰 저장소)이 타임아웃되면
// 로그인 제출 시 CsrfFilter가 AnonymousAuthenticationFilter보다 먼저 예외를 던져
// 익명으로 인식되지 못하고 기본 403("권한없음")으로 떨어진다.
// CSRF 만료는 권한 문제가 아니라 세션 만료이므로 기존 안내(/login?expired=true)로 보낸다.
.exceptionHandling(ex -> ex.accessDeniedHandler(csrfAwareAccessDeniedHandler()))
// 중복로그인/유휴 만료는 DB 기반 SessionValidationFilter가 처리 (maximumSessions 미사용)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
@@ -121,6 +129,23 @@ public class PortalConfigSecurity {
}
}
/**
* CSRF 토큰 누락/불일치는 "권한없음"이 아니라 (대개) 세션 만료다.
* 로그인 페이지에 오래 머물러 세션 내 CSRF 토큰이 사라진 경우가 대표적이므로
* 기존 세션 만료 안내(login.html의 {@code param.expired})를 재사용해 오해를 막는다.
* 그 외 진짜 권한 위반(@Secured 등)은 기본 핸들러에 위임해 403을 유지한다.
*/
private AccessDeniedHandler csrfAwareAccessDeniedHandler() {
AccessDeniedHandlerImpl delegate = new AccessDeniedHandlerImpl();
return (request, response, accessDeniedException) -> {
if (accessDeniedException instanceof CsrfException) {
response.sendRedirect(request.getContextPath() + "/login?expired=true");
return;
}
delegate.handle(request, response, accessDeniedException);
};
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
@@ -45,6 +45,12 @@ public class PortalConfigWebDispatcherServlet implements WebMvcConfigurer {
@Value("${app.resource-versioning.enabled:true}")
private boolean resourceVersioningEnabled;
// 정적자원 서버측 캐싱(ResourceChain 캐시) 토글(application.yml: app.resource-caching.enabled).
// prod 는 이 값을 무시하고 항상 ON 으로 동작한다(isResourceCachingEnabled 참고).
// 개발환경에서 OFF 면 sass/JS 변경이 서버 재시작 없이 즉시 반영된다.
@Value("${app.resource-caching.enabled:false}")
private boolean resourceCachingEnabled;
public PortalConfigWebDispatcherServlet(Environment environment) {
this.environment = environment;
}
@@ -97,10 +103,15 @@ public class PortalConfigWebDispatcherServlet implements WebMvcConfigurer {
* {@code @{/css/main.css}} 링크는 {@link #resourceUrlEncodingFilter()} 가 해시 URL 로 치환한다.</p>
*/
private void addStaticResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
// 캐싱 ON(prod) 이면 브라우저에 1일 캐시를, OFF(dev/local) 면 no-store 를 내려보낸다.
// no-store 는 브라우저가 아예 저장하지 않으므로 강제 새로고침 없이 sass/JS 변경이 바로 보인다.
CacheControl cacheControl = isResourceCachingEnabled()
? CacheControl.maxAge(1, TimeUnit.DAYS)
: CacheControl.noStore();
ResourceChainRegistration chain = registry.addResourceHandler(pattern)
.addResourceLocations(locations)
.setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS))
.resourceChain(true);
.setCacheControl(cacheControl)
.resourceChain(isResourceCachingEnabled());
if (isResourceVersioningEnabled()) {
chain.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}
@@ -118,6 +129,20 @@ public class PortalConfigWebDispatcherServlet implements WebMvcConfigurer {
return resourceVersioningEnabled;
}
/**
* 정적자원 서버측 캐싱(ResourceChain 의 CachingResourceResolver) 활성 여부.
* prod 프로파일은 토글과 무관하게 항상 ON, 그 외 프로파일은 {@code app.resource-caching.enabled} 값을 따른다.
*
* <p>OFF 면 매 요청마다 리소스를 재해석하므로, sass/JS 변경이 서버 재시작 없이 즉시 반영된다.
* ({@code spring.web.resources.cache} 는 브라우저 캐시 헤더만 제어할 뿐 이 서버측 캐시와는 무관하다.)</p>
*/
private boolean isResourceCachingEnabled() {
if (environment.acceptsProfiles(Profiles.of("prod"))) {
return true;
}
return resourceCachingEnabled;
}
@Bean
public MappingJackson2JsonView jsonView() {
+6
View File
@@ -30,3 +30,9 @@ spring:
portal:
test-auth-notice-enabled: false # prod 환경: UI 안내 미표시
app:
resource-versioning:
enabled: true
resource-caching:
enabled: true
+9 -2
View File
@@ -2,7 +2,9 @@ server:
servlet:
context-path: /
session:
timeout: 15m
timeout: 10m
cookie:
name: PORTAL_JSESSIONID
encoding:
charset: UTF-8
port: '39130'
@@ -59,7 +61,12 @@ spring:
# prod 는 PortalConfigWebDispatcherServlet 에서 항상 ON 으로 고정되어 이 값을 무시함.
app:
resource-versioning:
enabled: true
enabled: false
# 정적자원 서버측 캐싱(ResourceChain 캐시) 토글 (prod 제외 전 프로파일에 적용).
# false 면 sass/JS 변경이 서버 재시작 없이 즉시 반영됨(매 요청 재해석).
# prod 는 PortalConfigWebDispatcherServlet 에서 항상 ON 으로 고정되어 이 값을 무시함.
resource-caching:
enabled: false
security:
basic:
+1 -1
View File
@@ -10908,7 +10908,7 @@ button.djb-comment-submit:disabled {
}
}
.signup-selection-page {
min-height: calc(100vh - 140px);
min-height: calc(100vh - 380px);
display: flex;
align-items: center;
justify-content: center;
@@ -7,7 +7,7 @@
// -----------------------------------------------------------------------------
.signup-selection-page {
min-height: calc(100vh - 140px); // Account for header and footer
min-height: calc(100vh - 380px); // Account for header and footer
display: flex;
align-items: center;
justify-content: center;
@@ -82,6 +82,12 @@
<script th:src="@{/plugins/summernote/summernote-lite.min.js}"></script>
<script th:src="@{/plugins/summernote/summernote-cleaner.js}"></script>
<script th:src="@{/plugins/jquery-ui/jquery-ui.min.js}"></script>
<!-- LiveReload (개발 전용): DevTools 가 정적 리소스/템플릿 변경을 감지하면 브라우저를 자동 새로고침한다.
localhost 외 IP(예: 172.30.1.14)로 접속해도 동작하도록 접속 호스트 기준으로 livereload.js 를 로드한다.
prod/stage 에는 DevTools 자체가 없으므로(developmentOnly) 개발 프로파일에서만 주입한다. -->
<script th:if="${@environment.acceptsProfiles('local_rinjaemac','local')}"
th:src="|//${#request.serverName}:35729/livereload.js|"></script>
</head>
</html>