클라이언트 하드닝 기능 추가:
- DevTools 감지 및 화면 삭제, 우클릭 차단 기능 구현 - PortalProperty 기반 설정 관리 서비스 추가 - 클라이언트 가드 스크립트 및 토글 로직 템플릿 연동
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
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'}"/>
|
||||||
|
|||||||