클라이언트 하드닝 기능 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled

- DevTools 감지 및 화면 삭제, 우클릭 차단 기능 구현
- PortalProperty 기반 설정 관리 서비스 추가
- 클라이언트 가드 스크립트 및 토글 로직 템플릿 연동
This commit is contained in:
Rinjae
2026-07-06 15:32:18 +09:00
parent b02e598d62
commit 83d267faed
13 changed files with 141 additions and 0 deletions
@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import com.eactive.apim.portal.config.PortalProperties; import com.eactive.apim.portal.config.PortalProperties;
import com.eactive.apim.portal.apps.session.service.UserSessionService; import com.eactive.apim.portal.apps.session.service.UserSessionService;
import com.eactive.apim.portal.common.security.ClientGuardService;
@ControllerAdvice @ControllerAdvice
public class GlobalControllerAdvice { public class GlobalControllerAdvice {
@@ -21,6 +22,9 @@ public class GlobalControllerAdvice {
@Autowired @Autowired
private UserSessionService userSessionService; private UserSessionService userSessionService;
@Autowired
private ClientGuardService clientGuardService;
@ModelAttribute("breadcrumb") @ModelAttribute("breadcrumb")
public List<Map> addBreadcrumbToModel(HttpServletRequest request) { public List<Map> addBreadcrumbToModel(HttpServletRequest request) {
String currentPath = request.getRequestURI(); String currentPath = request.getRequestURI();
@@ -55,4 +59,22 @@ public class GlobalControllerAdvice {
public int sessionTimeoutMinutes() { public int sessionTimeoutMinutes() {
return userSessionService.getSessionTimeoutMinutes(); return userSessionService.getSessionTimeoutMinutes();
} }
/**
* 클라이언트 가드 - 우클릭(contextmenu) 차단 활성화 여부.
* PortalProperty(Portal/djb.client-guard.contextmenu.enabled)에서 조회.
*/
@ModelAttribute("clientGuardContextmenu")
public boolean clientGuardContextmenu() {
return clientGuardService.isContextmenuBlockEnabled();
}
/**
* 클라이언트 가드 - DevTools 감지 시 화면 삭제 활성화 여부.
* PortalProperty(Portal/djb.client-guard.devtools.enabled)에서 조회.
*/
@ModelAttribute("clientGuardDevtools")
public boolean clientGuardDevtools() {
return clientGuardService.isDevtoolsGuardEnabled();
}
} }
@@ -0,0 +1,42 @@
package com.eactive.apim.portal.common.security;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 클라이언트측 하드닝(우클릭 차단 · DevTools 감지) 토글을 DB(PortalProperty)에서 조회한다.
*
* <p>두 기능은 서로 독립된 property 로 on/off 한다. group 은 기존 {@code Portal} 을 재사용하여
* {@link PortalPropertyService#getOrCreateProperty} 의 자동 생성이 동작하도록 한다.</p>
*
* <ul>
* <li>{@code djb.client-guard.contextmenu.enabled} - 우클릭(contextmenu) 차단</li>
* <li>{@code djb.client-guard.devtools.enabled} - DevTools 감지 시 화면 삭제</li>
* </ul>
*/
@Service
@RequiredArgsConstructor
public class ClientGuardService {
private static final String GROUP = "Portal";
private static final String NAME_CONTEXTMENU = "djb.client-guard.contextmenu.enabled";
private static final String NAME_DEVTOOLS = "djb.client-guard.devtools.enabled";
private final PortalPropertyService portalPropertyService;
/** 우클릭(contextmenu) 차단 활성화 여부 */
public boolean isContextmenuBlockEnabled() {
return read(NAME_CONTEXTMENU, "우클릭(contextmenu) 차단");
}
/** DevTools 감지 시 화면 삭제 활성화 여부 */
public boolean isDevtoolsGuardEnabled() {
return read(NAME_DEVTOOLS, "DevTools 감지 시 화면 삭제");
}
private boolean read(String name, String desc) {
return Boolean.parseBoolean(
portalPropertyService.getOrCreateProperty(GROUP, name, "false", desc).trim());
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,60 @@
/**
* client-guard.js
*
* 클라이언트측 하드닝. 서버가 내려준 window.__CLIENT_GUARD__ 플래그에 따라 각 기능을 독립 적용한다.
* - contextmenu: 우클릭(컨텍스트 메뉴) 차단
* - devtools : 창 크기 휴리스틱으로 DevTools 열림 감지 시 화면 전체 요소 영구 삭제
*
* 주의(한계): 창 크기 휴리스틱은 오탐/미탐이 있다(줌·북마크바·사이드패널·좁은창=오탐, undock 창=미탐).
* JS 비활성화·breakpoint·DOM 편집으로 우회 가능하므로 실질 보안이 아니라 억제용 데코이다.
*/
(function () {
'use strict';
var cfg = window.__CLIENT_GUARD__ || {};
// ── 우클릭(contextmenu) 차단 ─────────────────────────────────────────
if (cfg.contextmenu) {
document.addEventListener('contextmenu', function (e) {
e.preventDefault();
}, true);
}
// ── DevTools 감지 → 화면 영구 삭제 ───────────────────────────────────
if (cfg.devtools) {
var THRESHOLD = 160; // 도킹된 DevTools 패널이 차지하는 최소 폭/높이(px) 추정값
var POLL_MS = 500;
var wiped = false;
function isDevtoolsOpen() {
var widthGap = window.outerWidth - window.innerWidth;
var heightGap = window.outerHeight - window.innerHeight;
return widthGap > THRESHOLD || heightGap > THRESHOLD;
}
function wipe() {
if (wiped) {
return;
}
wiped = true;
// 화면 전체 요소 영구 삭제 (닫아도 복구되지 않음, 새로고침 필요)
try {
document.documentElement.replaceChildren();
} catch (ignore) {
if (document.body) {
document.body.innerHTML = '';
}
}
}
function check() {
if (!wiped && isDevtoolsOpen()) {
wipe();
}
}
window.setInterval(check, POLL_MS);
window.addEventListener('resize', check);
check();
}
})();
@@ -7,6 +7,23 @@
<meta content="https://www.eactive.co.kr/" property="og:url"/> <meta content="https://www.eactive.co.kr/" property="og:url"/>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<!-- Favicon (DJB 로고) -->
<link rel="icon" type="image/png" sizes="32x32" th:href="@{/img/favicon/favicon-32x32.png}"/>
<link rel="icon" type="image/png" sizes="16x16" th:href="@{/img/favicon/favicon-16x16.png}"/>
<link rel="shortcut icon" type="image/png" th:href="@{/img/favicon/favicon.png}"/>
<link rel="apple-touch-icon" sizes="180x180" th:href="@{/img/favicon/apple-touch-icon.png}"/>
<!-- 클라이언트 가드 (우클릭 차단 · DevTools 감지). PortalProperty 토글이 하나라도 켜지면 로드 -->
<th:block th:if="${clientGuardContextmenu or clientGuardDevtools}">
<script th:inline="javascript">
window.__CLIENT_GUARD__ = {
contextmenu: /*[[${clientGuardContextmenu}]]*/ false,
devtools: /*[[${clientGuardDevtools}]]*/ false
};
</script>
<script th:src="@{/js/djb/client-guard.js}"></script>
</th:block>
<!-- CSRF 토큰 (세션 기반). 정적 JS/AJAX에서 토큰·헤더명을 읽어 사용한다. --> <!-- CSRF 토큰 (세션 기반). 정적 JS/AJAX에서 토큰·헤더명을 읽어 사용한다. -->
<meta name="_csrf" th:content="${_csrf != null ? _csrf.token : ''}"/> <meta name="_csrf" th:content="${_csrf != null ? _csrf.token : ''}"/>
<meta name="_csrf_header" th:content="${_csrf != null ? _csrf.headerName : 'X-XSRF-TOKEN'}"/> <meta name="_csrf_header" th:content="${_csrf != null ? _csrf.headerName : 'X-XSRF-TOKEN'}"/>