From eb74a99a5ac970d6118e0467451aab2eefb559b4 Mon Sep 17 00:00:00 2001
From: Rinjae
Date: Wed, 5 Aug 2026 17:47:37 +0900
Subject: [PATCH 1/4] =?UTF-8?q?-=20PersonalDataEncryptConverter:=20`isFake?=
=?UTF-8?q?Mode`=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80=20-=20?=
=?UTF-8?q?LegacyEncryptionMigration:=20=ED=97=88=EC=9A=A9=20IP=20?=
=?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20?=
=?UTF-8?q?-=20=EC=8A=A4=ED=82=B5=20=EB=82=B4=EC=97=AD=20=EB=A1=9C?=
=?UTF-8?q?=EA=B7=B8=20=EA=B0=95=ED=99=94=20-=20=EC=95=94=ED=98=B8?=
=?UTF-8?q?=ED=99=94=20=EB=8C=80=EC=83=81=20=EC=BB=AC=EB=9F=BC=20=EC=B6=94?=
=?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20DamoMode=20=EA=B5=AC=EB=B6=84=20?=
=?UTF-8?q?=EC=A7=80=EC=9B=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../LegacyEncryptionMigrationController.java | 104 ++++++++++++++----
1 file changed, 83 insertions(+), 21 deletions(-)
diff --git a/src/main/java/com/eactive/apim/portal/common/migration/LegacyEncryptionMigrationController.java b/src/main/java/com/eactive/apim/portal/common/migration/LegacyEncryptionMigrationController.java
index c539cbc..c9d4680 100644
--- a/src/main/java/com/eactive/apim/portal/common/migration/LegacyEncryptionMigrationController.java
+++ b/src/main/java/com/eactive/apim/portal/common/migration/LegacyEncryptionMigrationController.java
@@ -2,6 +2,7 @@ package com.eactive.apim.portal.common.migration;
import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.eactive.apim.portal.jpa.PersonalDataEncryptConverter;
+import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
@@ -20,6 +21,8 @@ import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
/**
* [임시] 레거시 평문 데이터를 {@link PersonalDataEncryptConverter} 규칙으로 일괄 정규화(암호화)하는 운영 도구.
@@ -29,15 +32,19 @@ import java.util.Map;
* 쓰기에서 무조건 인코딩하므로, {@code convertToDatabaseColumn(convertToEntityAttribute(x))}는
* 평문→인코딩, 인코딩→동일값(멱등)으로 정규화된다. 이 값이 기존과 다를 때만 UPDATE 한다.
*
- * 보안: 오직 127.0.0.1(localhost)에서 직접 호출한 요청만 허용한다. 기본은 dry-run(미변경)이며,
- * 실제 실행은 {@code dryRun=false}를 명시해야 한다. 작업 완료 후 이 클래스는 제거한다.
+ * 보안: PTL_PROPERTY {@code Portal / migration.internal.allow-ips} 허용 IP 목록(콤마 구분,
+ * 기본 loopback)에 포함된 IP 의 직접 호출만 허용한다 ({@code MenuInternalController} 모델).
+ * 운영 서버는 bind IP 가 NIC IP 라 loopback 호출이 불가하므로, 실행 전 property 에 호출자 IP 를
+ * 추가하고 작업 완료 후 원복한다. 프록시 경유(X-Forwarded-For 존재) 요청은 거부한다.
+ * 기본은 dry-run(미변경)이며, 실제 실행은 {@code dryRun=false}를 명시해야 한다.
+ * 작업 완료 후 이 클래스는 제거한다.
*
*
* # 미리보기(변경 안 함)
* curl -X POST 'http://127.0.0.1:39130/internal/migration/encrypt-legacy'
* # 실제 실행 (PII 컬럼)
* curl -X POST 'http://127.0.0.1:39130/internal/migration/encrypt-legacy?dryRun=false'
- * # audit 컬럼(created_by/last_modified_by, 15개 테이블)까지 포함
+ * # audit 컬럼(created_by/last_modified_by, 19개 테이블)까지 포함
* curl -X POST 'http://127.0.0.1:39130/internal/migration/encrypt-legacy?dryRun=false&includeAudit=true'
*
*/
@@ -46,13 +53,14 @@ import java.util.Map;
@RequestMapping("/internal/migration")
public class LegacyEncryptionMigrationController {
- /** PII 직접 컬럼 (로그인/검색에 직접 영향) */
+ /** PII 직접 컬럼 (로그인/검색에 직접 영향). ofctelno 는 admin(UnifbwkManService)이 컨버터를 수동 호출해 암호화하는 컬럼 */
private static final List PII_TARGETS = Arrays.asList(
new TargetTable("PTL_USER", Arrays.asList("login_id", "email_addr", "phone_number", "mobile_number")),
new TargetTable("PTL_MESSAGE_REQUEST", Arrays.asList("email", "phone")),
- new TargetTable("tseairm02", Arrays.asList("cphnno", "emad")),
+ new TargetTable("tseairm02", Arrays.asList("cphnno", "emad", "ofctelno")),
new TargetTable("PTL_USER_LOG", Arrays.asList("login_id")),
- new TargetTable("PTL_TWO_FACTOR_AUTH", Arrays.asList("recipient"))
+ new TargetTable("PTL_TWO_FACTOR_AUTH", Arrays.asList("recipient")),
+ new TargetTable("PTL_USER_INVITATION", Arrays.asList("INVITATION_MOBILE"))
);
/** Auditable(@MappedSuperclass) 상속 테이블의 감사 컬럼 (옵션) */
@@ -61,16 +69,24 @@ public class LegacyEncryptionMigrationController {
"DJB_APISTATUS_INCIDENT", "DJB_APISTATUS_INCIDENT_TIMELINE", "DJB_APISTATUS_INCIDENT_API",
"ptl_file", "PTL_MESSAGE_TEMPLATE", "ptl_notice", "ptl_terms",
"ptl_user_privacy_policy_agreement", "ptl_approval_line",
- "PTL_INQUIRY_COMMENT", "ptl_inquiry", "ptl_partnership_application"
+ "PTL_INQUIRY_COMMENT", "ptl_inquiry", "ptl_partnership_application",
+ "PTL_MENU_ITEM", "PTL_MENU_PLACEMENT", "PTL_ROLE", "PTL_ROLE_AUTHORITY"
);
private static final List AUDIT_COLUMNS = Arrays.asList("created_by", "last_modified_by");
+ static final String PROP_GROUP = "Portal";
+ static final String PROP_ALLOW_IPS = "migration.internal.allow-ips";
+ static final String DEFAULT_ALLOW_IPS = "127.0.0.1,::1";
+
private final JdbcTemplate jdbcTemplate;
+ private final PortalPropertyService portalPropertyService;
private final PersonalDataEncryptConverter converter = new PersonalDataEncryptConverter();
- public LegacyEncryptionMigrationController(@Qualifier("portalDataSource") DataSource emsDataSource) {
+ public LegacyEncryptionMigrationController(@Qualifier("portalDataSource") DataSource emsDataSource,
+ PortalPropertyService portalPropertyService) {
// EMS(EMSAPP) 스키마 데이터소스. 컨버터 적용 테이블은 모두 EMS에 존재한다.
this.jdbcTemplate = new JdbcTemplate(emsDataSource);
+ this.portalPropertyService = portalPropertyService;
}
@PostMapping("/encrypt-legacy")
@@ -78,7 +94,7 @@ public class LegacyEncryptionMigrationController {
public Map encryptLegacy(HttpServletRequest request,
@RequestParam(defaultValue = "true") boolean dryRun,
@RequestParam(defaultValue = "false") boolean includeAudit) {
- assertLocalOnly(request);
+ assertAllowedIp(request);
assertNotBypass();
List targets = new ArrayList<>(PII_TARGETS);
@@ -90,24 +106,36 @@ public class LegacyEncryptionMigrationController {
List> results = new ArrayList<>();
int totalChanged = 0;
+ int totalSkipped = 0;
for (TargetTable target : targets) {
for (String column : target.columns) {
Map r = processColumn(target.table, column, dryRun);
results.add(r);
totalChanged += (int) r.get("changed");
+ totalSkipped += (int) r.get("skipped");
}
}
Map response = new LinkedHashMap<>();
response.put("mode", dryRun ? "dry-run (변경 없음)" : "executed");
+ response.put("damoMode", resolveDamoMode());
response.put("includeAudit", includeAudit);
response.put("totalChanged", totalChanged);
+ // 정규화 결과가 빈 값이라 UPDATE 를 생략한 건수. 0 이 아니면 원인 조사 후 진행할 것.
+ response.put("totalSkipped", totalSkipped);
response.put("results", results);
- log.info("[레거시 암호화 마이그레이션] mode={} includeAudit={} totalChanged={}",
- dryRun ? "dry-run" : "executed", includeAudit, totalChanged);
+ log.info("[레거시 암호화 마이그레이션] mode={} includeAudit={} totalChanged={} totalSkipped={}",
+ dryRun ? "dry-run" : "executed", includeAudit, totalChanged, totalSkipped);
return response;
}
+ private String resolveDamoMode() {
+ if (converter.isBypassMode()) {
+ return "BYPASS";
+ }
+ return converter.isFakeMode() ? "FAKE" : "REAL";
+ }
+
/**
* 단일 (테이블, 컬럼)의 고유값을 정규화하고, 값이 바뀌는 경우에만 UPDATE.
*/
@@ -125,11 +153,13 @@ public class LegacyEncryptionMigrationController {
log.warn("[마이그레이션] 조회 실패 table={} column={} : {}", table, column, e.toString());
r.put("distinct", 0);
r.put("changed", 0);
+ r.put("skipped", 0);
r.put("error", e.getMessage());
return r;
}
int changed = 0;
+ int skipped = 0;
for (String value : values) {
String normalized;
try {
@@ -139,6 +169,13 @@ public class LegacyEncryptionMigrationController {
log.warn("[마이그레이션] 정규화 실패 table={} column={} : {}", table, column, e.toString());
continue;
}
+ if ((normalized == null || normalized.isEmpty()) && !value.isEmpty()) {
+ // 방어: 원본이 비어있지 않은데 정규화 결과가 빈 값 → 절대 UPDATE 하지 않음 (데이터 소실 방지)
+ skipped++;
+ log.warn("[마이그레이션] 정규화 결과가 빈 값 — UPDATE 생략 table={} column={} valueLen={}",
+ table, column, value.length());
+ continue;
+ }
if (normalized != null && !normalized.equals(value)) {
if (!dryRun) {
jdbcTemplate.update(
@@ -151,6 +188,7 @@ public class LegacyEncryptionMigrationController {
r.put("distinct", values.size());
r.put("changed", changed);
+ r.put("skipped", skipped);
return r;
}
@@ -169,21 +207,45 @@ public class LegacyEncryptionMigrationController {
}
/**
- * 127.0.0.1(localhost) 직접 호출만 허용. 프록시 경유(X-Forwarded-For 존재) 요청은 거부한다.
+ * PTL_PROPERTY({@code Portal / migration.internal.allow-ips}) 허용 IP 목록 검사.
+ * 프록시 경유(X-Forwarded-For 존재) 요청은 IP 신뢰 불가로 거부한다. ({@code MenuInternalController} 모델)
+ * 운영 서버는 bind IP 가 NIC IP 라 loopback 기본값으로는 호출 불가 — 실행 전 property 에
+ * 호출자 IP 를 추가하고 완료 후 원복한다.
*/
- private void assertLocalOnly(HttpServletRequest request) {
- String remote = request.getRemoteAddr();
- boolean localAddr = "127.0.0.1".equals(remote)
- || "0:0:0:0:0:0:0:1".equals(remote)
- || "::1".equals(remote);
+ private void assertAllowedIp(HttpServletRequest request) {
+ String remote = canonicalize(request.getRemoteAddr());
boolean viaProxy = request.getHeader("X-Forwarded-For") != null;
- if (!localAddr || viaProxy) {
- log.warn("[마이그레이션] 비로컬 접근 차단 remoteAddr={} xff={}",
- StringMaskingUtil.maskIpAddress(remote), StringMaskingUtil.maskIpAddress(request.getHeader("X-Forwarded-For")));
- throw new ResponseStatusException(HttpStatus.FORBIDDEN, "localhost(127.0.0.1) 직접 호출만 허용됩니다.");
+
+ Set allowed = Arrays.stream(resolveAllowIps().split(","))
+ .map(String::trim)
+ .filter(ip -> !ip.isEmpty())
+ .map(LegacyEncryptionMigrationController::canonicalize)
+ .collect(Collectors.toSet());
+
+ if (viaProxy || !allowed.contains(remote)) {
+ log.warn("[마이그레이션] 비허용 접근 차단 remoteAddr={} viaProxy={} xff={}",
+ StringMaskingUtil.maskIpAddress(remote), viaProxy,
+ StringMaskingUtil.maskIpAddress(request.getHeader("X-Forwarded-For")));
+ throw new ResponseStatusException(HttpStatus.FORBIDDEN,
+ "허용되지 않은 접근입니다. (PTL_PROPERTY " + PROP_GROUP + "/" + PROP_ALLOW_IPS + " 확인)");
}
}
+ private String resolveAllowIps() {
+ try {
+ return portalPropertyService.getOrCreateProperty(PROP_GROUP, PROP_ALLOW_IPS,
+ DEFAULT_ALLOW_IPS, "레거시 암호화 마이그레이션 내부 API 허용 IP 목록(콤마 구분)");
+ } catch (Exception e) {
+ log.warn("[마이그레이션] 허용 IP 목록 조회 실패 - 기본값({}) 사용", DEFAULT_ALLOW_IPS, e);
+ return DEFAULT_ALLOW_IPS;
+ }
+ }
+
+ /** IPv6 loopback 표기 통일 */
+ private static String canonicalize(String ip) {
+ return "0:0:0:0:0:0:0:1".equals(ip) ? "::1" : ip;
+ }
+
private static final class TargetTable {
final String table;
final List columns;
From b3fc5c06bf187aa90cbeb7cb513d7cf4305a35a6 Mon Sep 17 00:00:00 2001
From: Rinjae
Date: Thu, 6 Aug 2026 09:24:48 +0900
Subject: [PATCH 2/4] =?UTF-8?q?webhook=20=EC=88=98=EC=A0=95=20=ED=99=94?=
=?UTF-8?q?=EB=A9=B4=EC=97=90=EC=84=9C=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?=
=?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=9C=EB=A0=A5=20=EC=A0=9C=EA=B1=B0.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../templates/views/apps/webhook/webhookModifyStep1.html | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/main/resources/templates/views/apps/webhook/webhookModifyStep1.html b/src/main/resources/templates/views/apps/webhook/webhookModifyStep1.html
index f10bf1b..1b100ec 100644
--- a/src/main/resources/templates/views/apps/webhook/webhookModifyStep1.html
+++ b/src/main/resources/templates/views/apps/webhook/webhookModifyStep1.html
@@ -114,7 +114,6 @@
이벤트명
- CODE
이벤트 오류
From 34dce308dc6aeb62536296509ec802533b611397 Mon Sep 17 00:00:00 2001
From: Rinjae
Date: Thu, 6 Aug 2026 09:59:34 +0900
Subject: [PATCH 3/4] =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EB=82=B4=EB=B6=80=20A?=
=?UTF-8?q?PI=20=ED=97=88=EC=9A=A9=20IP=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C?=
=?UTF-8?q?=EC=A7=81=20=EA=B0=9C=EC=84=A0:=20-=20IpAddressMatcher=EB=A1=9C?=
=?UTF-8?q?=20CIDR/=EC=99=80=EC=9D=BC=EB=93=9C=EC=B9=B4=EB=93=9C=20?=
=?UTF-8?q?=EC=A7=80=EC=9B=90=20-=20=ED=97=88=EC=9A=A9=20IP=20=EB=AA=A9?=
=?UTF-8?q?=EB=A1=9D=20=EC=84=A4=EB=AA=85=20=ED=95=84=EB=93=9C=20=EC=B6=94?=
=?UTF-8?q?=EA=B0=80=20-=20RemoteAddr=20=EC=A0=95=EA=B7=9C=ED=99=94=20?=
=?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../djb/menu/MenuInternalController.java | 26 +++++++------------
1 file changed, 9 insertions(+), 17 deletions(-)
diff --git a/src/main/java/com/eactive/apim/portal/djb/menu/MenuInternalController.java b/src/main/java/com/eactive/apim/portal/djb/menu/MenuInternalController.java
index 1663d58..75ea49c 100644
--- a/src/main/java/com/eactive/apim/portal/djb/menu/MenuInternalController.java
+++ b/src/main/java/com/eactive/apim/portal/djb/menu/MenuInternalController.java
@@ -1,5 +1,6 @@
package com.eactive.apim.portal.djb.menu;
+import com.eactive.apim.portal.common.util.IpAddressMatcher;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -12,11 +13,8 @@ import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
/**
* 메뉴 캐시 내부 API — eapim-admin 의 reload 명령 수신용.
@@ -24,6 +22,8 @@ import java.util.stream.Collectors;
* 가드: PTL_PROPERTY {@code Portal / menu.internal.allow-ips} 허용 IP 목록
* (기본 loopback) + X-Forwarded-For 동반 요청 거부
* ({@code LegacyEncryptionMigrationController.assertLocalOnly} 모델).
+ * 허용 목록은 {@link IpAddressMatcher} 규칙을 따라 정확 일치 외에
+ * IPv4 CIDR({@code 172.30.1.0/24}) 과 옥텟 와일드카드({@code 172.30.*.*}) 를 지원한다.
* CSRF 는 PortalConfigSecurity 에서 {@code /internal/menu/**} 예외 처리.
*
* curl -X POST http://127.0.0.1:39130/internal/menu/reload
@@ -37,6 +37,9 @@ public class MenuInternalController {
static final String PROP_GROUP = "Portal";
static final String PROP_ALLOW_IPS = "menu.internal.allow-ips";
static final String DEFAULT_ALLOW_IPS = "127.0.0.1,::1";
+ static final String PROP_ALLOW_IPS_DESCRIPTION =
+ "메뉴 내부 API(리로드) 허용 IP 목록. 콤마(,)/세미콜론(;)/줄바꿈 구분, "
+ + "정확일치·IPv4 CIDR(172.30.1.0/24)·와일드카드(172.30.*.*) 지원";
private final MenuService menuService;
private final PortalPropertyService portalPropertyService;
@@ -66,16 +69,10 @@ public class MenuInternalController {
* 그 경우에도 allowlist 검사로 차단된다. WebLogic WAR 배포에서는 원 소켓 IP 로 검사된다.)
*/
private boolean isAllowed(HttpServletRequest request) {
- String remote = canonicalize(request.getRemoteAddr());
+ String remote = IpAddressMatcher.canonicalize(request.getRemoteAddr());
boolean viaProxy = request.getHeader("X-Forwarded-For") != null;
- Set allowed = Arrays.stream(resolveAllowIps().split(","))
- .map(String::trim)
- .filter(ip -> !ip.isEmpty())
- .map(MenuInternalController::canonicalize)
- .collect(Collectors.toSet());
-
- if (viaProxy || !allowed.contains(remote)) {
+ if (viaProxy || !IpAddressMatcher.matches(resolveAllowIps(), remote)) {
log.warn("메뉴 내부 API 차단 - remote: {}, viaProxy: {}", remote, viaProxy);
return false;
}
@@ -85,15 +82,10 @@ public class MenuInternalController {
private String resolveAllowIps() {
try {
return portalPropertyService.getOrCreateProperty(PROP_GROUP, PROP_ALLOW_IPS,
- DEFAULT_ALLOW_IPS, "메뉴 내부 API(리로드) 허용 IP 목록(콤마 구분)");
+ DEFAULT_ALLOW_IPS, PROP_ALLOW_IPS_DESCRIPTION);
} catch (Exception e) {
log.warn("허용 IP 목록 조회 실패 - 기본값({}) 사용", DEFAULT_ALLOW_IPS, e);
return DEFAULT_ALLOW_IPS;
}
}
-
- /** IPv6 loopback 표기 통일 */
- private static String canonicalize(String ip) {
- return "0:0:0:0:0:0:0:1".equals(ip) ? "::1" : ip;
- }
}
From c42a278c8ec6f5041c25128f74556de1a918a4e6 Mon Sep 17 00:00:00 2001
From: Rinjae
Date: Fri, 7 Aug 2026 09:20:39 +0900
Subject: [PATCH 4/4] =?UTF-8?q?=EC=88=98=EC=8B=A0=EC=9E=90=20=EC=9D=B4?=
=?UTF-8?q?=EB=A6=84=20=EC=A7=80=EC=A0=95=20=EC=9D=B8=EC=A6=9D=EB=B2=88?=
=?UTF-8?q?=ED=98=B8=20=EB=B0=9C=EC=86=A1=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?=
=?UTF-8?q?=EA=B0=80:=20-=20%USER=5FNAME%=20=EC=A7=80=EC=9B=90=20=EC=9C=84?=
=?UTF-8?q?=ED=95=9C=20createMessageRecipient=20=EC=88=98=EC=A0=95=20-=20G?=
=?UTF-8?q?UEST=5FUSER=5FNAME=20=EC=83=81=EC=88=98=20=EB=B0=8F=20=EA=B4=80?=
=?UTF-8?q?=EB=A0=A8=20=EA=B8=B0=EB=B3=B8=EA=B0=92=20=EC=B2=98=EB=A6=AC=20?=
=?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../apps/auth/service/AuthNumberService.java | 12 ++++++++++
.../auth/service/AuthNumberServiceImpl.java | 22 ++++++++++++++++---
.../apps/user/facade/AuthFacadeImpl.java | 8 ++++++-
3 files changed, 38 insertions(+), 4 deletions(-)
diff --git a/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberService.java b/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberService.java
index a142517..b747af2 100644
--- a/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberService.java
+++ b/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberService.java
@@ -4,11 +4,23 @@ public interface AuthNumberService {
String sendRequestAuthNumber(String recipientKey, String msgType);
+ /**
+ * 기본 TTL 로 발송하되 수신자 이름을 지정한다. 세 번째 인자가 int 인 오버로드(TTL 지정)와 혼동하지 말 것.
+ */
+ String sendRequestAuthNumber(String recipientKey, String msgType, String username);
+
/**
* 인증번호를 지정한 유효시간(초)으로 발송한다. 로그인/step-up 2FA 는 회원가입 기본 TTL 과
* 다른 값을 쓸 수 있으므로 호출부에서 TTL 을 지정한다.
*/
String sendRequestAuthNumber(String recipientKey, String msgType, int ttlSeconds);
+ /**
+ * 수신자 이름을 지정해 인증번호를 발송한다. 메시지 템플릿의 %USER_NAME% 치환에 사용되며,
+ * 회원가입·아이디/비밀번호 찾기처럼 사용자 이름을 알 수 없는 흐름은 "guest" 를 넘긴다.
+ * username 이 비어 있으면 %USER_NAME% 은 치환되지 않고 원문이 그대로 남는다.
+ */
+ String sendRequestAuthNumber(String recipientKey, String msgType, int ttlSeconds, String username);
+
boolean verifyAuthNumber(String recipientKey, String authNumber);
}
diff --git a/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberServiceImpl.java b/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberServiceImpl.java
index ce08043..b140f76 100644
--- a/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberServiceImpl.java
+++ b/src/main/java/com/eactive/apim/portal/apps/auth/service/AuthNumberServiceImpl.java
@@ -45,19 +45,31 @@ public class AuthNumberServiceImpl implements AuthNumberService {
@Override
@Transactional(noRollbackFor = AuthNumberException.class)
public String sendRequestAuthNumber(String recipientKey, String msgType) {
- return sendRequestAuthNumber(recipientKey, msgType, authNumberExpirationTime);
+ return sendRequestAuthNumber(recipientKey, msgType, authNumberExpirationTime, null);
+ }
+
+ @Override
+ @Transactional(noRollbackFor = AuthNumberException.class)
+ public String sendRequestAuthNumber(String recipientKey, String msgType, String username) {
+ return sendRequestAuthNumber(recipientKey, msgType, authNumberExpirationTime, username);
}
@Override
@Transactional(noRollbackFor = AuthNumberException.class)
public String sendRequestAuthNumber(String recipientKey, String msgType, int ttlSeconds) {
+ return sendRequestAuthNumber(recipientKey, msgType, ttlSeconds, null);
+ }
+
+ @Override
+ @Transactional(noRollbackFor = AuthNumberException.class)
+ public String sendRequestAuthNumber(String recipientKey, String msgType, int ttlSeconds, String username) {
logger.info("Sending auth number to: {} via {} (ttl={}s)", recipientKey, msgType, ttlSeconds);
validateResendTime(recipientKey);
String authNumber = generator.generateAuthNumber();
- MessageRecipient recipient = createMessageRecipient(recipientKey, msgType);
+ MessageRecipient recipient = createMessageRecipient(recipientKey, msgType, username);
messageSender.sendAuthMessage(recipient, authNumber, msgType);
storage.saveAuthNumber(recipientKey, authNumber,
@@ -99,9 +111,13 @@ public class AuthNumberServiceImpl implements AuthNumberService {
});
}
- private MessageRecipient createMessageRecipient(String recipientKey, String msgType) {
+ private MessageRecipient createMessageRecipient(String recipientKey, String msgType, String username) {
MessageRecipient recipient = new MessageRecipient();
recipient.setUserId(recipientKey);
+ // 메시지 템플릿 %USER_NAME% 치환용. 비어 있으면 MessageSendService 가 파라미터 자체를 넣지 않는다.
+ if (username != null && !username.trim().isEmpty()) {
+ recipient.setUsername(username);
+ }
if ("SMS".equalsIgnoreCase(msgType)) {
recipient.setPhone(recipientKey);
} else if ("EMAIL".equalsIgnoreCase(msgType)) {
diff --git a/src/main/java/com/eactive/apim/portal/apps/user/facade/AuthFacadeImpl.java b/src/main/java/com/eactive/apim/portal/apps/user/facade/AuthFacadeImpl.java
index b4ec6b1..0c3776e 100644
--- a/src/main/java/com/eactive/apim/portal/apps/user/facade/AuthFacadeImpl.java
+++ b/src/main/java/com/eactive/apim/portal/apps/user/facade/AuthFacadeImpl.java
@@ -17,6 +17,12 @@ public class AuthFacadeImpl implements AuthFacade {
private final AuthNoticeProperties authNoticeProperties;
private static final Logger log = LoggerFactory.getLogger(AuthFacadeImpl.class);
+ /**
+ * 회원가입·아이디/비밀번호 찾기 등 로그인 이전 흐름은 수신자 이름을 알 수 없으므로
+ * 메시지 템플릿 %USER_NAME% 자리에 넣을 기본값.
+ */
+ private static final String GUEST_USER_NAME = "guest";
+
/**
* 인증 요청
@@ -43,7 +49,7 @@ public class AuthFacadeImpl implements AuthFacade {
}
try {
- String generatedAuthNumber = authNumberService.sendRequestAuthNumber(recipientKey, msgType);
+ String generatedAuthNumber = authNumberService.sendRequestAuthNumber(recipientKey, msgType, GUEST_USER_NAME);
response.setValid(true);
response.setMessage("인증번호를 발송하였습니다.");
// 테스트 환경(PTL_PROPERTY auth.test-notice.enabled=true, prod 제외)에서만 인증번호를 응답에 노출