diff --git a/WebContent/WEB-INF/auditpoints.dat b/WebContent/WEB-INF/auditpoints.dat index 136d44c..8c4f211 100644 --- a/WebContent/WEB-INF/auditpoints.dat +++ b/WebContent/WEB-INF/auditpoints.dat @@ -12,6 +12,6 @@ PortalTermsManController_약관관리_APIGW_INSERT,UPDATE,DELETE ProdClientManController_운영Client (키정보)관리_APIGW_INSERT,UPDATE,DELETE ApiSpecController_API스펙관리_APIGW_INSERT,DELETE MessageTemplateManController_메세지템플릿관리_APIGW_INSERT,UPDATE,DELETE -PortalPartnershipManController_피드백/개선요청관리_APIGW_UNMASK +PortalPartnershipManController_피드백/개선요청관리_APIGW_UNMASK,DELETE PortalPropertyController_포탈 프로퍼티 관리_APIGW_UPDATE PortalUserTermsManController_약관동의 이력_APIGW_UNMASK \ No newline at end of file diff --git a/WebContent/jsp/js/stringMaskingUtil.js b/WebContent/jsp/js/stringMaskingUtil.js index 184c2af..c4fed19 100644 --- a/WebContent/jsp/js/stringMaskingUtil.js +++ b/WebContent/jsp/js/stringMaskingUtil.js @@ -1,5 +1,10 @@ /** * 문자열 마스킹 처리를 위한 유틸리티 + * + * 보안아키텍처 표준 마스킹 규칙(Java MaskingUtils)과 동일한 결과를 내도록 구현한다. + * 이름 2자: 뒤 1자리(가나→가*) / 3자 이상: 앞뒤 제외 가운데 전체(가나다라→가**라) + * 이메일 아이디 앞2·뒤2 마스킹, 4자 이하 전체(test→****, test123→**st1**) + * 전화 2·3번째 세그먼트 각각 뒤 2자리(010-1234-1234→010-12**-12**) */ const StringMaskingUtil = { @@ -8,41 +13,51 @@ const StringMaskingUtil = { return str != null && str !== ''; }, - // 이름 마스킹 처리 + // '*' 반복 생성 + _stars: function(count) { + return count > 0 ? '*'.repeat(count) : ''; + }, + + // 세그먼트 뒤 count 자리 마스킹 + _maskSegmentTail: function(segment, count) { + if (segment.length <= count) { + return this._stars(segment.length); + } + return segment.substring(0, segment.length - count) + this._stars(count); + }, + + // 이름 마스킹 처리 (2자: 뒤 1자리, 3자 이상: 앞뒤 제외 가운데 전체) maskName: function(name) { if (!this.isValidString(name)) { return name; } - return name.length > 1 ? - name.substring(0, name.length - 1) + "*" : - name; + const len = name.length; + if (len === 1) { + return name; + } + if (len === 2) { + return name.charAt(0) + '*'; + } + return name.charAt(0) + this._stars(len - 2) + name.charAt(len - 1); }, - // 이메일 마스킹 처리 + // 이메일 마스킹 처리 (아이디 앞2·뒤2, 4자 이하 전체) maskEmail: function(email) { if (!this.isValidString(email) || !email.includes('@')) { return email; } - const parts = email.split('@'); - if (parts.length !== 2) { - return email; + const atIdx = email.indexOf('@'); + const local = email.substring(0, atIdx); + const domain = email.substring(atIdx); + + if (local.length <= 4) { + return this._stars(local.length) + domain; } - - const localPart = parts[0]; - let maskedLocal; - - if (localPart.length <= 3) { - maskedLocal = localPart.substring(0, 1) + - '*'.repeat(localPart.length - 1); - } else { - maskedLocal = localPart.substring(0, localPart.length - 3) + '***'; - } - - return maskedLocal + '@' + parts[1]; + return this._stars(2) + local.substring(2, local.length - 2) + this._stars(2) + domain; }, - // 휴대폰 번호 마스킹 처리 + // 휴대폰/전화 번호 마스킹 처리 (2·3번째 세그먼트 각각 뒤 2자리) maskMobileNumber: function(number) { if (!this.isValidString(number)) { return number; @@ -50,8 +65,10 @@ const StringMaskingUtil = { const parts = number.split('-'); if (parts.length === 3) { - return parts[0] + '-****-' + parts[2]; + return parts[0] + '-' + + this._maskSegmentTail(parts[1], 2) + '-' + + this._maskSegmentTail(parts[2], 2); } return number; } -}; \ No newline at end of file +}; diff --git a/WebContent/jsp/onl/apim/portalpartnership/portalPartnershipMan.jsp b/WebContent/jsp/onl/apim/portalpartnership/portalPartnershipMan.jsp index 27f5b61..eae9443 100644 --- a/WebContent/jsp/onl/apim/portalpartnership/portalPartnershipMan.jsp +++ b/WebContent/jsp/onl/apim/portalpartnership/portalPartnershipMan.jsp @@ -26,31 +26,6 @@ function formatFile(cellvalue, options, rowObject) { return cellvalue + icon; } -function formatDelete(cellvalue, options, rowObject) { - return ''; -} - -function deleteRow(id) { - if (!confirm('선택한 항목을 삭제하시겠습니까?')) { - return; - } - - $.ajax({ - type: "POST", - url: url, - dataType: "json", - data: { cmd: 'DELETE', id: id }, - success: function(data) { - alert('삭제되었습니다.'); - $("#grid").trigger("reloadGrid"); - }, - error: function(e) { - alert('삭제 중 오류가 발생했습니다.'); - } - }); -} - function deleteSelectedRows() { var selectedIds = $("#grid").jqGrid('getGridParam', 'selarrrow'); @@ -91,14 +66,13 @@ $(document).ready(function() { searchBizSubject: $('input[name=searchBizSubject]').val() , searchBizDetail: $('input[name=searchBizDetail]').val() }, - colNames:['No.', 'ID', '제목', '작성자', '작성일시', '관리'], + colNames:['No.', 'ID', '제목', '작성자', '작성일시'], colModel:[ { name : 'rowNum' , align:'center', width: 40, sortable: false }, { name : 'id' , align:'center', key: true, hidden: true}, { name : 'bizSubject' , align:'center', width: 150, formatter: formatFile }, { name : 'createdByName' , align:'center' ,width: 80}, - { name : 'createdDate' , align:'center', width: 100, formatter: timeStampFormat }, - { name : 'act' , align:'center', width: 60, sortable: false, formatter: formatDelete } + { name : 'createdDate' , align:'center', width: 100, formatter: timeStampFormat } ], jsonReader: { repeatitems:false diff --git a/WebContent/jsp/onl/apim/portalpartnership/portalPartnershipManDetail.jsp b/WebContent/jsp/onl/apim/portalpartnership/portalPartnershipManDetail.jsp index 756a7f5..11b4289 100644 --- a/WebContent/jsp/onl/apim/portalpartnership/portalPartnershipManDetail.jsp +++ b/WebContent/jsp/onl/apim/portalpartnership/portalPartnershipManDetail.jsp @@ -92,6 +92,27 @@ $(document).ready(function() { goNav(returnUrl);//LIST로 이동 }); + $("#btn_delete").click(function(){ + if (!confirm('선택한 항목을 삭제하시겠습니까?')) { + return; + } + + $.ajax({ + type: "POST", + url: url, + dataType: "json", + data: { cmd: 'DELETE', id: key }, + success: function(data) { + alert('삭제되었습니다.'); + var returnUrl = getReturnUrlForReturn(); + goNav(returnUrl);//LIST로 이동 + }, + error: function(e) { + alert('삭제 중 오류가 발생했습니다.'); + } + }); + }); + $("#btn_unmask").click(function () { const reason = prompt("마스킹 해제 사유를 입력하세요:"); @@ -134,6 +155,7 @@ $(document).ready(function() {
+
피드백/개선요청 신청 상세
diff --git a/src/main/java/com/eactive/eai/rms/ext/kjb/smsauth/SmsAuthService.java b/src/main/java/com/eactive/eai/rms/ext/kjb/smsauth/SmsAuthService.java index ea0a1fa..16f7f05 100644 --- a/src/main/java/com/eactive/eai/rms/ext/kjb/smsauth/SmsAuthService.java +++ b/src/main/java/com/eactive/eai/rms/ext/kjb/smsauth/SmsAuthService.java @@ -267,17 +267,14 @@ public class SmsAuthService { } /** - * 전화번호 마스킹 + * 전화번호 마스킹 (표준 규칙: 2·3번째 세그먼트 각각 뒤 2자리) + * 예: 010-1234-5678 → 010-12**-56** */ public String maskPhoneNumber(String phone) { if (StringUtils.isEmpty(phone)) { return "****"; } - String cleaned = phone.replaceAll("[^0-9]", ""); - if (cleaned.length() < 7) { - return "****"; - } - return cleaned.substring(0, 3) + "****" + cleaned.substring(cleaned.length() - 4); + return com.eactive.eai.common.util.MaskingUtils.maskPhoneNumber(phone); } /** diff --git a/src/main/java/com/eactive/eai/rms/ext/kjb/sso/SsoController.java b/src/main/java/com/eactive/eai/rms/ext/kjb/sso/SsoController.java index 114623f..5789cb3 100644 --- a/src/main/java/com/eactive/eai/rms/ext/kjb/sso/SsoController.java +++ b/src/main/java/com/eactive/eai/rms/ext/kjb/sso/SsoController.java @@ -134,8 +134,8 @@ public class SsoController implements InterceptorSkipController { log.debug("SSO 사용자 정보 - userId: {}, name: {}, phone: {}, email: {}", userId, MaskingUtils.maskName(name), - maskPhoneNumber(cellPhoneNo), - maskEmail(email)); + MaskingUtils.maskPhoneNumber(cellPhoneNo), + MaskingUtils.maskEmailId(email)); // 사용자 생성 또는 업데이트 UserInfo userInfo = findOrCreateUser(info, extendedInfo); @@ -341,33 +341,4 @@ public class SsoController implements InterceptorSkipController { } return phone.replaceAll("[^0-9]", ""); } - - /** - * 휴대폰번호 마스킹 (로그용) - */ - private String maskPhoneNumber(String phone) { - if (StringUtils.isEmpty(phone) || phone.length() < 4) { - return "****"; - } - // 010-1234-5678 → 010-****-5678 - String cleaned = phone.replaceAll("[^0-9]", ""); - if (cleaned.length() < 7) { - return "****"; - } - return cleaned.substring(0, 3) + "****" + cleaned.substring(cleaned.length() - 4); - } - - /** - * 이메일 마스킹 (로그용) - */ - private String maskEmail(String email) { - if (StringUtils.isEmpty(email) || !email.contains("@")) { - return "****"; - } - int atIndex = email.indexOf("@"); - if (atIndex <= 2) { - return "**" + email.substring(atIndex); - } - return email.substring(0, 2) + "**" + email.substring(atIndex); - } } diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/masking/MaskingUtils.java b/src/main/java/com/eactive/eai/rms/onl/apim/masking/MaskingUtils.java index 18e836b..4c03cef 100644 --- a/src/main/java/com/eactive/eai/rms/onl/apim/masking/MaskingUtils.java +++ b/src/main/java/com/eactive/eai/rms/onl/apim/masking/MaskingUtils.java @@ -1,83 +1,52 @@ package com.eactive.eai.rms.onl.apim.masking; -import com.eactive.eai.rms.common.util.StringUtils; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - +/** + * 개인정보 마스킹 유틸리티 (eapim-admin) + * + *

실제 마스킹 규칙은 보안아키텍처 표준 구현인 + * {@link com.eactive.eai.common.util.MaskingUtils}(elink-online-core)에 위임한다. + * 기존 호출부 호환을 위해 메서드명/시그니처는 그대로 유지한다.

+ * + *
+ *   이메일   eact***@gmail.com (구) → **ct1**@gmail.com (앞2·뒤2, 4자 이하 전체)
+ *   이름     홍길* (구) → 홍*동 (2자 뒤1, 3자↑ 가운데 전체)
+ *   전화     010-****-5678 (구) → 010-12**-56** (세그먼트별 뒤 2자리)
+ *   IPv4     192.168.***.1 (3번째 옥텟) / IPv6 마지막 그룹, 콤마 다중 IP 지원
+ * 
+ */ public class MaskingUtils { + + private MaskingUtils() {} + /** * 이메일 주소의 ID 부분을 마스킹 처리 - * 예: eactive@gmail.com => eact***@gmail.com + * 예: aajjbacc@gmail.com => **jjba**@gmail.com */ public static String maskEmailId(String email) { - if (email == null || email.isEmpty() || !email.contains("@")) { - return email; - } - - String[] parts = email.split("@"); - String id = parts[0]; - String domain = parts[1]; - - if (id.length() <= 3) { - return "***" + "@" + domain; - } - - String maskedId = id.substring(0, id.length() - 3) + "***"; - return maskedId + "@" + domain; + return com.eactive.eai.common.util.MaskingUtils.maskEmail(email); } /** - * 이름의 마지막 글자를 마스킹 처리 + * 이름 마스킹 처리 (2자: 뒤 1자리, 3자 이상: 앞뒤 제외 가운데 전체) + * 예: 홍길동 => 홍*동 */ public static String maskName(String name) { - if (name == null || name.isEmpty() || name.length() < 2) { - return name; - } - - return name.substring(0, name.length() - 1) + "*"; + return com.eactive.eai.common.util.MaskingUtils.maskKoreanName(name); } /** - * 전화번호 마스킹 처리 - * 예: 010-1234-5678 => 010-****-5678 + * 전화번호/휴대폰/FAX 마스킹 처리 (2·3번째 세그먼트 각각 뒤 2자리) + * 예: 010-1234-5678 => 010-12**-56** */ public static String maskPhoneNumber(String phoneNumber) { - if (phoneNumber == null || phoneNumber.isEmpty()) { - return phoneNumber; - } - - String[] parts = phoneNumber.split("-"); - if (parts.length != 3) { - return phoneNumber; - } - - return parts[0] + "-****-" + parts[2]; + return com.eactive.eai.common.util.MaskingUtils.maskPhoneNumber(phoneNumber); } /** - * IP 주소 마스킹 처리 - * 예: 192.168.1.1 => 192.168.*.* + * IP 주소 마스킹 처리 (IPv4 3번째 옥텟 / IPv6 마지막 그룹, 콤마 다중 IP 지원) + * 예: 192.168.1.1 => 192.168.***.1 */ public static String maskIpAddress(String ipAddresses) { - if (StringUtils.isBlank(ipAddresses)) { - return ipAddresses; - } - - String[] ips = ipAddresses.split(","); - List maskedIps = Arrays.stream(ips) - .map(ip -> { - String[] parts = ip.trim().split("\\."); - if (parts.length == 4) { - // 세 번째 octet을 ***로 마스킹 - parts[2] = "***"; - return String.join(".", parts); - } - return ip; // 유효하지 않은 IP 형식은 그대로 반환 - }) - .collect(Collectors.toList()); - - return String.join(",", maskedIps); + return com.eactive.eai.common.util.MaskingUtils.maskIpAddress(ipAddresses); } } diff --git a/src/test/java/com/eactive/eai/rms/onl/apim/masking/MaskingUtilsTest.java b/src/test/java/com/eactive/eai/rms/onl/apim/masking/MaskingUtilsTest.java new file mode 100644 index 0000000..b13918c --- /dev/null +++ b/src/test/java/com/eactive/eai/rms/onl/apim/masking/MaskingUtilsTest.java @@ -0,0 +1,128 @@ +package com.eactive.eai.rms.onl.apim.masking; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * eapim-admin {@link MaskingUtils}가 보안아키텍처 표준 규칙(elink-online-core)으로 + * 위임되어 동작하는지 검증한다. + */ +class MaskingUtilsTest { + + // --- 이메일 (maskEmailId) --- + + @Test + @DisplayName("이메일 — 아이디 앞2·뒤2 마스킹") + void maskEmailId_normal() { + assertEquals("**st1**@test.com", MaskingUtils.maskEmailId("test123@test.com")); + } + + @Test + @DisplayName("이메일 — 아이디 4자: 전체 마스킹") + void maskEmailId_fourChars() { + assertEquals("****@test.com", MaskingUtils.maskEmailId("test@test.com")); + } + + @Test + @DisplayName("이메일 — 아이디 1자: 전체 마스킹") + void maskEmailId_oneChar() { + assertEquals("*@123.com", MaskingUtils.maskEmailId("1@123.com")); + } + + @Test + @DisplayName("이메일 — @ 없으면 원본 / null·빈문자 방어") + void maskEmailId_invalidAndNull() { + assertEquals("notanemail", MaskingUtils.maskEmailId("notanemail")); + assertNull(MaskingUtils.maskEmailId(null)); + assertEquals("", MaskingUtils.maskEmailId("")); + } + + // --- 이름 (maskName) --- + + @Test + @DisplayName("이름 — 2자: 뒤 1자리") + void maskName_2chars() { + assertEquals("가*", MaskingUtils.maskName("가나")); + } + + @Test + @DisplayName("이름 — 3자: 가운데 1자리") + void maskName_3chars() { + assertEquals("가*다", MaskingUtils.maskName("가나다")); + } + + @Test + @DisplayName("이름 — 4자: 앞뒤 제외 가운데 전체") + void maskName_4chars() { + assertEquals("가**라", MaskingUtils.maskName("가나다라")); + } + + @Test + @DisplayName("이름 — 5자: 앞뒤 제외 가운데 전체") + void maskName_5chars() { + assertEquals("가***마", MaskingUtils.maskName("가나다라마")); + } + + @Test + @DisplayName("이름 — 1자/null/빈문자 방어") + void maskName_oneAndNull() { + assertEquals("가", MaskingUtils.maskName("가")); + assertNull(MaskingUtils.maskName(null)); + assertEquals("", MaskingUtils.maskName("")); + } + + // --- 전화/휴대폰/Fax (maskPhoneNumber) --- + + @Test + @DisplayName("전화 — 2·3번째 세그먼트 각 뒤 2자리") + void maskPhoneNumber_normal() { + assertEquals("010-12**-12**", MaskingUtils.maskPhoneNumber("010-1234-1234")); + assertEquals("064-12**-56**", MaskingUtils.maskPhoneNumber("064-1234-5678")); + } + + @Test + @DisplayName("전화 — 하이픈 없는 11자리") + void maskPhoneNumber_noHyphen() { + assertEquals("01012**56**", MaskingUtils.maskPhoneNumber("01012345678")); + } + + @Test + @DisplayName("전화 — null/빈문자 방어") + void maskPhoneNumber_null() { + assertNull(MaskingUtils.maskPhoneNumber(null)); + assertEquals("", MaskingUtils.maskPhoneNumber("")); + } + + // --- IP (maskIpAddress) --- + + @Test + @DisplayName("IPv4 — 3번째 옥텟 마스킹") + void maskIpAddress_ipv4() { + assertEquals("172.0.***.1", MaskingUtils.maskIpAddress("172.0.0.1")); + assertEquals("192.168.***.1", MaskingUtils.maskIpAddress("192.168.100.1")); + } + + @Test + @DisplayName("IPv6 — 마지막 그룹 마스킹") + void maskIpAddress_ipv6() { + assertEquals( + "AAAA:BBBB:CCCC:DDDD:EEEE:FFFF:0000:****", + MaskingUtils.maskIpAddress("AAAA:BBBB:CCCC:DDDD:EEEE:FFFF:0000:1111")); + } + + @Test + @DisplayName("IP — 쉼표 구분 다중 IPv4") + void maskIpAddress_multiple() { + assertEquals("192.168.***.1,10.0.***.2", MaskingUtils.maskIpAddress("192.168.100.1,10.0.0.2")); + } + + @Test + @DisplayName("IP — null/빈문자 방어") + void maskIpAddress_null() { + assertNull(MaskingUtils.maskIpAddress(null)); + assertEquals("", MaskingUtils.maskIpAddress("")); + } +}