Merge branch 'master' into design

# Conflicts:
#	src/main/resources/templates/views/apps/service/oauth2-guide.html
This commit is contained in:
Rinjae
2026-07-23 18:09:00 +09:00
10 changed files with 92 additions and 47 deletions
+4 -5
View File
@@ -2,11 +2,10 @@ server:
servlet:
context-path: /
session:
# 물리 세션 타임아웃은 DB PortalProperty(Portal/session.timeout.minutes)로 관리한다.
# 로그인 성공 시 PortalAuthenticationSuccessHandler 가
# session.setMaxInactiveInterval(session.timeout.minutes * 60) 으로 적용 → 물리=논리 일치.
# 익명/로그인 전 세션은 컨테이너 기본값으로 fallback (weblogic.xml <timeout-secs>1800).
# timeout: 10m
# 세션 타임아웃 10분 고정 (DB property 관리 폐지).
# WebLogic 배포 시에는 weblogic.xml <timeout-secs>600 이 동일 값을 적용한다.
# CSRF 토큰은 세션에 저장(HttpSessionCsrfTokenRepository)되므로 수명도 이 값과 동일하다.
timeout: 10m
cookie:
name: JSESSIONID_PORTAL
encoding:
@@ -115,6 +115,24 @@
form.checkId.checked = ((form.id.value = getCookie('saveid')) !== null);
}
// CSRF 토큰 재발급 안전망: keepalive ping이 끊긴 경우(절전·네트워크 단절 등)
// 세션이 만료됐어도 제출 직전 새 토큰을 받아 403(만료) 대신 정상 로그인되게 한다.
// 재발급 실패 시에는 기존 토큰으로 그대로 제출한다.
function refreshCsrfAndThen(form, next) {
$.ajax({
url: /*[[@{/api/session/csrf}]]*/ '/api/session/csrf',
type: 'GET',
success: function (data) {
if (data && data.token) {
if (form['_csrf']) { form['_csrf'].value = data.token; }
var meta = document.querySelector('meta[name="_csrf"]');
if (meta) { meta.content = data.token; }
}
},
complete: function () { next(); }
});
}
// 로그인 전 중복 접속 확인 → 중복 시 기존 세션 강제 로그아웃 여부 질의
function checkDuplicateAndLogin(form) {
var loginId = form.id.value;
@@ -157,26 +175,8 @@
$(function () {
// CSRF 토큰은 세션 기반 → 세션 만료(server.servlet.session.timeout=10m) 전에
// 메인 페이지로 이동시켜 stale 토큰 제출(403) 방지. 이동은 서버 요청이라 세션 idle도 리셋됨.
// 단, 입력 중에는 이탈 금지 → idle 타이머(활동 시 리셋, 입력값/포커스 있으면 보류).
// 만료 시간은 하드코딩하지 않고 서버 세션 타임아웃(초)을 렌더 시점에 읽어 파생 → yml 변경 시 자동 반영.
var SESSION_TIMEOUT_SEC = /*[[${#request.session.maxInactiveInterval}]]*/ 600;
var LOGIN_IDLE_LIMIT_MS = Math.max(60, SESSION_TIMEOUT_SEC - 120) * 1000; // 세션 만료 2분 전
var loginIdleTimer;
function scheduleIdleRedirect() {
clearTimeout(loginIdleTimer);
loginIdleTimer = setTimeout(function () {
var idEl = document.getElementById('id');
var pwEl = document.getElementById('password');
var busy = document.activeElement === idEl || document.activeElement === pwEl
|| (idEl && idEl.value) || (pwEl && pwEl.value);
if (busy) { scheduleIdleRedirect(); return; } // 입력 중/입력값 있음 → 이탈 보류
window.location.href = /*[[@{/}]]*/ '/';
}, LOGIN_IDLE_LIMIT_MS);
}
$('#id, #password, #checkId').on('input keydown focus click', scheduleIdleRedirect);
scheduleIdleRedirect();
// 세션 만료 대응은 head의 익명 keepalive ping(/api/session/ping)이 담당하고,
// 제출 직전 refreshCsrfAndThen()이 CSRF 토큰 재발급 안전망 역할을 한다.
var successMsg = [[${success}]];
console.log("Success message:", successMsg);
@@ -225,7 +225,9 @@
$('#loginLoading').removeClass('active');
customPopups.showAlert('[[#{login.passLengthShort}]]');
} else {
checkDuplicateAndLogin(form);
refreshCsrfAndThen(form, function () {
checkDuplicateAndLogin(form);
});
}
}
form.classList.add('was-validated');
@@ -1,5 +1,6 @@
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head th:fragment="headFragment">
<title th:text="#{title.html}">개발자 포털</title>
@@ -27,6 +28,23 @@
<!-- CSRF 토큰 (세션 기반). 정적 JS/AJAX에서 토큰·헤더명을 읽어 사용한다. -->
<meta name="_csrf" th:content="${_csrf != null ? _csrf.token : ''}"/>
<meta name="_csrf_header" th:content="${_csrf != null ? _csrf.headerName : 'X-XSRF-TOKEN'}"/>
<!-- 익명(비로그인) 세션 keepalive: 로그인/회원가입 등에서 페이지에 머무는 동안 주기적 ping으로
세션 비활성 타이머를 리셋해 세션 저장 CSRF 토큰·회원가입 본인인증 상태의 만료(10분)를 방지한다.
탭을 닫으면 ping이 멈춰 정상 만료. 인증 사용자는 헤더의 세션 타이머/heartbeat가 대신 처리한다. -->
<script sec:authorize="isAnonymous()" th:inline="javascript">
(function () {
var PING_URL = /*[[@{/api/session/ping}]]*/ '/api/session/ping';
var PING_INTERVAL_MS = 4 * 60 * 1000; // 세션 타임아웃(10분)의 절반 이하
setInterval(function () {
try {
var xhr = new XMLHttpRequest();
xhr.open('GET', PING_URL, true);
xhr.send();
} catch (ignore) { /* keepalive 실패는 화면 동작에 영향 없음 */ }
}, PING_INTERVAL_MS);
})();
</script>
<meta content="max-age=0, public" http-equiv="Cache-Control"/>
<meta content="index, follow" name="robots"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
@@ -284,7 +284,7 @@
var FORCE_LOGOUT_URL = /*[[@{/login?forceLogout=true}]]*/ '/login?forceLogout=true';
var LOGOUT_URL = /*[[@{/actionLogout.do}]]*/ '/actionLogout.do';
var timeoutMinutes = /*[[${sessionTimeoutMinutes}]]*/ 15;
var timeoutMinutes = /*[[${sessionTimeoutMinutes}]]*/ 10;
var remainingSeconds = timeoutMinutes * 60;
var WARNING_SECONDS = 60; // 만료 60초 전 연장 확인 모달
var POLL_INTERVAL_MS = 30000; // 서버 잔여시간 동기화 주기
+2 -1
View File
@@ -14,7 +14,8 @@
</container-descriptor>
<session-descriptor>
<timeout-secs>1800</timeout-secs>
<!-- 세션 타임아웃 10분 고정 (application.yml server.servlet.session.timeout=10m 과 동일 값 유지) -->
<timeout-secs>600</timeout-secs>
<cookie-name>JSESSIONID_PORTAL</cookie-name>
<persistent-store-type>replicated_if_clustered</persistent-store-type>
</session-descriptor>