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.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
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.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
import org.springframework.security.web.csrf.CsrfException;
|
||||||
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
|
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
|
||||||
import org.springframework.security.web.session.HttpSessionEventPublisher;
|
import org.springframework.security.web.session.HttpSessionEventPublisher;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
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("/api/session/check-duplicate"))
|
||||||
.ignoringRequestMatchers(new AntPathRequestMatcher("/internal/migration/**"))
|
.ignoringRequestMatchers(new AntPathRequestMatcher("/internal/migration/**"))
|
||||||
)
|
)
|
||||||
|
// 로그인 페이지에 오래 머물러 세션(=CSRF 토큰 저장소)이 타임아웃되면
|
||||||
|
// 로그인 제출 시 CsrfFilter가 AnonymousAuthenticationFilter보다 먼저 예외를 던져
|
||||||
|
// 익명으로 인식되지 못하고 기본 403("권한없음")으로 떨어진다.
|
||||||
|
// CSRF 만료는 권한 문제가 아니라 세션 만료이므로 기존 안내(/login?expired=true)로 보낸다.
|
||||||
|
.exceptionHandling(ex -> ex.accessDeniedHandler(csrfAwareAccessDeniedHandler()))
|
||||||
// 중복로그인/유휴 만료는 DB 기반 SessionValidationFilter가 처리 (maximumSessions 미사용)
|
// 중복로그인/유휴 만료는 DB 기반 SessionValidationFilter가 처리 (maximumSessions 미사용)
|
||||||
.sessionManagement(session -> session
|
.sessionManagement(session -> session
|
||||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
.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
|
@Bean
|
||||||
public HttpSessionEventPublisher httpSessionEventPublisher() {
|
public HttpSessionEventPublisher httpSessionEventPublisher() {
|
||||||
return new HttpSessionEventPublisher();
|
return new HttpSessionEventPublisher();
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ public class PortalConfigWebDispatcherServlet implements WebMvcConfigurer {
|
|||||||
@Value("${app.resource-versioning.enabled:true}")
|
@Value("${app.resource-versioning.enabled:true}")
|
||||||
private boolean resourceVersioningEnabled;
|
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) {
|
public PortalConfigWebDispatcherServlet(Environment environment) {
|
||||||
this.environment = environment;
|
this.environment = environment;
|
||||||
}
|
}
|
||||||
@@ -97,10 +103,15 @@ public class PortalConfigWebDispatcherServlet implements WebMvcConfigurer {
|
|||||||
* {@code @{/css/main.css}} 링크는 {@link #resourceUrlEncodingFilter()} 가 해시 URL 로 치환한다.</p>
|
* {@code @{/css/main.css}} 링크는 {@link #resourceUrlEncodingFilter()} 가 해시 URL 로 치환한다.</p>
|
||||||
*/
|
*/
|
||||||
private void addStaticResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
|
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)
|
ResourceChainRegistration chain = registry.addResourceHandler(pattern)
|
||||||
.addResourceLocations(locations)
|
.addResourceLocations(locations)
|
||||||
.setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS))
|
.setCacheControl(cacheControl)
|
||||||
.resourceChain(true);
|
.resourceChain(isResourceCachingEnabled());
|
||||||
if (isResourceVersioningEnabled()) {
|
if (isResourceVersioningEnabled()) {
|
||||||
chain.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
|
chain.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
|
||||||
}
|
}
|
||||||
@@ -118,6 +129,20 @@ public class PortalConfigWebDispatcherServlet implements WebMvcConfigurer {
|
|||||||
return resourceVersioningEnabled;
|
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
|
@Bean
|
||||||
public MappingJackson2JsonView jsonView() {
|
public MappingJackson2JsonView jsonView() {
|
||||||
|
|||||||
@@ -29,4 +29,10 @@ spring:
|
|||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
portal:
|
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:
|
servlet:
|
||||||
context-path: /
|
context-path: /
|
||||||
session:
|
session:
|
||||||
timeout: 15m
|
timeout: 10m
|
||||||
|
cookie:
|
||||||
|
name: PORTAL_JSESSIONID
|
||||||
encoding:
|
encoding:
|
||||||
charset: UTF-8
|
charset: UTF-8
|
||||||
port: '39130'
|
port: '39130'
|
||||||
@@ -59,7 +61,12 @@ spring:
|
|||||||
# prod 는 PortalConfigWebDispatcherServlet 에서 항상 ON 으로 고정되어 이 값을 무시함.
|
# prod 는 PortalConfigWebDispatcherServlet 에서 항상 ON 으로 고정되어 이 값을 무시함.
|
||||||
app:
|
app:
|
||||||
resource-versioning:
|
resource-versioning:
|
||||||
enabled: true
|
enabled: false
|
||||||
|
# 정적자원 서버측 캐싱(ResourceChain 캐시) 토글 (prod 제외 전 프로파일에 적용).
|
||||||
|
# false 면 sass/JS 변경이 서버 재시작 없이 즉시 반영됨(매 요청 재해석).
|
||||||
|
# prod 는 PortalConfigWebDispatcherServlet 에서 항상 ON 으로 고정되어 이 값을 무시함.
|
||||||
|
resource-caching:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
security:
|
security:
|
||||||
basic:
|
basic:
|
||||||
|
|||||||
@@ -10908,7 +10908,7 @@ button.djb-comment-submit:disabled {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.signup-selection-page {
|
.signup-selection-page {
|
||||||
min-height: calc(100vh - 140px);
|
min-height: calc(100vh - 380px);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
.signup-selection-page {
|
.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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -82,6 +82,12 @@
|
|||||||
<script th:src="@{/plugins/summernote/summernote-lite.min.js}"></script>
|
<script th:src="@{/plugins/summernote/summernote-lite.min.js}"></script>
|
||||||
<script th:src="@{/plugins/summernote/summernote-cleaner.js}"></script>
|
<script th:src="@{/plugins/summernote/summernote-cleaner.js}"></script>
|
||||||
<script th:src="@{/plugins/jquery-ui/jquery-ui.min.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>
|
</head>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user