CSRF 만료 세션 처리 및 정적 리소스 캐싱 제어 추가
- CSRF 만료 시 기존 세션 만료 안내로 리다이렉트 처리 - 정적 리소스 캐싱 토글 기능 추가 (개발 환경 즉시 반영 지원) - 회원가입 페이지 및 세션 설정 스타일 수정
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -29,4 +29,10 @@ spring:
|
||||
cache: false
|
||||
|
||||
portal:
|
||||
test-auth-notice-enabled: false # prod 환경: UI 안내 미표시
|
||||
test-auth-notice-enabled: false # prod 환경: UI 안내 미표시
|
||||
|
||||
app:
|
||||
resource-versioning:
|
||||
enabled: true
|
||||
resource-caching:
|
||||
enabled: true
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user