diff --git a/src/main/java/com/eactive/apim/portal/common/util/StringMaskingUtil.java b/src/main/java/com/eactive/apim/portal/common/util/StringMaskingUtil.java index c25609b..857122b 100644 --- a/src/main/java/com/eactive/apim/portal/common/util/StringMaskingUtil.java +++ b/src/main/java/com/eactive/apim/portal/common/util/StringMaskingUtil.java @@ -1,10 +1,24 @@ package com.eactive.apim.portal.common.util; +import java.util.Arrays; + /** * 문자열 마스킹 처리를 위한 유틸리티 클래스 + * + *

보안아키텍처 표준 마스킹 규칙(elink-online-core {@code MaskingUtils})과 동일한 알고리즘을 + * 구현한다. eapim-portal은 elink-online-core에 의존하지 않으므로 로직을 직접 포함한다. + * 소유자 본인이 자신의 정보를 조회하는 경우 마스킹하지 않는 owner 인지 동작은 유지한다.

+ * + *
+ *   이름     2자: 뒤 1자리(가나→가*) / 3자 이상: 앞뒤 제외 가운데 전체(가나다라→가**라)
+ *   이메일   아이디 앞2·뒤2 마스킹, 4자 이하 전체(test→****, test123→**st1**)
+ *   전화     2·3번째 세그먼트 각각 뒤 2자리(010-1234-1234→010-12**-12**)
+ * 
*/ public class StringMaskingUtil { + private static final char MASK = '*'; + // ID 비교 private static boolean isOwner(String ownerId, String currentUserId) { return currentUserId != null && ownerId != null && currentUserId.equals(ownerId); @@ -21,8 +35,15 @@ public class StringMaskingUtil { return name; } - return name.length() > 1 ? - name.substring(0, name.length() - 1) + "*" : name; + int len = name.length(); + if (len == 1) { + return name; + } + if (len == 2) { + return name.charAt(0) + String.valueOf(MASK); + } + // 3자 이상: 첫 자 + 가운데 전체 마스킹 + 마지막 자 + return name.charAt(0) + stars(len - 2) + name.charAt(len - 1); } // 이메일 마스킹 처리 @@ -31,32 +52,32 @@ public class StringMaskingUtil { return email; } - String[] parts = email.split("@", 2); - if (parts.length != 2) { - return email; + int atIdx = email.indexOf('@'); + String local = email.substring(0, atIdx); + String domain = email.substring(atIdx); + + if (local.length() <= 4) { + return stars(local.length()) + domain; } - - String localPart = parts[0]; - String maskedLocal; - - if (localPart.length() <= 2) { - maskedLocal = "***"; - } else { - maskedLocal = localPart.substring(0, localPart.length() - 3) + "***"; - } - - return maskedLocal + "@" + parts[1]; + return stars(2) + local.substring(2, local.length() - 2) + stars(2) + domain; } - // 휴대폰 번호 마스킹 처리 + // 휴대폰/전화/FAX 번호 마스킹 처리 public static String maskMobileNumber(String number, String ownerId, String currentUserId) { if (!isValidString(number) || isOwner(ownerId, currentUserId)) { return number; } - String[] parts = number.split("-", 3); - return parts.length == 3 ? - parts[0] + "-***-" + parts[2] : number; + boolean hasHyphen = number.contains("-"); + String normalized = hasHyphen ? number : normalizePhoneNumber(number); + String[] parts = normalized.split("-"); + if (parts.length == 3) { + String m0 = parts[0]; + String m1 = maskSegmentTail(parts[1], 2); + String m2 = maskSegmentTail(parts[2], 2); + return hasHyphen ? m0 + "-" + m1 + "-" + m2 : m0 + m1 + m2; + } + return number; } // 기존 메서드 오버로딩 (하위 호환성) @@ -71,4 +92,41 @@ public class StringMaskingUtil { public static String maskMobileNumber(String number) { return maskMobileNumber(number, null, null); } -} \ No newline at end of file + + // ========================================================================= + // private helpers + // ========================================================================= + + private static String stars(int count) { + if (count <= 0) return ""; + char[] arr = new char[count]; + Arrays.fill(arr, MASK); + return new String(arr); + } + + private static String maskSegmentTail(String segment, int count) { + if (segment.length() <= count) return stars(segment.length()); + return segment.substring(0, segment.length() - count) + stars(count); + } + + /** + * 한국 전화번호(숫자만) → XXX-XXXX-XXXX 형식으로 정규화 + *
+     *   02 + 7자리  → 02-XXX-XXXX
+     *   02 + 8자리  → 02-XXXX-XXXX
+     *   0XX + 7자리 → 0XX-XXX-XXXX
+     *   0XX + 8자리 → 0XX-XXXX-XXXX
+     * 
+ */ + private static String normalizePhoneNumber(String digits) { + int len = digits.length(); + if (digits.startsWith("02")) { + if (len == 9) return digits.substring(0, 2) + "-" + digits.substring(2, 5) + "-" + digits.substring(5); + if (len == 10) return digits.substring(0, 2) + "-" + digits.substring(2, 6) + "-" + digits.substring(6); + } else if (digits.startsWith("0")) { + if (len == 10) return digits.substring(0, 3) + "-" + digits.substring(3, 6) + "-" + digits.substring(6); + if (len == 11) return digits.substring(0, 3) + "-" + digits.substring(3, 7) + "-" + digits.substring(7); + } + return digits; + } +} diff --git a/src/test/java/com/eactive/apim/portal/common/util/StringMaskingUtilTest.java b/src/test/java/com/eactive/apim/portal/common/util/StringMaskingUtilTest.java new file mode 100644 index 0000000..dae0e2e --- /dev/null +++ b/src/test/java/com/eactive/apim/portal/common/util/StringMaskingUtilTest.java @@ -0,0 +1,116 @@ +package com.eactive.apim.portal.common.util; + +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-portal {@link StringMaskingUtil}가 보안아키텍처 표준 규칙으로 마스킹하며, + * 소유자 본인 조회 시에는 마스킹하지 않는 동작을 검증한다. + */ +class StringMaskingUtilTest { + + // --- 이름 --- + + @Test + @DisplayName("이름 — 2자: 뒤 1자리") + void maskName_2chars() { + assertEquals("가*", StringMaskingUtil.maskName("가나")); + } + + @Test + @DisplayName("이름 — 3자: 가운데 1자리") + void maskName_3chars() { + assertEquals("가*다", StringMaskingUtil.maskName("가나다")); + } + + @Test + @DisplayName("이름 — 4자: 앞뒤 제외 가운데 전체") + void maskName_4chars() { + assertEquals("가**라", StringMaskingUtil.maskName("가나다라")); + } + + @Test + @DisplayName("이름 — 5자: 앞뒤 제외 가운데 전체") + void maskName_5chars() { + assertEquals("가***마", StringMaskingUtil.maskName("가나다라마")); + } + + @Test + @DisplayName("이름 — 1자/null/빈문자 방어") + void maskName_oneAndNull() { + assertEquals("가", StringMaskingUtil.maskName("가")); + assertNull(StringMaskingUtil.maskName(null)); + assertEquals("", StringMaskingUtil.maskName("")); + } + + // --- 이메일 --- + + @Test + @DisplayName("이메일 — 아이디 앞2·뒤2 마스킹") + void maskEmail_normal() { + assertEquals("**st1**@test.com", StringMaskingUtil.maskEmail("test123@test.com")); + } + + @Test + @DisplayName("이메일 — 아이디 4자: 전체 마스킹") + void maskEmail_fourChars() { + assertEquals("****@test.com", StringMaskingUtil.maskEmail("test@test.com")); + } + + @Test + @DisplayName("이메일 — 아이디 1자: 전체 마스킹") + void maskEmail_oneChar() { + assertEquals("*@123.com", StringMaskingUtil.maskEmail("1@123.com")); + } + + @Test + @DisplayName("이메일 — @ 없으면 원본 / null·빈문자 방어") + void maskEmail_invalidAndNull() { + assertEquals("notanemail", StringMaskingUtil.maskEmail("notanemail")); + assertNull(StringMaskingUtil.maskEmail(null)); + assertEquals("", StringMaskingUtil.maskEmail("")); + } + + // --- 휴대폰/전화 --- + + @Test + @DisplayName("휴대폰 — 2·3번째 세그먼트 각 뒤 2자리") + void maskMobileNumber_normal() { + assertEquals("010-12**-12**", StringMaskingUtil.maskMobileNumber("010-1234-1234")); + assertEquals("010-12**-56**", StringMaskingUtil.maskMobileNumber("010-1234-5678")); + } + + @Test + @DisplayName("휴대폰 — 하이픈 없는 11자리") + void maskMobileNumber_noHyphen() { + assertEquals("01012**56**", StringMaskingUtil.maskMobileNumber("01012345678")); + } + + @Test + @DisplayName("휴대폰 — null/빈문자 방어") + void maskMobileNumber_null() { + assertNull(StringMaskingUtil.maskMobileNumber(null)); + assertEquals("", StringMaskingUtil.maskMobileNumber("")); + } + + // --- owner 인지 (본인 조회 시 비마스킹) --- + + @Test + @DisplayName("owner — 본인 조회 시 마스킹하지 않음") + void ownerAware_noMaskWhenOwner() { + assertEquals("가나다", StringMaskingUtil.maskName("가나다", "user1", "user1")); + assertEquals("test123@test.com", StringMaskingUtil.maskEmail("test123@test.com", "user1", "user1")); + assertEquals("010-1234-1234", StringMaskingUtil.maskMobileNumber("010-1234-1234", "user1", "user1")); + } + + @Test + @DisplayName("owner — 타인 조회 시 마스킹") + void ownerAware_maskWhenNotOwner() { + assertEquals("가*다", StringMaskingUtil.maskName("가나다", "user1", "user2")); + assertEquals("**st1**@test.com", StringMaskingUtil.maskEmail("test123@test.com", "user1", "user2")); + assertEquals("010-12**-12**", StringMaskingUtil.maskMobileNumber("010-1234-1234", "user1", "user2")); + } +}